/**
* Tier-4 E2E — deterministic UI-presence journeys for the Insurance Advisor
* frontend. Run via the playwright-skill runner:
* node /Users/rohitsar/.claude/skills/playwright-skill/run.js \
* audit/e2e/insurancebot_e2e.js
*
* The runner injects Playwright (resolved from the skill's node_modules) and
* passes this file through verbatim because it already has `require(` + an
* async IIFE. Every journey resolves to a boolean; everything is wrapped so a
* single `RJSON {...}` line is ALWAYS printed to stdout — even on a hard
* failure — so the Python check can parse it (no RJSON => WARN/infra, never a
* silent FAIL). These are presence/no-error checks only: NO multi-turn LLM
* chat (that is flaky/slow and not a deterministic UI signal).
*/
const { chromium } = require('playwright');
const TARGET = process.env.TARGET_URL || 'http://localhost:3000';
(async () => {
const R = {
loads: false,
marketplaceRenders: false,
logosOk: false,
composerWorks: false,
noConsoleError: false,
};
let browser;
// Console / page errors collected across the whole session.
const errors = [];
try {
browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
});
const page = await context.newPage();
page.on('pageerror', (e) => errors.push('pageerror: ' + (e && e.message)));
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push('console.error: ' + msg.text());
});
// ---- journey: loads -------------------------------------------------
try {
await page.goto(TARGET, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('h1', { state: 'visible', timeout: 30000 });
R.loads = (await page.locator('h1').count()) > 0;
} catch (e) {
errors.push('loads: ' + (e && e.message));
}
// ---- journey: marketplaceRenders -----------------------------------
// Open the Policy-Library / marketplace panel, assert >=1 policy card is
// visible, and assert no horizontal overflow at 1440px.
try {
const trigger = page
.locator(
'button:has-text("Policy Library"), button:has-text("पॉलिसी लाइब्रेरी")'
)
.first();
await trigger.click({ timeout: 20000 });
await page.waitForSelector('.policy-card', {
state: 'visible',
timeout: 30000,
});
const cards = await page.locator('.policy-card:visible').count();
const overflow = await page.evaluate(
() =>
document.documentElement.scrollWidth >
window.innerWidth + 1
);
R.marketplaceRenders = cards >= 1 && !overflow;
} catch (e) {
errors.push('marketplaceRenders: ' + (e && e.message));
}
// ---- journey: logosOk ----------------------------------------------
// At least one insurer logo
with naturalWidth>0.
try {
await page
.waitForSelector('img[src*="insurer-logos"]', { timeout: 20000 })
.catch(() => {});
const logoLoaded = await page.evaluate(() => {
const imgs = Array.from(
document.querySelectorAll('img[src*="insurer-logos"]')
);
if (imgs.length === 0) return false;
return imgs.some((im) => im.naturalWidth && im.naturalWidth > 0);
});
R.logosOk = logoLoaded;
} catch (e) {
errors.push('logosOk: ' + (e && e.message));
}
// ---- journey: composerWorks ----------------------------------------
// The chat composer textarea is fillable and the Send button is clickable
// once non-empty. (We do NOT actually submit a turn — no LLM dependency.)
try {
// The marketplace panel is a single-active