SPRINT_20260422_001 (Notify compat surface restoration) — closes NOTIFY-COMPAT-003 final criterion. notify-web rebuilt, 7/7 gateway probes green post-rebuild (8th returns contracted 501 notify_overrides_not_supported). Browser replay via direct Node+Playwright driver (MCP bridge rejected the self-signed cert) confirmed Dashboard/Channels/Quiet Hours/Overrides/ Escalation/Throttle tabs render without runtime-unavailable banners. All 3 tasks DONE. SPRINT_20260421_003 (Concelier advisory connector runtime alignment) — all 7 tasks were already DONE before this session; archival is purely administrative. Evidence bundle at docs/qa/notify-compat-20260422/ includes EVIDENCE.md, verify.mjs + verify_with_token.mjs, verify-results.json, and screenshots for each verified tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
5.7 KiB
JavaScript
155 lines
5.7 KiB
JavaScript
// Browser verification for NOTIFY-COMPAT-003.
|
|
// Verifies the notifications setup tabs render without "runtime-unavailable"
|
|
// banners after the notify-web rebuild.
|
|
import { chromium } from 'playwright';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const OUT_DIR = path.resolve('docs/qa/notify-compat-20260422');
|
|
const BASE = 'https://stella-ops.local';
|
|
const USER = 'admin';
|
|
const PASS = 'Admin@Stella2026!';
|
|
|
|
const TABS = [
|
|
{ id: 'dashboard', path: '/setup/notifications' },
|
|
{ id: 'channels', path: '/setup/notifications/channels' },
|
|
{ id: 'quiet-hours', path: '/setup/notifications/config/quiet-hours' },
|
|
{ id: 'overrides', path: '/setup/notifications/config/overrides' },
|
|
{ id: 'escalation', path: '/setup/notifications/config/escalation' },
|
|
{ id: 'throttle', path: '/setup/notifications/config/throttle' },
|
|
];
|
|
|
|
function log(m) { console.log(`[verify] ${m}`); }
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({
|
|
ignoreHTTPSErrors: true,
|
|
viewport: { width: 1400, height: 900 },
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
const consoleErrors = [];
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
});
|
|
page.on('pageerror', err => consoleErrors.push('pageerror: ' + err.message));
|
|
|
|
log(`navigate ${BASE}/`);
|
|
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
// Allow SPA redirect to /welcome to settle
|
|
await page.waitForTimeout(2500);
|
|
log(`landed at ${page.url()}`);
|
|
|
|
// If we hit the /welcome landing page, click "Sign In" to start OIDC flow
|
|
if (/\/welcome/i.test(page.url())) {
|
|
log('at welcome page; clicking sign-in link');
|
|
const signIn = await page.$('a:has-text("Sign In"), a:has-text("Sign in"), button:has-text("Sign In")');
|
|
if (!signIn) {
|
|
await page.screenshot({ path: path.join(OUT_DIR, '00-welcome-no-signin.png'), fullPage: true });
|
|
throw new Error('sign-in link not found on /welcome');
|
|
}
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'networkidle', timeout: 30000 }).catch(() => {}),
|
|
signIn.click(),
|
|
]);
|
|
log(`after sign-in click at ${page.url()}`);
|
|
}
|
|
|
|
// Now we should be on the Authority login page
|
|
if (/authority|connect\/authorize|\/login/i.test(page.url())) {
|
|
log('at authority login; filling credentials');
|
|
await page.waitForSelector('input[name="username"], input[name="Username"], #Username, input[type="text"]', { timeout: 15000 });
|
|
const userSel = 'input[name="username"], input[name="Username"], #Username, input[type="text"]';
|
|
const passSel = 'input[name="password"], input[name="Password"], #Password, input[type="password"]';
|
|
await page.fill(userSel, USER);
|
|
await page.fill(passSel, PASS);
|
|
await page.screenshot({ path: path.join(OUT_DIR, '01-login-filled.png'), fullPage: true });
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'networkidle', timeout: 30000 }).catch(() => {}),
|
|
page.click('button[type="submit"], input[type="submit"]'),
|
|
]);
|
|
log(`after login submit at ${page.url()}`);
|
|
}
|
|
|
|
// Consent if present
|
|
for (let i = 0; i < 2; i++) {
|
|
if (!/consent|authorize/i.test(page.url())) break;
|
|
const btn = await page.$('button[type="submit"]:has-text("Allow"), button:has-text("Allow"), button:has-text("Continue"), button[name="submit.Grant"], button[name="accept"]');
|
|
if (!btn) break;
|
|
log('clicking consent');
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }).catch(() => {}),
|
|
btn.click(),
|
|
]);
|
|
}
|
|
|
|
log(`post-auth at ${page.url()}`);
|
|
await page.screenshot({ path: path.join(OUT_DIR, '02-post-login.png'), fullPage: true });
|
|
|
|
const results = [];
|
|
|
|
for (const tab of TABS) {
|
|
const url = BASE + tab.path;
|
|
log(`--- tab ${tab.id}: ${url}`);
|
|
try {
|
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
} catch (e) {
|
|
log(`navigation error: ${e.message}`);
|
|
}
|
|
await page.waitForTimeout(2000);
|
|
|
|
const shot = path.join(OUT_DIR, `tab-${tab.id}.png`);
|
|
await page.screenshot({ path: shot, fullPage: true });
|
|
|
|
const finalUrl = page.url();
|
|
const bodyText = await page.evaluate(() => document.body.innerText || '');
|
|
const hasUnavailable = /runtime.?unavailable|runtime unavailable/i.test(bodyText);
|
|
const hasEndpointNotFound = /Endpoint not found/i.test(bodyText);
|
|
const has404 = /HTTP 404|404 Endpoint|404\s+Page Not Found/i.test(bodyText);
|
|
// Require that we actually landed on the setup route (not bounced to /welcome)
|
|
const landedOnSetup = /\/setup\/notifications/.test(finalUrl);
|
|
|
|
const pass = landedOnSetup && !hasUnavailable && !hasEndpointNotFound && !has404;
|
|
const result = {
|
|
tab: tab.id,
|
|
url,
|
|
finalUrl,
|
|
screenshot: shot,
|
|
landedOnSetup,
|
|
hasUnavailable,
|
|
hasEndpointNotFound,
|
|
has404,
|
|
pass,
|
|
snippet: bodyText.slice(0, 500).replace(/\s+/g, ' '),
|
|
};
|
|
log(`tab ${tab.id}: pass=${pass} landed=${landedOnSetup} unavailable=${hasUnavailable} endpointNotFound=${hasEndpointNotFound}`);
|
|
results.push(result);
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
const summary = {
|
|
generatedAt: new Date().toISOString(),
|
|
base: BASE,
|
|
user: USER,
|
|
results,
|
|
consoleErrors: consoleErrors.slice(0, 50),
|
|
};
|
|
fs.writeFileSync(path.join(OUT_DIR, 'verify-results.json'), JSON.stringify(summary, null, 2));
|
|
log('results written');
|
|
const failed = results.filter(r => !r.pass);
|
|
if (failed.length) {
|
|
log(`FAILURES: ${failed.map(f => f.tab).join(', ')}`);
|
|
process.exit(2);
|
|
}
|
|
log('ALL TABS PASS');
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|