Files
Codeman/test/header-host-sensors.browser.test.ts
T
Codeman maintainer fb83a5c8df fix: keep the host readings' labels under header density, hide ringless ones at Compact tiny (release review)
#689's density steps hide every `.header-system-stats .stat-label` once the
toolbar is narrowed, and #692's readings (GPU, VRAM, GPU MHz, CPU MHz) have
nothing else naming them: at tight, dense and tiny the header showed "40%"
beside CPU's "12%", "3.2G" beside MEM's "7.9G", and two bare "1800 MHz" /
"4200 MHz" with no way to tell the GPU clock from the CPU one. At Compact +
tiny, where values are hidden too, the two clocks (no ring) were left as
0-px items that still took an 8 px flex gap each. Neither PR shows it alone.

- The four host items carry `stat-item--host`, excluded from the density
  label hide, so they keep their labels at every step.
- `stat-item--ringless` (static on the two clocks, toggled by
  `_updateHostSensors` on VRAM while its total, and so its ring, is unknown)
  is hidden at Compact + tiny instead of staying an empty item.

Tests: the browser test drives every density in all three styles against
the real markup and styles.css (it fails on the previous files), and the
unit test pins the VRAM ringless toggle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 16:45:05 +02:00

117 lines
5.6 KiB
TypeScript

/**
* Real Chromium, real header markup and styles.css: the opt-in host readings stay out of the header until they
* are switched on and reported, under every header-stats style, and a reported one is drawn with its value.
* As the header divider narrows the toolbar (data-header-density), they keep the label that says which reading
* is which, and the Compact style's rings-only step hides a reading with no ring instead of leaving it empty.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
const dir = resolve(import.meta.dirname, '../src/web/public');
const styles = readFileSync(resolve(dir, 'styles.css'), 'utf8');
const header = /<header class="header">[\s\S]*?<\/header>/.exec(readFileSync(resolve(dir, 'index.html'), 'utf8'))![0];
const ITEMS = ['statGpuItem', 'statVramItem', 'statCpuMhzItem', 'statGpuMhzItem'];
/** Each host reading: its value element, a value that looks like CPU's or MEM's, and the label naming it. */
const READINGS: Record<string, { value: string; text: string; label: string }> = {
statGpuItem: { value: 'statGpu', text: '40%', label: 'GPU' },
statVramItem: { value: 'statVram', text: '3.2G', label: 'VRAM' },
statGpuMhzItem: { value: 'statGpuMhz', text: '1800 MHz', label: 'GPU' },
statCpuMhzItem: { value: 'statCpuMhz', text: '4200 MHz', label: 'CPU' },
};
const RINGLESS = ['statGpuMhzItem', 'statCpuMhzItem'];
describe('opt-in host readings in the header (real Chromium)', () => {
let browser: Browser;
let p: Page;
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
for (const style of ['compact', 'tiles', 'classic']) {
it(`draws none of them until reported, and the reported one with its value, in the ${style} style`, async () => {
p = await browser.newPage({ viewport: { width: 1500, height: 400 } });
await p.setContent(
`<!doctype html><html data-header-stats="${style}" data-tab-orientation="horizontal" data-session-list="header"><head><style>${styles}</style></head><body>${header}</body></html>`
);
const shown = () =>
p.evaluate(
(ids) => ids.filter((id) => getComputedStyle(document.getElementById(id)!).display !== 'none'),
ITEMS
);
expect(await shown()).toEqual([]);
await p.evaluate(() => {
document.getElementById('statGpuItem')!.hidden = false;
document.getElementById('statGpu')!.textContent = '42%';
});
expect(await shown()).toEqual(['statGpuItem']);
const box = await p.locator('#statGpuItem').boundingBox();
expect(box!.width).toBeGreaterThan(20);
expect(await p.locator('#statGpuItem').innerText()).toContain('42%');
await p.close();
});
it(`keeps every host reading's label as the toolbar narrows, in the ${style} style`, async () => {
p = await browser.newPage({ viewport: { width: 1600, height: 400 } });
await p.setContent(
`<!doctype html><html data-header-stats="${style}" data-tab-orientation="horizontal" data-session-list="header"><head><style>${styles}</style></head><body>${header}</body></html>`
);
await p.evaluate((readings) => {
document.getElementById('statCpu')!.textContent = '12%';
document.getElementById('statMem')!.textContent = '7.9G';
for (const [id, r] of Object.entries(readings)) {
document.getElementById(id)!.hidden = false;
document.getElementById(r.value)!.textContent = r.text;
}
}, READINGS);
const read = () =>
p.evaluate((ids) => {
const shown = (el: Element) => getComputedStyle(el).display !== 'none';
return Object.fromEntries(
ids.map((id) => {
const item = document.getElementById(id)!;
const label = item.querySelector('.stat-label')!;
return [
id,
{
shown: shown(item),
width: item.getBoundingClientRect().width,
label: shown(label) && label.getBoundingClientRect().width > 0 ? label.textContent : null,
},
];
})
);
}, ITEMS);
const cpuLabel = () =>
p.evaluate(() => getComputedStyle(document.querySelector('.header-system-stats .stat-label')!).display);
for (const density of ['tight', 'dense', 'tiny']) {
await p.evaluate((d) => (document.documentElement.dataset.headerDensity = d), density);
const items = await read();
// the density step still sheds CPU's and MEM's labels
expect(await cpuLabel(), density).toBe('none');
for (const id of ITEMS) {
if (style === 'compact' && density === 'tiny' && RINGLESS.includes(id)) {
// rings only: a clock has no ring, so it goes rather than stay an empty item taking a gap
expect(items[id].shown, `${density} ${id}`).toBe(false);
continue;
}
expect(items[id].shown, `${density} ${id}`).toBe(true);
expect(items[id].width, `${density} ${id}`).toBeGreaterThan(0);
expect(items[id].label, `${density} ${id}`).toBe(READINGS[id].label);
}
}
// VRAM whose total is unknown has no ring either (panels-ui.js marks it ringless)
await p.evaluate(() => {
document.getElementById('statVramItem')!.classList.add('stat-item--ringless');
document.getElementById('statVramRing')!.style.display = 'none';
});
expect((await read()).statVramItem.shown).toBe(style !== 'compact');
await p.close();
});
}
});