Spaces:
Sleeping
Sleeping
| import type { Page } from 'playwright'; | |
| import { logger } from '../../utils/logger'; | |
| import { SELECTORS } from '../selectors'; | |
| import { acceptEulaEverywhere } from './login'; | |
| export interface DropClassResult { | |
| attempted: boolean; | |
| dropped: boolean; | |
| reason?: string; | |
| } | |
| /** | |
| * Drop a class from the Turnitin student home page. This is used only after a | |
| * hard submission quota limit so exhausted accounts do not keep showing stale | |
| * class entries during future quota scans. | |
| */ | |
| export async function dropClassByTitle( | |
| page: Page, | |
| classTitle: string, | |
| ): Promise<DropClassResult> { | |
| if (!classTitle || classTitle.trim() === '') { | |
| return { | |
| attempted: false, | |
| dropped: false, | |
| reason: 'Class title is empty', | |
| }; | |
| } | |
| logger.info('Attempting to drop quota-limited class', { classTitle }); | |
| await page.goto('https://www.turnitin.com/s_home.asp?lang=en_us', { | |
| waitUntil: 'domcontentloaded', | |
| timeout: 60000, | |
| }); | |
| await page.waitForTimeout(1500); | |
| await acceptEulaEverywhere(page); | |
| const classLink = page | |
| .locator(SELECTORS.class.classNameLink) | |
| .filter({ hasText: classTitle }) | |
| .first(); | |
| if (!(await classLink.isVisible({ timeout: 10000 }).catch(() => false))) { | |
| logger.info('Class is already absent from Turnitin home', { classTitle }); | |
| return { | |
| attempted: true, | |
| dropped: true, | |
| reason: 'Class already absent', | |
| }; | |
| } | |
| const classRow = page.locator('tr').filter({ hasText: classTitle }).first(); | |
| const deleteLink = classRow | |
| .locator('td.class_delete a, a[href*="confirmDropClass"]') | |
| .first(); | |
| if (!(await deleteLink.isVisible({ timeout: 5000 }).catch(() => false))) { | |
| logger.warn('Class delete link was not found', { classTitle }); | |
| return { | |
| attempted: true, | |
| dropped: false, | |
| reason: 'Delete link not found', | |
| }; | |
| } | |
| let dialogAccepted = false; | |
| const dialogHandler = async (dialog: any) => { | |
| dialogAccepted = true; | |
| await dialog.accept().catch(() => {}); | |
| }; | |
| page.once('dialog', dialogHandler); | |
| await Promise.all([ | |
| page | |
| .waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }) | |
| .catch(() => null), | |
| deleteLink.click({ force: true }), | |
| ]); | |
| await page.waitForTimeout(2500); | |
| if (!dialogAccepted) { | |
| logger.warn('Drop class confirmation dialog was not observed', { | |
| classTitle, | |
| }); | |
| } | |
| await page.goto('https://www.turnitin.com/s_home.asp?lang=en_us', { | |
| waitUntil: 'domcontentloaded', | |
| timeout: 60000, | |
| }); | |
| await page.waitForTimeout(1500); | |
| const stillVisible = await page | |
| .locator(SELECTORS.class.classNameLink) | |
| .filter({ hasText: classTitle }) | |
| .first() | |
| .isVisible({ timeout: 5000 }) | |
| .catch(() => false); | |
| if (stillVisible) { | |
| logger.warn('Class still visible after delete attempt', { classTitle }); | |
| return { | |
| attempted: true, | |
| dropped: false, | |
| reason: 'Class still visible after delete', | |
| }; | |
| } | |
| logger.info('Quota-limited class dropped successfully', { classTitle }); | |
| return { attempted: true, dropped: true }; | |
| } | |