From 47314f50c9a93cd6f83a419a3b3a9d636aba6c8b Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 11 Feb 2026 22:12:42 +0000 Subject: [PATCH] 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 --- skills/web-automation/scripts/flow.ts | 95 +++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/skills/web-automation/scripts/flow.ts b/skills/web-automation/scripts/flow.ts index 90dc3ac..0f56758 100644 --- a/skills/web-automation/scripts/flow.ts +++ b/skills/web-automation/scripts/flow.ts @@ -102,9 +102,77 @@ function parseInstruction(instruction: string): Step[] { return steps; } -async function clickByText(page: Page, text: string) { - const loc = page.getByRole('button', { name: text }).or(page.getByRole('link', { name: text })).or(page.getByText(text)); - await loc.first().click({ timeout: 15000 }); +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function isLikelyLoginText(text: string): boolean { + return /(login|accedi|sign\s*in|entra)/i.test(text); +} + +async function clickByText(page: Page, text: string): Promise { + 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 { + 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; + + 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) { @@ -133,9 +201,24 @@ async function runSteps(page: Page, steps: Step[]) { await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout: 60000 }); break; case 'click': - if (step.selector) await page.locator(step.selector).first().click({ timeout: 15000 }); - else if (step.text) await clickByText(page, step.text); - else throw new Error('click step missing selector/text'); + if (step.selector) { + await page.locator(step.selector).first().click({ timeout: 15000 }); + } 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; case 'type': await typeInBestTarget(page, step.text, step.selector);