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