mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
Make shared History window controls accessible on phones
Name each chart window and expose its selected period without cross-chart changes. Keep native keyboard and touch controls out of enclosing form submission, and wrap all existing choices inside narrow charts. Exact-parent browser controls reproduce missing semantics, fixture form submission and phone overflow; current-source Chromium and WebKit verify scope, selection, paint and layout. Preserve History ownership, samples, periods and the unchanged dependency graph; full committed-source validation follows. Change-source: pulse-maintainer
This commit is contained in:
@@ -8439,6 +8439,23 @@ for missing data. Tooltips reserve room for a wrapped local timestamp and readin
|
||||
fixture cover navigation, bounds, focus exit, refresh and replacement. Synthetic
|
||||
browser proof does not establish native collector or assistive-device acceptance.
|
||||
|
||||
### Shared History window control access
|
||||
|
||||
Each visible shared chart window selector is a named group for its metric (or
|
||||
"History window" when no label is supplied). Exactly the current period carries
|
||||
`aria-pressed=true`; switching one chart must not change another chart's selected
|
||||
state. Period buttons retain native keyboard/touch behaviour and explicit
|
||||
`type=button`, so changing the window cannot submit an enclosing form. A hidden
|
||||
selector exposes neither a group nor controls.
|
||||
|
||||
The header, summary and existing period choices wrap within their chart on
|
||||
narrow screens instead of forcing horizontal overflow. This changes neither
|
||||
available periods, subscriptions, History ownership nor sample interpretation.
|
||||
Mounted `HistoryChart.test.tsx` and the production-component
|
||||
`browser-tests/history-window-controls.cjs` cover selection, scope, native input,
|
||||
form non-submission and phone layout. Synthetic observations are not native
|
||||
PBS/NAS, physical device or assistive-device acceptance.
|
||||
|
||||
### Shared History request-state honesty
|
||||
|
||||
An empty successful response states only that the selected time range has no
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const crypto = require('node:crypto');
|
||||
const assert = require('node:assert/strict');
|
||||
const { chromium, webkit } = require('playwright');
|
||||
const root = '/workspace/frontend-modern';
|
||||
const parent = Boolean(global.__historyWindowParentControl);
|
||||
const output = '/workspace/tmp/history-window-controls/' + (parent ? 'parent' : 'final');
|
||||
const preview = output + '/preview';
|
||||
const origin = 'http://127.0.0.1:5332';
|
||||
const runtime = 'frontend-modern/src/components/shared/HistoryChartHeader.tsx';
|
||||
const parentFile = '/workspace/tmp/history-window-controls/parent.tsx';
|
||||
const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const load = name => import(require.resolve(name, { paths: [root] }));
|
||||
(async () => {
|
||||
fs.mkdirSync(output, {recursive:true});
|
||||
const result = { result:'incomplete', control:parent ? 'exact parent expected defects' : 'final',
|
||||
playwright:require('playwright/package.json').version,
|
||||
runtime_hash:hash(parent ? parentFile : '/workspace/' + runtime),
|
||||
script_hash:hash(root + '/browser-tests/history-window-controls.cjs'),
|
||||
cases:[], captures:[], cleanup:{browser:false,server:false},
|
||||
limits:'Production header and CSS with synthetic state. No native PBS/NAS, collector, physical device, assistive-device, installed or release acceptance.' };
|
||||
let server, browser;
|
||||
try {
|
||||
assert.equal(result.playwright,JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages['node_modules/@playwright/test'].version);
|
||||
const {build} = await load('vite');
|
||||
const solidModule = await load('vite-plugin-solid');
|
||||
const solid = solidModule.default.default || solidModule.default;
|
||||
const {default:tailwind} = await load('@tailwindcss/vite');
|
||||
const plugins = [];
|
||||
if(parent) plugins.push({name:'exact-parent-header-control',enforce:'pre',load(id) {
|
||||
if(id.split('?')[0] === '/workspace/' + runtime) return fs.readFileSync(parentFile,'utf8');
|
||||
}});
|
||||
plugins.push(solid(),tailwind());
|
||||
await build({root,configFile:false,logLevel:'warn',plugins,resolve:{alias:{'@':root + '/src'}},
|
||||
build:{target:'esnext',outDir:preview,emptyOutDir:true,minify:false,rollupOptions:{input:root + '/browser-tests/history-window-controls.html'}}});
|
||||
server = http.createServer((req,res) => {
|
||||
const url=new URL(req.url,origin);
|
||||
let file=path.resolve(preview,'.' + decodeURIComponent(url.pathname));
|
||||
if(file!==preview && !file.startsWith(preview + '/')) {res.writeHead(403);return res.end();}
|
||||
if(!fs.existsSync(file)||fs.statSync(file).isDirectory()) file=preview+'/browser-tests/history-window-controls.html';
|
||||
res.setHeader('Content-Type',({'.html':'text/html','.js':'text/javascript','.css':'text/css'})[path.extname(file)]||'application/octet-stream');
|
||||
res.end(fs.readFileSync(file));
|
||||
});
|
||||
await new Promise(resolve=>server.listen(5332,'127.0.0.1',resolve));
|
||||
for(const [name,engine,width,dark] of [['chromium-desktop',chromium,1280,false],['webkit-phone',webkit,390,true]]) {
|
||||
browser=await engine.launch(engine===chromium ? {headless:true,channel:'chromium',args:['--no-sandbox']} : {headless:true});
|
||||
const context=await browser.newContext({viewport:{width,height:760},isMobile:width<=768,hasTouch:width<=768,locale:'en-GB'});
|
||||
const page=await context.newPage(); page.setDefaultTimeout(10000);
|
||||
const record={name,browser:browser.version(),width,errors:[],blocked:[]}; result.cases.push(record);
|
||||
page.on('pageerror',error=>record.errors.push(error.message));
|
||||
await page.route('**/*',route=> {
|
||||
if(new URL(route.request().url()).origin!==origin) {record.blocked.push(route.request().url());return route.abort();}
|
||||
return route.continue();
|
||||
});
|
||||
await page.addInitScript(dark=> {
|
||||
window.__trustedHistoryClicks=[];
|
||||
document.addEventListener('click',event=>window.__trustedHistoryClicks.push(event.isTrusted),true);
|
||||
document.addEventListener('DOMContentLoaded',()=>{if(dark)document.documentElement.classList.add('dark');});
|
||||
},dark);
|
||||
await page.goto(origin,{waitUntil:'domcontentloaded'});
|
||||
await page.waitForFunction(()=>window.__historyWindows);
|
||||
const cpu=page.locator('[data-chart="CPU"]');
|
||||
const memory=page.locator('[data-chart="Memory"]');
|
||||
record.initialLayout=await cpu.evaluate(el=>({width:el.clientWidth,scroll:el.scrollWidth,document:document.documentElement.clientWidth,documentScroll:document.documentElement.scrollWidth}));
|
||||
if(parent) {
|
||||
assert.equal(await cpu.getByRole('group').count(),0);
|
||||
assert.equal(await cpu.locator('[aria-pressed]').count(),0);
|
||||
await cpu.getByRole('button',{name:'24h',exact:true}).click();
|
||||
assert.equal(await page.evaluate(()=>window.__historyWindows.submissions()),1);
|
||||
if(width<=768) assert.ok(record.initialLayout.scroll>record.initialLayout.width+1,JSON.stringify(record.initialLayout));
|
||||
record.expectedDefects={noNamedGroup:true,noSelectionState:true,nativeFormSubmitted:true,phoneOverflow:width<=768};
|
||||
} else {
|
||||
const cpuGroup=cpu.getByRole('group',{name:'CPU history window'});
|
||||
const memoryGroup=memory.getByRole('group',{name:'Memory history window'});
|
||||
assert.equal(await cpuGroup.getByRole('button',{pressed:true}).count(),1);
|
||||
assert.equal(await cpuGroup.getByRole('button',{name:'1h',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
if(width<=768) {
|
||||
await cpuGroup.getByRole('button',{name:'7d',exact:true}).tap();
|
||||
assert.equal(await cpuGroup.getByRole('button',{name:'7d',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
await memoryGroup.getByRole('button',{name:'6h',exact:true}).tap();
|
||||
await page.waitForFunction(()=>Array.from(document.querySelectorAll('[data-chart="Memory"] button')).some(button=>button.textContent.trim()==='6h' && button.getAttribute('aria-pressed')==='true'));
|
||||
assert.equal(await memoryGroup.getByRole('button',{name:'6h',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
assert.equal(await cpuGroup.getByRole('button',{name:'7d',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
} else {
|
||||
const button=cpuGroup.getByRole('button',{name:'24h',exact:true});
|
||||
await button.focus();await page.keyboard.press('Enter');
|
||||
assert.equal(await button.getAttribute('aria-pressed'),'true');
|
||||
assert.equal(await button.evaluate(el=>el===document.activeElement),true);
|
||||
await page.keyboard.press('Tab');
|
||||
assert.equal(await cpuGroup.getByRole('button',{name:'7d',exact:true}).evaluate(el=>el===document.activeElement),true);
|
||||
await page.keyboard.press('Space');
|
||||
assert.equal(await cpuGroup.getByRole('button',{name:'7d',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
assert.equal(await memoryGroup.getByRole('button',{name:'1h',exact:true}).getAttribute('aria-pressed'),'true');
|
||||
}
|
||||
assert.equal(await cpuGroup.getByRole('button',{pressed:true}).count(),1);
|
||||
assert.equal(await memoryGroup.getByRole('button',{pressed:true}).count(),1);
|
||||
assert.equal(await page.evaluate(()=>window.__historyWindows.submissions()),0);
|
||||
assert.equal(await page.locator('[data-chart="hidden"]').getByRole('group').count(),0);
|
||||
assert.ok(record.initialLayout.scroll<=record.initialLayout.width+1,JSON.stringify(record.initialLayout));
|
||||
assert.ok(record.initialLayout.documentScroll<=record.initialLayout.document+1,JSON.stringify(record.initialLayout));
|
||||
record.selection=await page.locator('[data-chart] button').evaluateAll(buttons=>buttons.map(button=>({text:button.textContent.trim(),pressed:button.getAttribute('aria-pressed'),class:button.className})));
|
||||
for(const button of record.selection) assert.equal(button.class.includes('bg-surface text-base-content'),button.pressed==='true');
|
||||
record.calls=await page.evaluate(()=>window.__historyWindows.calls);
|
||||
record.trusted=await page.evaluate(()=>window.__trustedHistoryClicks);
|
||||
assert.ok(record.trusted.length>=2 && record.trusted.every(Boolean));
|
||||
}
|
||||
assert.deepEqual(record.errors,[]);assert.deepEqual(record.blocked,[]);
|
||||
await page.evaluate(async()=> {
|
||||
// Observe completed finite colour transitions, not a transient tap frame.
|
||||
await Promise.all(document.getAnimations().filter(animation=>Number.isFinite(animation.effect?.getComputedTiming().endTime)).map(animation=>animation.finished.catch(()=>{})));
|
||||
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
|
||||
});
|
||||
const capture=name+'.png';await page.screenshot({path:output+'/'+capture,fullPage:true});
|
||||
result.captures.push({file:capture,sha256:hash(output+'/'+capture)});
|
||||
await browser.close();browser=undefined;
|
||||
}
|
||||
result.result='passed';
|
||||
} finally {
|
||||
if(browser)await browser.close(); result.cleanup.browser=true;
|
||||
if(server)await new Promise(resolve=>server.close(resolve)); result.cleanup.server=true;
|
||||
fs.writeFileSync(output+'/result.json',JSON.stringify(result,null,2)+'\n');
|
||||
}
|
||||
console.log(JSON.stringify(result));
|
||||
})().catch(error=>{console.error(error);process.exitCode=1;});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>History window control verification</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./history-window-controls.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
import { render } from 'solid-js/web';
|
||||
import { HistoryChartHeader } from '../src/components/shared/HistoryChartHeader';
|
||||
import { HISTORY_CHART_RANGES } from '../src/components/shared/historyChartModel';
|
||||
import type { HistoryChartState } from '../src/components/shared/useHistoryChartState';
|
||||
import type { HistoryTimeRange } from '../src/api/charts';
|
||||
import '../src/index.css';
|
||||
|
||||
const calls: Array<{ chart: string; range: HistoryTimeRange }> = [];
|
||||
let submissions = 0;
|
||||
const makeChart = (name: string) => {
|
||||
const [range, setRange] = createSignal<HistoryTimeRange>('1h');
|
||||
return {
|
||||
ranges: HISTORY_CHART_RANGES,
|
||||
range,
|
||||
updateRange: (next: HistoryTimeRange) => {
|
||||
calls.push({ chart: name, range: next });
|
||||
setRange(next);
|
||||
},
|
||||
dataMin: () => 0,
|
||||
dataMax: () => 75,
|
||||
source: () => 'store',
|
||||
} as unknown as HistoryChartState;
|
||||
};
|
||||
const cpu = makeChart('CPU');
|
||||
const memory = makeChart('Memory');
|
||||
|
||||
Object.assign(window, { __historyWindows: { calls, submissions: () => submissions } });
|
||||
render(
|
||||
() => (
|
||||
<main class="min-h-screen space-y-4 bg-surface p-4 text-base-content">
|
||||
<h1 class="text-lg font-semibold">History window controls</h1>
|
||||
<p>Production header and styles; synthetic readings. No collector or API is contacted.</p>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submissions += 1;
|
||||
}}
|
||||
>
|
||||
<section data-chart="CPU" class="mb-4 max-w-xl rounded-md border border-border p-3">
|
||||
<HistoryChartHeader chart={cpu} label="CPU" unit="%" />
|
||||
</section>
|
||||
<section data-chart="Memory" class="mb-4 max-w-xl rounded-md border border-border p-3">
|
||||
<HistoryChartHeader chart={memory} label="Memory" unit="%" />
|
||||
</section>
|
||||
<section data-chart="hidden" class="max-w-xl rounded-md border border-border p-3">
|
||||
<HistoryChartHeader chart={memory} label="Disk I/O" hideSelector />
|
||||
</section>
|
||||
</form>
|
||||
<button id="after-chart" type="button" class="border border-border px-3 py-1">
|
||||
After charts
|
||||
</button>
|
||||
</main>
|
||||
),
|
||||
document.getElementById('root')!,
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "e78de344ea5e2d3211149c2a845cdff90432adf2",
|
||||
"verified_at": "2026-10-06T04:42:13.612566Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/shared/HistoryChartHeader.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/shared/HistoryChartHeader.tsx": "e74f8058a9f26bdd6d906152f2af68139e3b0966f8b6c860e33d3915ad15999e"
|
||||
},
|
||||
"routes": [
|
||||
"Shared History header used by Storage/PBS/NAS metric panels (production-component preview, synthetic state)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 760
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 760
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Two independently scoped named History window groups, exactly one pressed period in each",
|
||||
"Eight existing periods, zero/max summaries and hidden selector",
|
||||
"Phone dark layout wraps without horizontal overflow",
|
||||
"ARIA selection and active styling agree after finite colour transitions"
|
||||
],
|
||||
"interactions": [
|
||||
"Trusted desktop Enter, Tab and Space; focus retained and second chart unchanged",
|
||||
"Trusted phone CPU 7d and Memory 6h taps; independent selection",
|
||||
"Native form owns zero submissions when period buttons activate"
|
||||
],
|
||||
"evidence": {
|
||||
"script": "frontend-modern/browser-tests/history-window-controls.cjs",
|
||||
"script_sha256": "c8fe327e9f0d4377008283a76723f14712532af3b8a34af37150794d8ae2b8b8",
|
||||
"playwright": "1.56.1",
|
||||
"browsers": [
|
||||
{
|
||||
"name": "chromium-desktop",
|
||||
"browser": "141.0.7390.37",
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"name": "webkit-phone",
|
||||
"browser": "26.0",
|
||||
"width": 390
|
||||
}
|
||||
],
|
||||
"captures": [
|
||||
{
|
||||
"file": "chromium-desktop.png",
|
||||
"sha256": "4ddf8126d67b69eec339d13f2701b08fbb722e70baf8bb92b37dc1aed2b6e8b4"
|
||||
},
|
||||
{
|
||||
"file": "webkit-phone.png",
|
||||
"sha256": "06f5c326641853f78153e9404f227f669ab925d10c0e73f181795118a105eaea"
|
||||
}
|
||||
],
|
||||
"limits": "Production header and CSS with synthetic state. No native PBS/NAS, collector, physical device, assistive-device, installed or release acceptance."
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ interface HistoryChartHeaderProps {
|
||||
|
||||
export const HistoryChartHeader: Component<HistoryChartHeaderProps> = (props) => {
|
||||
return (
|
||||
<div class={`flex items-center justify-between ${props.compact ? 'mb-2' : 'mb-4'}`}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class={`flex flex-wrap items-center justify-between gap-2 ${props.compact ? 'mb-2' : 'mb-4'}`}
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content">{props.label || 'History'}</span>
|
||||
<Show when={props.unit}>
|
||||
<span class="text-xs text-slate-400">({props.unit})</span>
|
||||
@@ -36,7 +38,7 @@ export const HistoryChartHeader: Component<HistoryChartHeaderProps> = (props) =>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex min-w-0 max-w-full flex-wrap items-center gap-3">
|
||||
<Show when={props.chart.dataMin() !== null && props.chart.dataMax() !== null}>
|
||||
<div class="flex items-center gap-2 text-[10px]">
|
||||
<span>
|
||||
@@ -54,9 +56,15 @@ export const HistoryChartHeader: Component<HistoryChartHeaderProps> = (props) =>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!props.hideSelector}>
|
||||
<div class="flex bg-surface-hover rounded-md p-0.5">
|
||||
<div
|
||||
role="group"
|
||||
aria-label={props.label ? `${props.label} history window` : 'History window'}
|
||||
class="flex max-w-full flex-wrap bg-surface-hover rounded-md p-0.5"
|
||||
>
|
||||
{props.chart.ranges.map((range) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={props.chart.range() === range}
|
||||
onClick={() => props.chart.updateRange(range)}
|
||||
class={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
props.chart.range() === range
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { ChartsAPI } from '@/api/charts';
|
||||
import { eventBus } from '@/stores/events';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library';
|
||||
import historyChartHeaderSource from '@/components/shared/HistoryChartHeader.tsx?raw';
|
||||
import historyChartHoverGroupSource from '@/components/shared/HistoryChartHoverGroup.tsx?raw';
|
||||
import historyChartOverlaySource from '@/components/shared/HistoryChartOverlay.tsx?raw';
|
||||
@@ -10,6 +10,8 @@ import historyChartSource from '@/components/shared/HistoryChart.tsx?raw';
|
||||
import historyChartModelSource from '@/components/shared/historyChartModel.ts?raw';
|
||||
import historyChartStateSource from '@/components/shared/useHistoryChartState.ts?raw';
|
||||
import historyChartTooltipSource from '@/components/shared/HistoryChartTooltip.tsx?raw';
|
||||
import { HistoryChartHeader } from '@/components/shared/HistoryChartHeader';
|
||||
import type { HistoryChartState } from '@/components/shared/useHistoryChartState';
|
||||
import { HistoryChart, HistoryChartHoverGroup } from '@/components/shared/HistoryChart';
|
||||
import {
|
||||
formatHistoryChartTooltipValue,
|
||||
@@ -708,3 +710,69 @@ describe('History access boundary rendering', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('History window controls', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
const makeChart = () => {
|
||||
const [range, setRange] = createSignal<
|
||||
'1h' | '6h' | '12h' | '24h' | '7d' | '14d' | '30d' | '90d'
|
||||
>('1h');
|
||||
const updateRange = vi.fn(setRange);
|
||||
const chart = {
|
||||
ranges: HISTORY_CHART_RANGES,
|
||||
range,
|
||||
updateRange,
|
||||
dataMin: () => 0,
|
||||
dataMax: () => 75,
|
||||
source: () => 'store',
|
||||
} as unknown as HistoryChartState;
|
||||
return { chart, updateRange };
|
||||
};
|
||||
|
||||
it('names each chart window and announces only its selected period', () => {
|
||||
const cpu = makeChart();
|
||||
const memory = makeChart();
|
||||
render(() => (
|
||||
<>
|
||||
<HistoryChartHeader chart={cpu.chart} label="CPU" unit="%" />
|
||||
<HistoryChartHeader chart={memory.chart} label="Memory" unit="%" />
|
||||
</>
|
||||
));
|
||||
const cpuGroup = within(screen.getByRole('group', { name: 'CPU history window' }));
|
||||
const memoryGroup = within(screen.getByRole('group', { name: 'Memory history window' }));
|
||||
expect(cpuGroup.getAllByRole('button', { pressed: true })).toHaveLength(1);
|
||||
expect(cpuGroup.getByRole('button', { name: '1h' })).toHaveAttribute('aria-pressed', 'true');
|
||||
fireEvent.click(cpuGroup.getByRole('button', { name: '24h' }));
|
||||
expect(cpu.updateRange).toHaveBeenCalledExactlyOnceWith('24h');
|
||||
expect(cpuGroup.getByRole('button', { name: '24h' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(cpuGroup.getByRole('button', { name: '1h' })).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(cpuGroup.getAllByRole('button', { pressed: true })).toHaveLength(1);
|
||||
expect(memoryGroup.getByRole('button', { name: '1h' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(memory.updateRange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps range changes out of surrounding form submission', () => {
|
||||
const { chart } = makeChart();
|
||||
const onSubmit = vi.fn((event: SubmitEvent) => event.preventDefault());
|
||||
render(() => (
|
||||
<form onSubmit={onSubmit}>
|
||||
<HistoryChartHeader chart={chart} />
|
||||
</form>
|
||||
));
|
||||
const group = within(screen.getByRole('group', { name: 'History window' }));
|
||||
for (const button of group.getAllByRole('button'))
|
||||
expect(button).toHaveAttribute('type', 'button');
|
||||
fireEvent.click(group.getByRole('button', { name: '7d' }));
|
||||
expect(group.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not expose a phantom window group when the caller hides it', () => {
|
||||
const { chart } = makeChart();
|
||||
render(() => <HistoryChartHeader chart={chart} label="Disk I/O" hideSelector />);
|
||||
expect(screen.queryByRole('group')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Disk I/O')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user