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 { 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 { const viewerPage = await openReportViewerPage(page, context); return viewerPage.url(); }