File size: 5,002 Bytes
9e212e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28316e4
 
 
9e212e3
 
28316e4
 
 
 
 
 
9e212e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import type { Page, Frame } from 'playwright';
import { logger } from '../../utils/logger';
import { SELECTORS, deepClickByText } from '../selectors';

type Scope = Page | Frame;

export interface SubmissionDetails {
  studentId?: string;
  className?: string;
  classId?: string;
  submissionId?: string;
  submissionDate?: string;
  submissionCount?: number;
  fileName?: string;
  fileExtension?: string;
  fileSize?: number;
  charCount?: number;
  wordCount?: number;
  pageCount?: number;
  pageCountSource?: 'pdf' | 'docx_metadata';
  expectedWaitMinutes?: number;
  largeDocument?: boolean;
}

type ParsedSubmissionDetailKey = Exclude<
  keyof SubmissionDetails,
  'pageCountSource' | 'expectedWaitMinutes' | 'largeDocument'
>;

function normalizeKey(label: string): ParsedSubmissionDetailKey | null {
  const normalized = label.toLowerCase().replace(/\s+/g, ' ').trim();
  if (normalized === 'student id') return 'studentId';
  if (normalized === 'class name') return 'className';
  if (normalized === 'class id') return 'classId';
  if (normalized === 'submission id') return 'submissionId';
  if (normalized === 'submission date') return 'submissionDate';
  if (normalized === 'submission count') return 'submissionCount';
  if (normalized === 'file name') return 'fileName';
  if (normalized === 'file extension') return 'fileExtension';
  if (normalized === 'file size') return 'fileSize';
  if (normalized === 'char count') return 'charCount';
  if (normalized === 'character count') return 'charCount';
  if (normalized === 'word count') return 'wordCount';
  if (normalized === 'page count') return 'pageCount';
  return null;
}

function parseNumber(value: string): number | undefined {
  const numeric = Number(String(value || '').replace(/[^\d]/g, ''));
  return Number.isFinite(numeric) ? numeric : undefined;
}

/**
 * Open the Turnitin viewer details panel and read file metadata. The worker
 * emits these details to job events so the tracking page can show file facts
 * before/while the final report is downloaded.
 */
export async function readSubmissionDetails(
  page: Scope,
): Promise<SubmissionDetails | null> {
  const detailsButton = page
    .locator(
      [
        'tii-sws-submission-details-btn tdl-labeled-button',
        'tii-sws-submission-details-btn',
        'tii-sws-header [slot="submission-details-btn"]',
        'tdl-labeled-button[withdatapx="SubmissionDetailsMenuClicked"]',
        '[withdatapx="SubmissionDetailsMenuClicked"]',
        'button:has-text("Details")',
        'tdl-labeled-button:has-text("Details")',
      ].join(', '),
    )
    .first();

  if (!(await detailsButton.isVisible({ timeout: 10000 }).catch(() => false))) {
    const clickedByText = await deepClickByText(page, ['details']).catch(
      () => false,
    );
    if (!clickedByText) {
      logger.warn('Submission details button was not visible');
      return null;
    }
  } else {
    await detailsButton.click({ force: true });
  }

  await page.waitForTimeout(1200).catch(() => {});

  const fileTab = page
    .locator(
      [
        'tii-sws-tab-button#sub-details-tab-file',
        '#sub-details-tab-file',
        '.submission-details-tab:has-text("File")',
        '[role="tab"]:has-text("File")',
      ].join(', '),
    )
    .first();
  if (await fileTab.isVisible({ timeout: 5000 }).catch(() => false)) {
    await fileTab.click({ force: true }).catch(() => {});
    await page.waitForTimeout(700).catch(() => {});
  } else {
    await deepClickByText(page, ['file']).catch(() => false);
    await page.waitForTimeout(700).catch(() => {});
  }

  const details = await page
    .locator('#tii-sws-submission-details-list')
    .first()
    .evaluate((list) => {
      return Array.from(list.querySelectorAll('.submission-details-item')).map(
        (item) => {
          const term = item
            .querySelector('[role="term"]')
            ?.textContent?.trim() || '';
          const value = item
            .querySelector('[role="definition"], .submission-details-value')
            ?.textContent?.trim() || '';
          return { term, value };
        },
      );
    })
    .catch(() => []);

  const result: SubmissionDetails = {};
  for (const row of details) {
    const key = normalizeKey(row.term);
    if (!key) continue;
    if (
      key === 'fileName' ||
      key === 'fileExtension' ||
      key === 'studentId' ||
      key === 'className' ||
      key === 'classId' ||
      key === 'submissionId' ||
      key === 'submissionDate'
    ) {
      result[key] = row.value;
    } else {
      const parsed = parseNumber(row.value);
      if (parsed !== undefined) result[key] = parsed;
    }
  }

  await page
    .locator(SELECTORS.filters.similarityTab)
    .first()
    .click({ force: true, timeout: 3000 })
    .catch(() => {});
  await page.waitForTimeout(800).catch(() => {});

  if (Object.keys(result).length === 0) return null;
  logger.info('Read submission details from viewer', { ...result });
  return result;
}