mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
8342a35332
Sixteen worker-browser proofs still opened the removed History tabs,
clicked the removed Refresh button or depended on UI that has since
moved. Each now passes on pulse-dev:
- Charts are read on Overview (guest, resource, PBS) or inline in disk
and pool details; table-row-touch toggles Manage/Overview as its
nested input instead of the old tab.
- Reads are driven through the 30-second poll with the page clock.
- Locators select series paths and hover markers, since bands and
observation dots also draw paths.
- Rows lifted into Worth checking or kept under the closed Details
disclosure are read from the whole Overview, with Details opened.
- With stored history shown, the legend reads the latest stored time,
so series without a point say "no reading" rather than a live value.
- Stubbed history replies echo the requested series and carry min/max,
as the client has validated since 866da85c26.
- Fixtures that mount the resource drawer supply activeAlerts.
- The guest-stopped-disk write check allows the content-free
workload-history usage milestone that opening details records.
- On emulated phones, headless Chromium reports a fine pointer, so the
proof touches the plot before tapping the inspection slider.
254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
// Real production table rows and PBS drawers; synthetic bounded HTTP data.
|
|
// Do not instrument rows with native listeners: doing so masks the WebKit bug.
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { createHash } = require('node:crypto');
|
|
const { chromium, firefox, webkit } = require('playwright');
|
|
|
|
(async () => {
|
|
const root = '/workspace/frontend-modern';
|
|
process.chdir(root);
|
|
const engine = process.argv.find((arg) => arg.startsWith('--engine='))?.split('=')[1] || 'webkit';
|
|
const theme = process.argv.includes('--dark') ? 'dark' : 'light';
|
|
const width = process.argv.includes('--desktop') ? 1365 : 390;
|
|
assert.ok(['chromium', 'firefox', 'webkit'].includes(engine));
|
|
const out = path.join(root, 'node_modules', `table-row-touch-${engine}-${width}-${theme}`);
|
|
fs.mkdirSync(out, { recursive: true });
|
|
fs.chmodSync(out, 0o755);
|
|
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: fs.mkdtempSync(path.join(out, 'vite-cache-')),
|
|
server: { host: '127.0.0.1', port: 5227, strictPort: true },
|
|
});
|
|
const sourceHashes = Object.fromEntries(
|
|
['src/components/shared/Table.tsx', 'package-lock.json'].map((file) => [
|
|
file,
|
|
createHash('sha256')
|
|
.update(fs.readFileSync(path.join(root, file)))
|
|
.digest('hex'),
|
|
]),
|
|
);
|
|
const playwrightVersion = require('playwright/package.json').version;
|
|
assert.equal(
|
|
playwrightVersion,
|
|
JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
|
|
'node_modules/@playwright/test'
|
|
].version,
|
|
);
|
|
let browser, page;
|
|
const errors = [],
|
|
requests = [],
|
|
observations = [];
|
|
const snapshot = async (name) => {
|
|
const file = path.join(out, name + '.png');
|
|
await page.screenshot({ path: file });
|
|
fs.chmodSync(file, 0o644);
|
|
};
|
|
const activate = (locator) => (width === 390 ? locator.tap() : locator.click());
|
|
const noOverflow = async () =>
|
|
assert.ok(
|
|
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
|
|
'document overflow',
|
|
);
|
|
try {
|
|
await server.listen();
|
|
browser = await { chromium, firefox, webkit }[engine].launch(
|
|
engine === 'chromium'
|
|
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
|
|
: { headless: true },
|
|
);
|
|
page = await browser.newPage({
|
|
viewport: { width, height: width === 390 ? 844 : 900 },
|
|
locale: 'en-GB',
|
|
timezoneId: 'UTC',
|
|
...(width === 390 ? { isMobile: true, hasTouch: true } : {}),
|
|
});
|
|
page.setDefaultTimeout(20_000);
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.route('**/*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
if (url.origin !== 'http://127.0.0.1:5227') return route.abort();
|
|
if (!url.pathname.startsWith('/api/')) return route.continue();
|
|
requests.push({ path: url.pathname, search: url.search, method: route.request().method() });
|
|
assert.equal(route.request().method(), 'GET');
|
|
if (url.pathname === '/api/license/runtime-capabilities')
|
|
return route.fulfill({
|
|
json: {
|
|
capabilities: [],
|
|
limits: [],
|
|
max_history_days: 7,
|
|
hosted_mode: false,
|
|
runtime: { build: 'community', label: 'Pulse Community runtime' },
|
|
blocked_capabilities: [],
|
|
},
|
|
});
|
|
if (url.pathname === '/api/metrics-store/history') {
|
|
const id = url.searchParams.get('resourceId');
|
|
assert.ok(['vm-one', 'vm-two', 'agent-three'].includes(id), `unexpected target ${id}`);
|
|
const end = Date.UTC(2026, 9, 1, 12);
|
|
return route.fulfill({
|
|
json: {
|
|
resourceType: url.searchParams.get('resourceType'),
|
|
resourceId: id,
|
|
range: url.searchParams.get('range'),
|
|
start: end - 86400000,
|
|
end,
|
|
source: 'store',
|
|
metrics: {
|
|
cpu: [
|
|
{ timestamp: end - 600000, value: 20, min: 20, max: 20 },
|
|
{ timestamp: end - 300000, value: 30, min: 30, max: 30 },
|
|
],
|
|
memory: [{ timestamp: end - 300000, value: 40, min: 40, max: 40 }],
|
|
netin: [
|
|
{ timestamp: end - 600000, value: 1024, min: 1024, max: 1024 },
|
|
{ timestamp: end - 300000, value: 2048, min: 2048, max: 2048 },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
return route.fulfill({ json: { data: [], enabled: false } });
|
|
});
|
|
console.log(JSON.stringify({ stage: 'production-pbs', engine, theme, width, sourceHashes }));
|
|
await page.goto('http://127.0.0.1:5227/browser-tests/pbs-identity-boundary.html', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 120_000,
|
|
});
|
|
await page.evaluate(
|
|
(dark) => document.documentElement.classList.toggle('dark', dark),
|
|
theme === 'dark',
|
|
);
|
|
await activate(page.getByRole('button', { name: 'Corroborate links', exact: true }));
|
|
for (const suffix of ['one', 'two', 'three']) {
|
|
const cell = page.locator(`td[title="backup-connection-${suffix} · tank"]`);
|
|
const disclosure = cell.getByRole('button');
|
|
assert.equal(await disclosure.getAttribute('aria-expanded'), 'false');
|
|
// No focus, force, dispatchEvent, synthetic click or added event listeners.
|
|
await activate(cell);
|
|
await page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`).waitFor();
|
|
assert.equal(await disclosure.getAttribute('aria-expanded'), 'true');
|
|
const detail = page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`);
|
|
observations.push({ suffix, firstActivationOpened: true });
|
|
await noOverflow();
|
|
if (suffix === 'one') await snapshot('pbs-first-activation');
|
|
// History charts sit on Overview; switching tabs inside the drawer is the
|
|
// nested input that must not retoggle the row.
|
|
await activate(detail.getByRole('tab', { name: 'Manage', exact: true }));
|
|
await activate(detail.getByRole('tab', { name: 'Overview', exact: true }));
|
|
await detail
|
|
.getByTestId('guest-history-plot')
|
|
.first()
|
|
.locator('path[data-history-series], [data-history-observation]')
|
|
.first()
|
|
.waitFor({ state: 'attached' });
|
|
assert.equal(
|
|
await disclosure.getAttribute('aria-expanded'),
|
|
'true',
|
|
'tab must not retoggle row',
|
|
);
|
|
if (suffix === 'three') await snapshot('pbs-third-history');
|
|
// Close by the same ordinary cell; nested History input cannot consume it.
|
|
await activate(cell);
|
|
assert.equal(await disclosure.getAttribute('aria-expanded'), 'false');
|
|
assert.equal(await detail.count(), 0);
|
|
}
|
|
const keyboardCell = page.locator('td[title="backup-connection-one · tank"]');
|
|
const keyboardDisclosure = keyboardCell.getByRole('button');
|
|
await keyboardDisclosure.focus();
|
|
await page.keyboard.press('Enter');
|
|
assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'true');
|
|
await page.keyboard.press('Space');
|
|
assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'false');
|
|
assert.ok(requests.some((request) => request.search.includes('resourceId=vm-one')));
|
|
assert.ok(requests.some((request) => request.search.includes('resourceId=vm-two')));
|
|
assert.ok(requests.some((request) => request.search.includes('resourceId=agent-three')));
|
|
|
|
console.log(JSON.stringify({ stage: 'shared-controls', engine, theme, width }));
|
|
await page.goto('http://127.0.0.1:5227/browser-tests/table-row-touch.html', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 120_000,
|
|
});
|
|
await page.evaluate(
|
|
(dark) => document.documentElement.classList.toggle('dark', dark),
|
|
theme === 'dark',
|
|
);
|
|
const state = async () =>
|
|
JSON.parse(await page.getByRole('status', { name: 'Action state' }).innerText());
|
|
const name = page.getByText('Shared resource name', { exact: true });
|
|
await name.waitFor();
|
|
await activate(name);
|
|
assert.equal((await state()).rowActions, 1);
|
|
assert.equal((await state()).expanded, true);
|
|
await activate(page.getByRole('button', { name: 'Child action', exact: true }));
|
|
await activate(page.getByRole('link', { name: 'Native link' }));
|
|
assert.equal(new URL(page.url()).hash, '#native-link');
|
|
await activate(page.getByRole('checkbox', { name: 'Child checkbox' }));
|
|
assert.equal(await page.getByRole('checkbox').isChecked(), true);
|
|
assert.equal((await state()).rowActions, 1, 'nested controls must not run row action');
|
|
assert.equal((await state()).childActions, 2);
|
|
const toggle = page.getByTestId('control-row').locator('[data-row-action="true"]');
|
|
await activate(toggle);
|
|
assert.equal((await state()).rowActions, 2, 'nested disclosure must toggle once');
|
|
await toggle.focus();
|
|
await page.keyboard.press('Enter');
|
|
assert.equal((await state()).rowActions, 3);
|
|
await page.keyboard.press('Space');
|
|
assert.equal((await state()).rowActions, 4);
|
|
await activate(page.getByRole('button', { name: 'Toggle row action' }));
|
|
await activate(name);
|
|
assert.equal((await state()).rowActions, 4);
|
|
await activate(page.getByRole('button', { name: 'Toggle row action' }));
|
|
await activate(name);
|
|
assert.equal((await state()).rowActions, 5);
|
|
await activate(page.getByText('Static row', { exact: true }));
|
|
assert.equal((await state()).rowActions, 5);
|
|
await activate(page.getByText('Grid resource', { exact: true }));
|
|
assert.equal((await state()).gridActions, 1);
|
|
await activate(page.getByRole('button', { name: 'Grid child action', exact: true }));
|
|
assert.equal((await state()).gridActions, 1);
|
|
assert.equal((await state()).childActions, 3);
|
|
await noOverflow();
|
|
await snapshot('shared-controls');
|
|
assert.deepEqual(errors, []);
|
|
const result = {
|
|
engine,
|
|
browserVersion: browser.version(),
|
|
playwrightVersion,
|
|
theme,
|
|
width,
|
|
sourceHashes,
|
|
observations,
|
|
requests,
|
|
controlState: await state(),
|
|
errors,
|
|
result: 'passed',
|
|
};
|
|
fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify(result, null, 2) + '\n', {
|
|
mode: 0o644,
|
|
});
|
|
console.log(JSON.stringify(result));
|
|
} catch (error) {
|
|
if (page) await snapshot('failure').catch(() => {});
|
|
fs.writeFileSync(
|
|
path.join(out, 'failure.json'),
|
|
JSON.stringify(
|
|
{ error: String(error), errors, requests, observations, sourceHashes },
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
{ mode: 0o644 },
|
|
);
|
|
throw error;
|
|
} finally {
|
|
await browser?.close();
|
|
await server.close();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|