import type { Page, Frame } from 'playwright'; import { logger } from '../../utils/logger'; import { EULA_SELECTORS, EULA_BODY_REGEX, EULA_DEEP_CLICK_TEXTS, SELECTORS, clickVisibleCheckbox, deepClickByText, } from '../selectors'; // --------------------------------------------------------------------------- // EULA acceptance helpers // --------------------------------------------------------------------------- type Scope = Page | Frame; async function acceptEulaInScope(scope: Scope): Promise { for (const selector of EULA_SELECTORS) { const button = scope.locator(selector).first(); if (await button.isVisible({ timeout: 1500 }).catch(() => false)) { await button.click({ force: true }); await scope.waitForTimeout(2500).catch(() => {}); return true; } } const bodyText = await scope .locator('body') .innerText({ timeout: 1000 }) .catch(() => ''); if (EULA_BODY_REGEX.test(bodyText)) { await clickVisibleCheckbox(scope); if (await deepClickByText(scope, EULA_DEEP_CLICK_TEXTS)) { await scope.waitForTimeout(2500).catch(() => {}); return true; } } return false; } export async function acceptEulaEverywhere(page: Page): Promise { let accepted = await acceptEulaInScope(page); for (const frame of page.frames()) { accepted = (await acceptEulaInScope(frame).catch(() => false)) || accepted; } return accepted; } export async function acceptEulaUntilSettled( page: Page, timeoutMs = 15000, ): Promise { const deadline = Date.now() + timeoutMs; let accepted = false; // BUG-8 FIX: Break early when no EULA detected after 2 consecutive checks. // Previously the loop always ran for the full timeoutMs (15s), wasting ~30s // per job in the common case where no EULA is shown at all. let consecutiveNoEula = 0; while (Date.now() < deadline) { const foundEula = await acceptEulaEverywhere(page); accepted = foundEula || accepted; if (!foundEula) { consecutiveNoEula++; if (consecutiveNoEula >= 2) break; // No EULA on 2 consecutive checks — stop } else { consecutiveNoEula = 0; // Reset counter when EULA was found and accepted } await page.waitForTimeout(1200); } return accepted; } // --------------------------------------------------------------------------- // Login // --------------------------------------------------------------------------- const DEFAULT_TARGET_URL = 'https://www.turnitin.com/login_page.asp?lang=en_us'; const STUDENT_HOME_URL = 'https://www.turnitin.com/s_home.asp?lang=en_us'; /** * Log in to Turnitin with email/password, accept EULA if shown, * handle redirect to user-type page, and optionally save storage state. */ export async function loginToTurnitin( page: Page, email: string, password: string, storageStatePath?: string, targetUrl = DEFAULT_TARGET_URL, ): Promise { logger.info('Navigating to Turnitin login page'); let emailInputVisible = false; for (let attempt = 1; attempt <= 4; attempt++) { await page.goto(targetUrl || DEFAULT_TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 60000, }); emailInputVisible = await page .locator(SELECTORS.login.emailInput) .isVisible({ timeout: 15000 }) .catch(() => false); if (emailInputVisible) break; await acceptEulaEverywhere(page); const bodyText = await page .locator('body') .innerText({ timeout: 3000 }) .catch(() => ''); const currentUrl = page.url(); if ( !currentUrl.includes('login_page.asp') || /logout|student|class portfolio/i.test(bodyText) ) { logger.info('Login form not shown; continuing with existing Turnitin session', { url: currentUrl, }); return; } if (/403 ERROR|Request blocked|could not be satisfied/i.test(bodyText)) { logger.warn('Turnitin login page was temporarily blocked; retrying', { attempt, }); await page.waitForTimeout(2500 + attempt * 1500); continue; } await page.waitForTimeout(1500); } if (!emailInputVisible) { throw new Error( `Turnitin login form was not visible (URL: ${page.url()})`, ); } await page.fill(SELECTORS.login.emailInput, email); await page.fill(SELECTORS.login.passwordInput, password); await Promise.all([ page .waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 60000 }) .catch(() => null), page.click(SELECTORS.login.submitButton), ]); await page .waitForLoadState('domcontentloaded', { timeout: 30000 }) .catch(() => {}); // Check if we are still on the login page (i.e. login failed) const currentUrl = page.url(); if (currentUrl.includes('login_page.asp')) { const errorText = await page .locator('.error, .error-message, #error_message_box, #error_box, td.errorText, .errorText') .first() .innerText({ timeout: 2000 }) .catch(() => ''); const cleanMsg = errorText ? errorText.trim().replace(/\s+/g, ' ') : 'Invalid email or password'; throw new Error(`Login failed on Turnitin: ${cleanMsg} (URL: ${currentUrl})`); } await acceptEulaEverywhere(page); // Handle redirect to user type page if (page.url().includes('user_user_type.asp')) { await page.goto(STUDENT_HOME_URL, { waitUntil: 'domcontentloaded', timeout: 60000, }); await acceptEulaEverywhere(page); } // Double check if we got kicked back to login page if (page.url().includes('login_page.asp')) { throw new Error(`Login failed on Turnitin: redirected back to login page (URL: ${page.url()})`); } // Save storage state if requested if (storageStatePath) { await page.context().storageState({ path: storageStatePath }); logger.info('Storage state saved', { path: storageStatePath }); } logger.info('Login successful', { url: page.url() }); }