| 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'; |
|
|
| |
| |
| |
|
|
| 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; |
| |
| |
| |
| let consecutiveNoEula = 0; |
| while (Date.now() < deadline) { |
| const foundEula = await acceptEulaEverywhere(page); |
| accepted = foundEula || accepted; |
| if (!foundEula) { |
| consecutiveNoEula++; |
| if (consecutiveNoEula >= 2) break; |
| } else { |
| consecutiveNoEula = 0; |
| } |
| await page.waitForTimeout(1200); |
| } |
| return accepted; |
| } |
|
|
| |
| |
| |
|
|
| 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'; |
|
|
| |
| |
| |
| |
| 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(() => {}); |
|
|
| |
| 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); |
|
|
| |
| if (page.url().includes('user_user_type.asp')) { |
| await page.goto(STUDENT_HOME_URL, { |
| waitUntil: 'domcontentloaded', |
| timeout: 60000, |
| }); |
| await acceptEulaEverywhere(page); |
| } |
|
|
| |
| if (page.url().includes('login_page.asp')) { |
| throw new Error(`Login failed on Turnitin: redirected back to login page (URL: ${page.url()})`); |
| } |
|
|
| |
| if (storageStatePath) { |
| await page.context().storageState({ path: storageStatePath }); |
| logger.info('Storage state saved', { path: storageStatePath }); |
| } |
|
|
| logger.info('Login successful', { url: page.url() }); |
| } |
|
|