Harden flow.ts click handling for login flows
- Improve text click matching with exact/partial patterns - Add fallback login navigation when click target is JS/non-clickable - Add Corriere-specific fallback to /account/login - Keep flow resilient when click does not trigger navigation
This commit is contained in:
@@ -102,9 +102,77 @@ function parseInstruction(instruction: string): Step[] {
|
|||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickByText(page: Page, text: string) {
|
function escapeRegExp(value: string): string {
|
||||||
const loc = page.getByRole('button', { name: text }).or(page.getByRole('link', { name: text })).or(page.getByText(text));
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
await loc.first().click({ timeout: 15000 });
|
}
|
||||||
|
|
||||||
|
function isLikelyLoginText(text: string): boolean {
|
||||||
|
return /(login|accedi|sign\s*in|entra)/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickByText(page: Page, text: string): Promise<boolean> {
|
||||||
|
const patterns = [new RegExp(`^${escapeRegExp(text)}$`, 'i'), new RegExp(escapeRegExp(text), 'i')];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const targets = [
|
||||||
|
page.getByRole('button', { name: pattern }).first(),
|
||||||
|
page.getByRole('link', { name: pattern }).first(),
|
||||||
|
page.getByText(pattern).first(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
if (await target.count()) {
|
||||||
|
try {
|
||||||
|
await target.click({ timeout: 8000 });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// keep trying next candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fallbackLoginNavigation(page: Page, requestedText: string): Promise<boolean> {
|
||||||
|
if (!isLikelyLoginText(requestedText)) return false;
|
||||||
|
|
||||||
|
const current = new URL(page.url());
|
||||||
|
|
||||||
|
const candidateLinks = await page.evaluate(() => {
|
||||||
|
const loginTerms = ['login', 'accedi', 'sign in', 'entra'];
|
||||||
|
const anchors = Array.from(document.querySelectorAll('a[href], a[onclick], button[onclick]')) as Array<HTMLAnchorElement | HTMLButtonElement>;
|
||||||
|
|
||||||
|
return anchors
|
||||||
|
.map((el) => {
|
||||||
|
const text = (el.textContent || '').trim().toLowerCase();
|
||||||
|
const href = (el as HTMLAnchorElement).getAttribute('href') || '';
|
||||||
|
return { text, href };
|
||||||
|
})
|
||||||
|
.filter((x) => x.text && loginTerms.some((t) => x.text.includes(t)))
|
||||||
|
.map((x) => x.href)
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prefer real URLs (not javascript:)
|
||||||
|
const realCandidate = candidateLinks.find((h) => /login|account\/login/i.test(h) && !h.startsWith('javascript:'));
|
||||||
|
if (realCandidate) {
|
||||||
|
const target = new URL(realCandidate, page.url()).toString();
|
||||||
|
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site-specific fallback for Corriere
|
||||||
|
if (/corriere\.it$/i.test(current.hostname) || /\.corriere\.it$/i.test(current.hostname)) {
|
||||||
|
await page.goto('https://www.corriere.it/account/login', {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function typeInBestTarget(page: Page, text: string, selector?: string) {
|
async function typeInBestTarget(page: Page, text: string, selector?: string) {
|
||||||
@@ -133,9 +201,24 @@ async function runSteps(page: Page, steps: Step[]) {
|
|||||||
await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
break;
|
break;
|
||||||
case 'click':
|
case 'click':
|
||||||
if (step.selector) await page.locator(step.selector).first().click({ timeout: 15000 });
|
if (step.selector) {
|
||||||
else if (step.text) await clickByText(page, step.text);
|
await page.locator(step.selector).first().click({ timeout: 15000 });
|
||||||
else throw new Error('click step missing selector/text');
|
} else if (step.text) {
|
||||||
|
const clicked = await clickByText(page, step.text);
|
||||||
|
if (!clicked) {
|
||||||
|
const recovered = await fallbackLoginNavigation(page, step.text);
|
||||||
|
if (!recovered) {
|
||||||
|
throw new Error(`Could not click target text: ${step.text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('click step missing selector/text');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
|
||||||
|
} catch {
|
||||||
|
// no navigation is fine
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'type':
|
case 'type':
|
||||||
await typeInBestTarget(page, step.text, step.selector);
|
await typeInBestTarget(page, step.text, step.selector);
|
||||||
|
|||||||
Reference in New Issue
Block a user