InsuranceBot / audit /e2e /insurancebot_e2e.js
rohitsar567's picture
feat(audit): Tier 4 functionality (API smoke + Playwright E2E) + selftest fixtures
cf9ef2d
Raw
History Blame Contribute Delete
5.92 kB
/**
* 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 <img src*="insurer-logos"> 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 <aside> that REPLACES the
// chat view, so the composer textarea stays in the DOM but is hidden
// while the panel is open. Toggle the Policy-Library trigger again to
// close the panel (its onClick is togglePanel(...)), then wait for the
// composer to become visible again.
const ml = page
.locator(
'button:has-text("Policy Library"), button:has-text("पॉलिसी लाइब्रेरी")'
)
.first();
if (await ml.isVisible().catch(() => false)) {
await ml.click({ timeout: 10000 }).catch(() => {});
}
const ta = page.locator('textarea[aria-label="Message"]').first();
await ta.waitFor({ state: 'visible', timeout: 20000 });
await ta.fill('audit e2e presence check');
const val = await ta.inputValue();
const send = page
.locator('button:has-text("Send")')
.first();
// After typing, Send must no longer be disabled (it gates on input).
const sendEnabled = await send.isEnabled().catch(() => false);
R.composerWorks = val === 'audit e2e presence check' && sendEnabled;
} catch (e) {
errors.push('composerWorks: ' + (e && e.message));
}
// ---- journey: noConsoleError ---------------------------------------
// No pageerror / console.error captured during the whole session.
R.noConsoleError = errors.every(
(m) =>
!m.startsWith('pageerror:') && !m.startsWith('console.error:')
);
} catch (fatal) {
// Hard failure (browser launch, etc.) — journeys stay false; still emit
// RJSON so the Python side classifies (FAIL/WARN), never a silent hang.
errors.push('fatal: ' + (fatal && fatal.message));
} finally {
try {
if (browser) await browser.close();
} catch (_) {
/* ignore */
}
}
if (errors.length) {
console.log('E2E_DIAG ' + JSON.stringify(errors.slice(0, 20)));
}
console.log('RJSON ' + JSON.stringify(R));
// Exit 0 regardless: the boolean payload (not the exit code) is the signal.
process.exit(0);
})();