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