Files
pulse/frontend-modern/browser-tests/api-auth-help.cjs
T
pulse-triage[bot] 48aac2d784 Bind API help proof to the existing read-only security fixture
The production Docs fixture reads GET /api/security/status during the consuming journey. Record and admit that single endpoint/method class as synthetic fixture traffic, while aborting any other API or off-origin request. The desktop keyboard and touch-enabled narrow journey now completes exact-guide, linked help, fragment focus and reload checks.

Contract-Neutral: Browser verification driver only; the documented API helper, frontend runtime and subsystem contracts are unchanged.
Change-source: pulse-maintainer
2026-10-13 08:46:36 +01:00

131 lines
7.2 KiB
JavaScript

// Consume the shipped API guide through the production Docs page/router.
// Shell transport is proved separately with real curl and synthetic fixtures.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const { chromium } = require('playwright');
(async () => {
const root = path.resolve('frontend-modern');
const artifacts = path.join(root, 'node_modules', 'api-auth-browser');
fs.mkdirSync(artifacts, { recursive: true });
const guide = fs.readFileSync(path.join(root, 'public/docs/API.md'), 'utf8');
const helper = [...guide.matchAll(/```bash\n([\s\S]*?)```/g)]
.map(match => match[1].trim()).find(block => block.startsWith('pulse_api() ('));
assert.ok(helper);
const port = 5259;
process.chdir(root);
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
const server = await createServer({
root, configFile: path.join(root, 'vite.config.ts'),
cacheDir: path.join(root, 'node_modules', '.vite-api-auth'),
server: { host: '127.0.0.1', port, strictPort: true },
optimizeDeps: { noDiscovery: true, include: [] },
});
const results = [];
let browser;
try {
await server.listen();
for (const [width, tone] of [[1280, 'light'], [390, 'dark']]) {
const engine = 'chromium';
const narrow = width < 500;
browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] });
const page = await browser.newPage({ viewport: { width, height: 1000 },
...(narrow ? { isMobile: true, hasTouch: true } : {}) });
const errors = [];
const apiRequests = [];
const offOriginRequests = [];
page.on('pageerror', error => errors.push(error.message));
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
await page.route('**/*', route => {
const url = new URL(route.request().url());
if (url.origin !== `http://127.0.0.1:${port}`) {
offOriginRequests.push(url.href);
return route.abort();
}
if (url.pathname.startsWith('/api/')) {
const method = route.request().method();
apiRequests.push({ method, path: url.pathname });
// The existing Docs fixture's security context can read this one
// status. No product API server or administrative action is used.
if (method !== 'GET' || url.pathname !== '/api/security/status') return route.abort();
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
}
return route.continue();
});
await page.goto(`http://127.0.0.1:${port}/browser-tests/docs-fragment-navigation.html`,
{ waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.getByRole('link', { name: '← All documentation' }).click();
const entry = page.locator('a[data-doc-link][href="/docs/API"]').first();
if (narrow) await entry.tap();
else {
await entry.focus();
await page.keyboard.press('Enter');
}
await page.getByRole('heading', { name: 'API Token (Recommended)', exact: true }).waitFor();
if (tone === 'dark') await page.evaluate(() => document.documentElement.classList.add('dark'));
const code = page.locator('pre').filter({ hasText: 'pulse_api() (' });
assert.equal((await code.locator('code').textContent()).trim(), helper);
assert.ok(helper.includes('--globoff'));
// Place the changed command line in the viewport, and use the code
// block's own horizontal scroll on phones rather than overflowing the page.
await code.evaluate(pre => {
const style = getComputedStyle(pre.querySelector('code'));
const line = pre.textContent.split('\n').findIndex(text => text.includes('--globoff'));
window.scrollTo(0, pre.getBoundingClientRect().top + window.scrollY
+ parseFloat(getComputedStyle(pre).paddingTop) + line * parseFloat(style.lineHeight) - 200);
if (innerWidth < 500) pre.scrollLeft = pre.scrollWidth;
});
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
const captures = [`${engine}-${width}-helper.png`, `${engine}-${width}-boundary.png`];
await page.screenshot({ path: path.join(artifacts, captures[0]) });
const warning = page.locator('p').filter({ hasText: "shell quotes alone do not disable curl's URL globbing" });
await warning.scrollIntoViewIfNeeded();
const text = await warning.innerText();
for (const phrase of ['{review,ops}', '[1-3]', 'POST, PUT or DELETE',
'not as a batch', 'URL-encode', 'does not encode them for you']) {
assert.ok(text.includes(phrase), phrase);
}
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await page.screenshot({ path: path.join(artifacts, captures[1]) });
// Return through the guide's own linked administration help, then use
// the real renderer's fragment focus/reload handling on the API guide.
const rbac = page.locator('a[data-doc-link][href^="/docs/RBAC#"]').first();
await rbac.scrollIntoViewIfNeeded();
if (narrow) await rbac.tap();
else { await rbac.focus(); await page.keyboard.press('Enter'); }
await page.waitForURL(`**/docs/RBAC#managing-roles`);
await page.waitForFunction(() => document.activeElement?.id === 'managing-roles');
await page.goBack({ waitUntil: 'domcontentloaded' });
await page.evaluate(() => { window.location.hash = '#api-token-recommended'; });
await page.waitForFunction(() => document.activeElement?.id === 'api-token-recommended');
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.activeElement?.id === 'api-token-recommended');
assert.equal((await page.locator('pre').filter({ hasText: 'pulse_api() (' })
.locator('code').textContent()).trim(), helper);
assert.deepEqual(errors, []);
assert.ok(apiRequests.every(request => request.method === 'GET'
&& request.path === '/api/security/status'));
assert.deepEqual(offOriginRequests, []);
results.push({ engine, browserVersion: browser.version(), width, height: 1000, tone,
entry: narrow ? 'touch' : 'keyboard', exactRenderedShell: true,
literalURLAndEncodingBoundary: true, linkedAdminHelp: true,
fragmentFocusAndReload: true, noPageOverflow: true, captures, errors,
apiRequests, offOriginRequests });
await browser.close();
browser = undefined;
}
const result = { result: 'passed', verifiedAt: new Date().toISOString(),
playwrightVersion: require('playwright/package.json').version,
scope: 'Production Docs/router and exact copied guide; not installed API or customer acceptance',
docs: { 'frontend-modern/public/docs/API.md': createHash('sha256').update(guide).digest('hex') },
results };
fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify(result));
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });