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 { 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; }