relv-space4 / src /engine /steps /login.ts
relv-dev's picture
Deploy latest verified Turnitin worker
521a9b6 verified
Raw
History Blame Contribute Delete
5.94 kB
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() });
}