File size: 4,216 Bytes
d5c9d40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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();
}