Spaces:
Sleeping
Sleeping
| 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<boolean> { | |
| 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<boolean> { | |
| 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<boolean> { | |
| 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<void> { | |
| 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() }); | |
| } | |