Files
pulse/scripts/check-drawer-tab-retention.cjs
T
rcourtman 5168ae6460 Chart history on Overview in Docker host and resource drawers
PR 2913 moved the Proxmox guest and node drawers' charts onto Overview
and removed their History tab. The Docker host drawer and the shared
resource-detail drawer (agent machines, containers, PBS, TrueNAS,
Kubernetes and other platform rows) still kept the charts a tab away,
so the same kind of drawer behaved two ways depending on the page.

Both now render the history charts on Overview under the attention
list, opening on 24 hours with the range picker beside them, and drop
the History tab. Charts stop polling while another tab shows, and an
active alert on a charted metric draws its trigger the same way as in
the Proxmox drawers. A resource whose metrics target drops for one
snapshot simply loses its charts until the target returns; the
selected tab is kept.

Storage page disk and pool details keep their own History tab; they
are separate components and their Overview has no charts to repeat.

E2E specs 27 and 63 no longer click History; the drawer read starts
when it opens. The manual tab-retention proof now selects Manage. Its
fixture page fails before the drawer renders (no WebSocket provider)
on main as well, so it was not run.
2026-10-13 23:30:46 +01:00

195 lines
6.6 KiB
JavaScript

// Offline real-browser proof for the #1723 drawer tab-retention repair.
//
// Opens the production Proxmox Backups server drawer, whose history charts sit
// on Overview, selects Manage, then drives a snapshot that transiently removes
// the merged host metrics target (the field that gates those charts). The
// drawer must keep the user's Manage selection without remounting; restoring
// the target must bring the charts back on Overview. Synthetic API responses
// prove UI behaviour, not backend persistence or installed release behaviour.
// Run: pulse-worker-browser scripts/check-drawer-tab-retention.cjs
const path = require('node:path');
const assert = require('node:assert/strict');
const { chromium } = require('playwright');
const ROOT = path.resolve(process.cwd(), 'frontend-modern');
const launchOptions = {
headless: true,
channel: 'chromium',
args: ['--no-sandbox'],
};
(async () => {
process.chdir(ROOT);
const { createServer } = await import(
path.join(ROOT, 'node_modules', 'vite', 'dist', 'node', 'index.js')
);
const server = await createServer({
root: ROOT,
configFile: path.join(ROOT, 'vite.config.ts'),
server: { host: '127.0.0.1', port: 5199, strictPort: true },
});
let browser;
const results = [];
const pageErrors = [];
const failures = [];
try {
await server.listen();
browser = await chromium.launch(launchOptions);
for (const viewport of [
{ width: 1280, height: 900 },
{ width: 390, height: 844 },
]) {
const caseName = `${viewport.width}`;
const page = await browser.newPage({ viewport });
page.on('pageerror', (error) =>
pageErrors.push(`${viewport.width}px: ${error.message}`),
);
await page.route('**/api/license/runtime-capabilities', (route) =>
route.fulfill({
json: {
capabilities: [],
limits: [],
hosted_mode: false,
max_history_days: 7,
runtime: { build: 'community', label: 'Pulse Community runtime' },
blocked_capabilities: [],
},
}),
);
await page.route('**/api/settings/ai', (route) =>
route.fulfill({ json: { enabled: false } }),
);
await page.route('**/api/metrics-store/history?**', async (route) => {
const query = new URL(route.request().url()).searchParams;
await route.fulfill({
json: {
resourceType: query.get('resourceType'),
resourceId: query.get('resourceId'),
range: query.get('range'),
start: Date.now() - 3600000,
end: Date.now(),
source: 'store',
metrics: Object.fromEntries(
[
'cpu',
'memory',
'disk',
'netin',
'netout',
'diskread',
'diskwrite',
].map((metric) => [
metric,
[30, 20, 10].map((minutes, i) => ({
timestamp: Date.now() - minutes * 60000,
value: 10 + i * 5,
min: 10 + i * 5,
max: 10 + i * 5,
})),
]),
),
},
});
});
try {
await page.goto(
'http://127.0.0.1:5199/browser-tests/pbs-host-history-correlation.html?topology=guest&order=stable',
{ timeout: 60000 },
);
const serverRow = page
.locator('tr')
.filter({
has: page.getByRole('button', {
name: 'Expand details for proxback',
exact: true,
}),
})
.first();
await serverRow.waitFor({ timeout: 20000 });
await serverRow.getByRole('button').focus();
await page.keyboard.press('Enter');
await page
.locator('[data-testid="guest-history-plot"] path')
.first()
.waitFor({ timeout: 20000 });
const detail = page.locator(
'[data-inline-platform-resource-detail-for="pbs-1"]',
);
await detail.evaluate((node) => {
node.dataset.proofIdentity = 'retained';
});
const pathsBefore = await detail
.locator('[data-testid="guest-history-plot"] path')
.count();
assert.ok(pathsBefore > 0, 'history plot did not render before the drop');
await page.getByRole('tab', { name: 'Manage', exact: true }).click();
// Snapshot arrives without the merged host metrics target.
await page.getByRole('button', { name: 'Drop metrics target' }).click();
await detail
.locator('[data-testid="resource-metrics-history"]')
.waitFor({ state: 'detached', timeout: 20000 });
assert.equal(
await page
.getByRole('tab', { name: 'Manage', exact: true })
.getAttribute('aria-selected'),
'true',
'tab selection was discarded across the transient target loss',
);
assert.equal(
await detail.getAttribute('data-proof-identity'),
'retained',
'drawer remounted on the transient target loss',
);
// Next snapshot restores it; Overview shows the same charts again.
await page.getByRole('button', { name: 'Restore metrics target' }).click();
await page.getByRole('tab', { name: 'Overview', exact: true }).click();
await page
.locator('[data-testid="guest-history-plot"] path')
.first()
.waitFor({ timeout: 20000 });
assert.equal(
await detail
.locator('[data-testid="guest-history-plot"] path')
.count(),
pathsBefore,
);
results.push({ viewport, paths: pathsBefore, result: 'passed' });
} catch (error) {
failures.push(`${caseName}: ${error.message}`);
results.push({ viewport, result: 'failed', error: error.message });
} finally {
await page.screenshot({
path: path.join(ROOT, 'browser-tests', `drawer-tab-retention-${caseName}.png`),
});
await page.close();
}
}
if (pageErrors.length > 0) {
throw new Error(`page errors: ${pageErrors.join(' | ')}`);
}
console.log(
JSON.stringify(
{
result: failures.length ? 'failed' : 'passed',
browser: browser.version(),
results,
failures,
},
null,
2,
),
);
assert.equal(failures.length, 0, failures.join(' | '));
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch((error) => {
console.error('FAILED:', error && error.stack ? error.stack : error);
process.exitCode = 1;
});