Spaces:
Sleeping
Sleeping
File size: 5,002 Bytes
521a9b6 16e3957 521a9b6 16e3957 521a9b6 | 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;
}
|