mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
48aac2d784
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
131 lines
7.2 KiB
JavaScript
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; });
|