From c6edeee869a11f9206390e855fc63da744fb394a Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:52:26 +0100 Subject: [PATCH 1/2] Distinguish empty History from failed background refreshes Keep the last successful readings and source visible with an accessible warning until recovery or selection replacement. Stop promising future collection for empty history. Cover failure and recovery with request-state and two-engine browser controls. Change-source: pulse-maintainer --- .../subsystems/frontend-primitives.md | 14 +++ .../browser-tests/history-status.cjs | 93 +++++++++++++++++++ .../browser-tests/history-status.html | 1 + .../browser-tests/history-status.tsx | 30 ++++++ frontend-modern/browser-verification.json | 35 +++---- .../src/components/shared/HistoryChart.tsx | 15 ++- .../components/shared/HistoryChartOverlay.tsx | 2 +- .../shared/__tests__/HistoryChart.test.tsx | 3 +- .../__tests__/useHistoryChartState.test.tsx | 69 ++++++++++---- .../components/shared/useHistoryChartState.ts | 8 +- 10 files changed, 229 insertions(+), 41 deletions(-) create mode 100644 frontend-modern/browser-tests/history-status.cjs create mode 100644 frontend-modern/browser-tests/history-status.html create mode 100644 frontend-modern/browser-tests/history-status.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c213daa7bf..5e2e9760bf 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -7699,3 +7699,17 @@ selection changes and empty data clear stale readings. No readings are invented for missing data. Tooltips reserve room for a wrapped local timestamp and reading. The mounted HistoryChart regression and history-keyboard browser fixture cover navigation, bounds, focus exit, refresh and replacement. Synthetic browser proof does not establish native collector or assistive-device acceptance. + +### Shared History request-state honesty + +An empty successful response states only that the selected time range has no +samples; it does not claim collection is running or promise future readings. +A failed matching background refresh keeps the last successful data and source, +with a visible polite status also included in the chart's accessible description. +The warning persists while retrying and clears only on success or selection +replacement. This applies to previously empty results too. Initial failures +remain separate errors. No polling frequency, request or access policy changes. +`useHistoryChartState.test.tsx` covers failure, pending recovery, empty results +and selection changes; `browser-tests/history-status.cjs` checks actual chart +states in desktop Chromium and phone WebKit with synthetic transport failures. +These checks do not establish native collection or released availability. diff --git a/frontend-modern/browser-tests/history-status.cjs b/frontend-modern/browser-tests/history-status.cjs new file mode 100644 index 0000000000..8fe339bdcc --- /dev/null +++ b/frontend-modern/browser-tests/history-status.cjs @@ -0,0 +1,93 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium, webkit } = require('playwright'); +(async () => { + const root = '/workspace/frontend-modern'; + process.chdir(root); + const artifacts = path.join(root, 'node_modules/history-status-proof'); + fs.mkdirSync(artifacts, { recursive: true }); + 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(artifacts, 'vite-')), server: { host: '127.0.0.1', port: 5224, strictPort: true } }); + let browser; + const observations = []; + const parent = process.argv.includes('--parent'); + try { + await server.listen(); + for (const [name, engine, width, height] of [['chromium', chromium, 1365, 900], ['webkit', webkit, 390, 844]]) { + browser = await engine.launch(name === 'chromium' ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } : { headless: true }); + const page = await browser.newPage({ viewport: { width, height }, hasTouch: name === 'webkit', isMobile: name === 'webkit' }); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + let mode = 'empty', calls = 0; + const pending = []; + await page.route('**/*', route => { + const url = new URL(route.request().url()); + if (url.origin !== 'http://127.0.0.1:5224') return route.abort(); + if (!url.pathname.startsWith('/api/')) return route.continue(); + if (url.pathname.includes('/metrics-store/history')) { + calls++; + if (mode === 'pending') { pending.push(route); return; } + if (mode === 'failed') return route.fulfill({status: 503, json: {error: 'Synthetic unavailable'}}); + return route.fulfill({json: {points: mode === 'empty' ? [] : [0,1,2].map(i => ({timestamp: 1790942400000 + i*60000, value: 20+i*10, min: 20+i*10, max: 20+i*10})), source: 'store'}}); + } + return route.fulfill({json: url.pathname === '/api/license/runtime-capabilities' ? {capabilities: [], limits: [], max_history_days: 7, hosted_mode: false, runtime: {build:'community'}, blocked_capabilities: []} : {data: [], enabled: false}}); + }); + await page.clock.install(); + await page.goto('http://127.0.0.1:5224/browser-tests/history-status.html'); + if (name === 'webkit') await page.evaluate(() => document.documentElement.classList.add('dark')); + const chart = page.getByRole('img', {name:'Usage chart', exact:true}); + await chart.waitFor(); + const description = page.locator('#' + await chart.getAttribute('aria-describedby')); + async function state(phase, expected) { + await page.waitForFunction(({id, expected}) => document.getElementById(id).textContent.includes(expected), {id: await chart.getAttribute('aria-describedby'), expected}); + const actual = await description.textContent(); + const dimensions = await page.evaluate(() => ({scroll:document.documentElement.scrollWidth, width:innerWidth})); + assert.ok(dimensions.scroll <= dimensions.width + 1); + await page.screenshot({path:path.join(artifacts, `${parent?'parent-':''}${name}-${phase}.png`),fullPage:true}); + observations.push({name, version:browser.version(),phase,actual,dimensions,calls}); + } + await state('empty', 'No 1-hour'); + assert.equal(await page.getByText(parent ? 'Collecting data... History will appear here.' : 'No history samples in this time range.', {exact:true}).count(),1); + mode = 'populated'; + await page.clock.runFor(10_000); + await state('loaded','40.0%'); + mode = 'failed'; + await page.clock.runFor(10_000); + if (parent) { + await page.waitForTimeout(100); + assert.ok(!(await description.textContent()).includes('Could not refresh')); + assert.equal(await page.getByText('Could not refresh history. Showing the last successful result.',{exact:true}).count(),0); + await state('silent-failure','40.0%'); + await browser.close(); browser = null; break; + } + await state('refresh-failed','Could not refresh history'); + assert.ok((await description.textContent()).includes('40.0%')); + assert.ok((await page.getByRole('status', {name:'History refresh status'}).textContent()).includes('last successful result')); + mode = 'pending'; + await page.clock.runFor(10_000); + await state('retry-pending','Could not refresh history'); + for (let i=0; !pending.length && i<100; i++) await new Promise(r=>setTimeout(r,20)); + assert.ok(pending.length > 0, 'pending refresh observed'); + for (const route of pending.splice(0)) await route.fulfill({json:{points:[],source:'store'}}); + await state('recovered-empty','No 1-hour'); + assert.equal(await page.getByRole('status', {name:'History refresh status'}).textContent(),''); + mode = 'failed'; + await page.clock.runFor(10_000); + await state('empty-refresh-failed','Could not refresh history'); + mode = 'pending'; + await page.getByRole('button',{name:'Select pool b'}).click(); + await state('target-loading','Loading'); + assert.equal(await page.getByRole('status', {name:'History refresh status'}).textContent(),''); + for (let i=0; !pending.length && i<100; i++) await new Promise(r=>setTimeout(r,20)); + assert.ok(pending.length > 0, 'pending refresh observed'); + for (const route of pending.splice(0)) await route.fulfill({status:503,json:{error:'Synthetic unavailable'}}); + await state('initial-failed','could not be loaded'); + assert.equal(await page.getByRole('status', {name:'History refresh status'}).textContent(),''); + assert.deepEqual(errors,[]); + await browser.close(); browser=null; + } + fs.writeFileSync(path.join(artifacts,parent?'parent.json':'result.json'),JSON.stringify({playwright:require('playwright/package.json').version,observations},null,2)); + console.log(JSON.stringify({result:'passed',states:observations.length,parent})); + } finally { if(browser) await browser.close(); await server.close(); } +})().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/frontend-modern/browser-tests/history-status.html b/frontend-modern/browser-tests/history-status.html new file mode 100644 index 0000000000..ce78449b9a --- /dev/null +++ b/frontend-modern/browser-tests/history-status.html @@ -0,0 +1 @@ +History request states verification
diff --git a/frontend-modern/browser-tests/history-status.tsx b/frontend-modern/browser-tests/history-status.tsx new file mode 100644 index 0000000000..19099f7933 --- /dev/null +++ b/frontend-modern/browser-tests/history-status.tsx @@ -0,0 +1,30 @@ +import { createSignal } from 'solid-js'; +import { render } from 'solid-js/web'; +import { HistoryChart } from '../src/components/shared/HistoryChart'; +import '../src/index.css'; +const Fixture = () => { + const [target, setTarget] = createSignal('a'); + return ( +
+

Storage History request states

+

Synthetic transport; production History chart. Selected pool: {target()}

+ +
+ +
+
+ ); +}; +render(() => , document.getElementById('root')!); diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index d0574b1b31..f9f7e62f7e 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,20 +1,20 @@ { "version": 1, - "base_sha": "cf5d6cdf066539b6e29f589e42d408edb1ceb73a", - "verified_at": "2026-10-02T15:02:34.899181Z", + "base_sha": "65a36dd4e7a83e682b9dadc4bb624d79fcb10546", + "verified_at": "2026-10-02T15:52:23.739120+00:00", "result": "passed", "changed_paths": [ "frontend-modern/src/components/shared/HistoryChart.tsx", - "frontend-modern/src/components/shared/useHistoryChartState.ts", - "frontend-modern/src/components/shared/historyChartModel.ts" + "frontend-modern/src/components/shared/HistoryChartOverlay.tsx", + "frontend-modern/src/components/shared/useHistoryChartState.ts" ], "content_sha256": { - "frontend-modern/src/components/shared/HistoryChart.tsx": "24cd863fb6288bc2c13ef2d26e02aad95d3e4f54278f3078b1703fabf8785f8c", - "frontend-modern/src/components/shared/useHistoryChartState.ts": "b1ebd93d5fee328b78f8f88f1837514a30d12be069d852d08f5d7f2b6ce999ec", - "frontend-modern/src/components/shared/historyChartModel.ts": "bd9e8954ef7e274a1eb026752d304556df303f612c4485369f8c90a7183cc751" + "frontend-modern/src/components/shared/HistoryChart.tsx": "a2ab0198a705e66ce9bea222066078709c8334fc342415916b8c179987b139e2", + "frontend-modern/src/components/shared/HistoryChartOverlay.tsx": "4192731461a19ba7c33185c2e616c5023dfef58d93e3893cd3c4fceb58598391", + "frontend-modern/src/components/shared/useHistoryChartState.ts": "8bebaa331f1e2dfc57587f517628bed1b0c4f7d9c9466e21597e7f9f7a4d7137" }, "routes": [ - "/browser-tests/history-keyboard.html" + "/browser-tests/history-status.html" ], "viewports": [ { @@ -27,22 +27,25 @@ } ], "states": [ - "Populated, empty and linked charts in Chromium light and WebKit phone dark; synthetic data, not native PBS acceptance", - "Visible keyboard focus, wrapped timestamp and reading contained within tooltip; no horizontal overflow or page errors" + "Empty successful result, loaded readings, failed refresh, pending recovery, recovered empty result, failed refresh of empty result, changed-target loading and initial failure", + "Chromium desktop light and WebKit phone dark; screenshots inspected, no horizontal overflow or page errors; synthetic transport, not native collection" ], "interactions": [ - "Tab into and out of both charts; arrows, Home/End, endpoint clamping, Escape; only focused chart announces keyboard readings", - "Parent Tab skips both charts; final empty chart invents no reading" + "Advance polling clock without altering production timers; fail and recover HTTP responses; switch pool target", + "Parent control reproduces misleading empty promise and silent failed refresh; final warning retains data and clears only on recovery or selection replacement" ], - "command": "pulse-worker-browser frontend-modern/browser-tests/history-keyboard.cjs", + "command": "pulse-worker-browser frontend-modern/browser-tests/history-status.cjs", "browser_versions": { "playwright": "1.56.1", "chromium": "141.0.7390.37", "webkit": "26.0" }, "artifacts": [ - "/var/lib/pulse-maintainer/worker-outputs/web-product-tuqfk84d/browser/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-tuqfk84d/browser/webkit-keyboard.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-tuqfk84d/browser/chromium-keyboard.png" + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/parent.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/chromium-refresh-failed.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/webkit-refresh-failed.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/webkit-empty.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-d89v1n1d/browser/webkit-initial-failed.png" ] } diff --git a/frontend-modern/src/components/shared/HistoryChart.tsx b/frontend-modern/src/components/shared/HistoryChart.tsx index 8a37f456bc..ac36f7f4e7 100644 --- a/frontend-modern/src/components/shared/HistoryChart.tsx +++ b/frontend-modern/src/components/shared/HistoryChart.tsx @@ -28,6 +28,9 @@ export const HistoryChart: Component = (props) => { }, hoverGroup, ); + const refreshStatus = createMemo(() => + chart.refreshFailed() ? 'Could not refresh history. Showing the last successful result.' : '', + ); const accessibleDescription = createMemo(() => getHistoryChartAccessibleDescription({ data: chart.data(), @@ -51,6 +54,14 @@ export const HistoryChart: Component = (props) => { unit={props.unit} /> +

+ {refreshStatus()} +

+
= (props) => { onMouseLeave={chart.handleMouseLeave} />

- {accessibleDescription()} Use Left and Right arrow keys to inspect readings, Home and End - for the first and last reading, and Escape to clear inspection. + {refreshStatus()} {accessibleDescription()} Use Left and Right arrow keys to inspect + readings, Home and End for the first and last reading, and Escape to clear inspection.

{chart.keyboardInspecting() && chart.hoveredPoint() diff --git a/frontend-modern/src/components/shared/HistoryChartOverlay.tsx b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx index e5d9f01866..2244e8d0db 100644 --- a/frontend-modern/src/components/shared/HistoryChartOverlay.tsx +++ b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx @@ -35,7 +35,7 @@ export const HistoryChartOverlay: Component = (props)

-

Collecting data... History will appear here.

+

No history samples in this time range.

diff --git a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx index 19aa1eb53c..7bfe9b99c5 100644 --- a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx @@ -104,7 +104,8 @@ describe('HistoryChart', () => { expect(historyChartHeaderSource).not.toContain('ChartsAPI.getMetricsHistory'); expect(historyChartHeaderSource).not.toContain('setupCanvasDPR'); - expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('No history samples in this time range.'); + expect(historyChartOverlaySource).not.toContain('History will appear here.'); expect(historyChartOverlaySource).toContain( 'Historical data beyond {props.chart.lockDays()} days requires a higher license plan.', ); diff --git a/frontend-modern/src/components/shared/__tests__/useHistoryChartState.test.tsx b/frontend-modern/src/components/shared/__tests__/useHistoryChartState.test.tsx index 0942f115ee..872920316b 100644 --- a/frontend-modern/src/components/shared/__tests__/useHistoryChartState.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/useHistoryChartState.test.tsx @@ -117,25 +117,56 @@ describe('History request ownership', () => { }, ); - it('does not overlap polling and preserves matching samples on refresh failure', async () => { - const initial = deferred(); - request.mockReturnValueOnce(initial.promise); - const { state } = mount(); - vi.advanceTimersByTime(120_000); - expect(request).toHaveBeenCalledTimes(1); - initial.resolve({ points: points(10), source: 'store' } as never); - await settle(); - const refresh = deferred(); - request.mockReturnValueOnce(refresh.promise); - vi.advanceTimersByTime(120_000); - expect(request).toHaveBeenCalledTimes(2); - vi.spyOn(console, 'error').mockImplementation(() => {}); - refresh.reject(new Error('refresh failed')); - await settle(); - expect(state.data()).toEqual(points(10)); - expect(state.error()).toBeNull(); - expect(state.loading()).toBe(false); - }); + it.each([{ samples: points(10) }, { samples: [] }])( + 'retains the last result %j and exposes refresh failure until recovery', + async ({ samples }) => { + const initial = deferred(); + request.mockReturnValueOnce(initial.promise); + const { state } = mount(); + vi.advanceTimersByTime(120_000); + expect(request).toHaveBeenCalledTimes(1); + initial.resolve({ points: samples, source: 'store' } as never); + await settle(); + const refresh = deferred(); + request.mockReturnValueOnce(refresh.promise); + vi.advanceTimersByTime(120_000); + expect(request).toHaveBeenCalledTimes(2); + vi.spyOn(console, 'error').mockImplementation(() => {}); + refresh.reject(new Error('refresh failed')); + await settle(); + expect(state.data()).toEqual(samples); + expect(state.error()).toBeNull(); + expect(state.refreshFailed()).toBe(true); + expect(state.source()).toBe('store'); + expect(state.loading()).toBe(false); + const recovery = deferred(); + request.mockReturnValueOnce(recovery.promise); + vi.advanceTimersByTime(10_000); + expect(state.refreshFailed()).toBe(true); + recovery.resolve({ points: points(30), source: 'memory' } as never); + await settle(); + expect(state.refreshFailed()).toBe(false); + expect(state.data()).toEqual(points(30)); + expect(state.source()).toBe('memory'); + }, + ); + + it.each([{ resourceId: 'b' }, { range: '6h' }, { data: [] }, { range: '90d' }])( + 'clears refresh failure on selection replacement %j', + async (next) => { + request.mockResolvedValueOnce({ points: points(10), source: 'store' } as never); + const { state, change } = mount(); + await settle(); + vi.spyOn(console, 'error').mockImplementation(() => {}); + request.mockRejectedValueOnce(new Error('refresh failed')); + await vi.advanceTimersByTimeAsync(10_000); + expect(state.refreshFailed()).toBe(true); + request.mockReturnValueOnce(deferred().promise); + change(next as Partial); + expect(state.refreshFailed()).toBe(false); + expect(state.data()).toEqual([]); + }, + ); it('cancels fetched data when supplied data takes ownership, including empty samples', async () => { const old = deferred(); diff --git a/frontend-modern/src/components/shared/useHistoryChartState.ts b/frontend-modern/src/components/shared/useHistoryChartState.ts index 0ad5c2033c..a63b01d353 100644 --- a/frontend-modern/src/components/shared/useHistoryChartState.ts +++ b/frontend-modern/src/components/shared/useHistoryChartState.ts @@ -36,6 +36,7 @@ export function useHistoryChartState( const [data, setData] = createSignal(props.data ?? []); const [keyboardInspecting, setKeyboardInspecting] = createSignal(false); const [loading, setLoading] = createSignal(false); + const [refreshFailed, setRefreshFailed] = createSignal(false); const [error, setError] = createSignal(null); const [source, setSource] = createSignal<'store' | 'memory' | 'live' | 'mock_synthetic' | null>( null, @@ -119,6 +120,7 @@ export function useHistoryChartState( setData(suppliedData ?? []); setSource(suppliedData !== undefined ? 'live' : null); setError(null); + setRefreshFailed(false); setLoading(false); const selection = JSON.stringify([ resourceType, @@ -154,12 +156,13 @@ export function useHistoryChartState( if (!active) return; setData('points' in result ? (result.points ?? []) : []); setSource(result.source ?? 'store'); + setRefreshFailed(false); hasLoaded = true; } catch (err) { if (!active) return; console.error('Failed to fetch metrics history:', err); - if (!hasLoaded) setError('Failed to load history data'); - setSource(null); + if (hasLoaded) setRefreshFailed(true); + else setError('Failed to load history data'); } finally { if (active) { pending = false; @@ -416,6 +419,7 @@ export function useHistoryChartState( dataMax, dataMin, error, + refreshFailed, handleFocus, handleBlur, handleKeyDown, From df18123e7da3b682eed811c0e01c94a44dc2c29c Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:53:57 +0100 Subject: [PATCH 2/2] Align History guardrails and record valid UTC browser proof A fresh two-engine browser pass verifies the unchanged runtime source. Update the cross-surface empty-state assertions to the truthful copy. Contract-Neutral: Verification and receipt corrections only; runtime source and subsystem contracts are unchanged. Change-source: pulse-maintainer --- frontend-modern/browser-verification.json | 4 ++-- .../src/components/shared/SharedPrimitives.guardrails.test.ts | 3 ++- .../utils/__tests__/frontendResourceTypeBoundaries.test.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index f9f7e62f7e..bd379e2ebb 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,7 +1,7 @@ { "version": 1, - "base_sha": "65a36dd4e7a83e682b9dadc4bb624d79fcb10546", - "verified_at": "2026-10-02T15:52:23.739120+00:00", + "base_sha": "c6edeee869a11f9206390e855fc63da744fb394a", + "verified_at": "2026-10-02T15:53:54.725571Z", "result": "passed", "changed_paths": [ "frontend-modern/src/components/shared/HistoryChart.tsx", diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 1325a4260f..95c9485247 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -5283,7 +5283,8 @@ describe('shared primitive guardrails', () => { expect(historyChartHeaderSource).not.toContain('ChartsAPI.getMetricsHistory'); expect(historyChartHeaderSource).not.toContain('setupCanvasDPR'); - expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('No history samples in this time range.'); + expect(historyChartOverlaySource).not.toContain('History will appear here.'); expect(historyChartOverlaySource).toContain( 'Historical data beyond {props.chart.lockDays()} days requires a higher license plan.', ); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 35e2debb92..9dfa485452 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -2452,7 +2452,8 @@ describe('frontend resource type boundaries', () => { expect(historyChartModelSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('getHistoryChartScale'); expect(historyChartHeaderSource).toContain('formatHistoryChartTooltipValue'); - expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('No history samples in this time range.'); + expect(historyChartOverlaySource).not.toContain('History will appear here.'); expect(historyChartOverlaySource).not.toContain('presentationPolicyHidesUpgradePrompts'); expect(historyChartOverlaySource).not.toContain( 'Unlock {props.chart.lockTierLabel()} Features',