relv-space3 / src /engine /steps /viewer.ts
relv-dev's picture
Deploy latest verified Turnitin worker
9e212e3 verified
Raw
History Blame Contribute Delete
4.22 kB
import type { Page, BrowserContext, Frame, Locator } from 'playwright';
import { logger } from '../../utils/logger';
import {
SELECTORS,
deepClickByText,
resolveAssignmentScope,
} from '../selectors';
type ViewerScope = Page | Frame;
function ownerPage(scope: ViewerScope): Page {
return typeof (scope as any).page === 'function'
? (scope as Frame).page()
: (scope as Page);
}
function isReportSubmissionUrl(url: string): boolean {
return (
url.includes('reports.integrity.turnitin.com') &&
/submission-viewer|\/submission\//i.test(url)
);
}
function findReportViewerScope(context: BrowserContext): ViewerScope | null {
for (const candidatePage of context.pages()) {
if (isReportSubmissionUrl(candidatePage.url())) {
return candidatePage;
}
const reportFrame = candidatePage
.frames()
.find((frame) => isReportSubmissionUrl(frame.url()));
if (reportFrame) {
return reportFrame;
}
}
return null;
}
/**
* Click the submission title to open the report viewer.
* Returns the viewer page after it has navigated to reports.integrity.turnitin.com.
*/
export async function openReportViewerPage(
page: Page,
context: BrowserContext,
similarityScope?: ViewerScope,
similarityLocator?: Locator,
): Promise<ViewerScope> {
const scope = similarityScope || (await resolveAssignmentScope(page));
const titleButton = scope
.locator(SELECTORS.viewer.titleButton)
.first();
const specificTargetVisible = similarityLocator
? await similarityLocator.isVisible({ timeout: 3000 }).catch(() => false)
: false;
const clickTarget = specificTargetVisible
? similarityLocator!
: (await titleButton.isVisible({ timeout: 3000 }).catch(() => false))
? titleButton
: null;
const [newPage] = await Promise.all([
context
.waitForEvent('page', { timeout: 30000 })
.catch(() => null),
clickTarget
? clickTarget.click({ force: true }).catch(async () => {
await deepClickByText(scope, ['view submission']);
})
: deepClickByText(scope, ['view submission']),
]);
// BUG-6 FIX: Verify the captured page is actually the report viewer before
// waiting for its load state. context.waitForEvent('page') can fire for any
// new page (ads, redirects), so we must not blindly wait 30s on a wrong page.
let viewerScope: ViewerScope;
if (newPage && isReportSubmissionUrl(newPage.url())) {
// Correct page — wait for it to load normally
viewerScope = newPage;
await (viewerScope as Page)
.waitForLoadState('domcontentloaded', { timeout: 60000 })
.catch(() => {});
} else {
// Wrong page or no page event — scan existing pages/frames immediately
if (newPage) {
logger.warn('waitForEvent(page) captured a non-viewer page; scanning context for viewer', {
capturedUrl: newPage.url(),
});
}
// Give Turnitin a short moment to open the real viewer page
const scopeOwner = typeof (scope as any).page === 'function'
? (scope as Frame).page()
: (scope as Page);
await scopeOwner.waitForTimeout(3000).catch(() => {});
const existing = findReportViewerScope(context);
viewerScope = existing ||
(typeof (scope as any).page === 'function'
? (scope as Frame).page()
: (scope as Page));
}
const deadline = Date.now() + 30000;
while (
Date.now() < deadline &&
!isReportSubmissionUrl(viewerScope.url())
) {
const existing = findReportViewerScope(context);
if (existing) {
viewerScope = existing;
break;
}
await ownerPage(viewerScope).waitForTimeout(500).catch(() => {});
}
await ownerPage(viewerScope).waitForTimeout(2500);
const viewerUrl = viewerScope.url();
logger.info('Report viewer opened', { viewerUrl });
if (!isReportSubmissionUrl(viewerUrl)) {
throw new Error(
`Report viewer did not open a submission route correctly. Current URL: ${viewerUrl}`,
);
}
return viewerScope;
}
export async function openReportViewer(
page: Page,
context: BrowserContext,
): Promise<string> {
const viewerPage = await openReportViewerPage(page, context);
return viewerPage.url();
}