diff --git a/frontend-modern/browser-tests/guest-storage-2263.cjs b/frontend-modern/browser-tests/guest-storage-2263.cjs index 2b7592af45..4958c9f9f2 100644 --- a/frontend-modern/browser-tests/guest-storage-2263.cjs +++ b/frontend-modern/browser-tests/guest-storage-2263.cjs @@ -17,73 +17,219 @@ const { chromium } = require('playwright'); let browser; try { await server.listen(); - browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] }); + browser = await chromium.launch({ + headless: true, + channel: 'chromium', + args: ['--no-sandbox'], + }); const requests = []; + const detailRequests = []; const pageErrors = []; const disk = { - id: 'disk-guest-101', parentId: 'vm-resource-101', type: 'physical_disk', - name: 'guest-sata', displayName: 'Guest SATA', platformId: 'lab', - platformType: 'proxmox-pve', sourceType: 'agent', status: 'online', + id: 'disk-guest-101', + parentId: 'vm-resource-101', + type: 'physical_disk', + name: 'guest-sata', + displayName: 'Guest SATA', + platformId: 'lab', + platformType: 'proxmox-pve', + sourceType: 'agent', + status: 'online', lastSeen: new Date().toISOString(), - physicalDisk: { devPath: '/dev/sda', model: 'Guest SATA', diskType: 'sata', - sizeBytes: 1_000_000_000, health: 'PASSED', temperature: 45, - smart: { reallocatedSectors: 0, pendingSectors: 0, udmaCrcErrors: 2 } }, + physicalDisk: { + devPath: '/dev/sda', + model: 'Guest SATA', + diskType: 'sata', + sizeBytes: 1_000_000_000, + health: 'PASSED', + temperature: 45, + smart: { reallocatedSectors: 0, pendingSectors: 0, udmaCrcErrors: 2 }, + }, }; for (const width of [1440, 390]) { - const page = await browser.newPage({ viewport: { width, height: width === 390 ? 844 : 900 } }); + const page = await browser.newPage({ + viewport: { width, height: width === 390 ? 844 : 900 }, + }); page.on('pageerror', (error) => pageErrors.push(`${width}: ${error.message}`)); - page.on('requestfailed', (request) => console.error('requestfailed', request.url(), request.failure())); + page.on('requestfailed', (request) => + console.error('requestfailed', request.url(), request.failure()), + ); + page.on('request', (request) => { + if (new URL(request.url()).pathname === '/src/components/Storage/DiskDetail.tsx') { + detailRequests.push({ width, url: request.url() }); + } + }); await page.routeWebSocket(/\/ws(?:\?|$)/, () => {}); await page.route('**/*', (route) => { const url = new URL(route.request().url()); if (url.origin !== 'http://127.0.0.1:5199') return route.abort(); if (url.pathname === '/api/resources') { const parent = url.searchParams.get('parent'); - requests.push({ width, type: url.searchParams.get('type'), parent, - page: url.searchParams.get('page'), limit: url.searchParams.get('limit') }); - if (parent === 'vm-resource-error') return route.fulfill({ status: 503, json: { error: 'fixture' } }); - return route.fulfill({ json: { resources: [disk, { ...disk, id: 'host-disk', - parentId: 'pve-host' }], meta: { totalPages: 1 } } }); + requests.push({ + width, + type: url.searchParams.get('type'), + parent, + page: url.searchParams.get('page'), + limit: url.searchParams.get('limit'), + }); + if (parent === 'vm-resource-error') + return route.fulfill({ status: 503, json: { error: 'fixture' } }); + return route.fulfill({ + json: { + resources: [disk, { ...disk, id: 'host-disk', parentId: 'pve-host' }], + meta: { totalPages: 1 }, + }, + }); } if (url.pathname.startsWith('/api/')) return route.fulfill({ json: {} }); return route.continue(); }); + let releaseDetail; + await page.route('**/src/components/Storage/DiskDetail.tsx*', async (route) => { + await new Promise((resolve) => { + releaseDetail = resolve; + }); + await route.continue(); + }); await page.goto('http://127.0.0.1:5199/browser-tests/guest-storage-2263.html', { - waitUntil: 'domcontentloaded', timeout: 120_000, + waitUntil: 'domcontentloaded', + timeout: 120_000, }); await page.locator('[data-testid="guest-physical-disks"]').waitFor(); assert.match(await page.locator('body').innerText(), /Physical Disks & SMART \(1\)/i); assert.equal(await page.locator('[data-testid="guest-physical-disk"]').count(), 1); assert.match(await page.locator('body').innerText(), /Guest RAID/i); assert.match(await page.locator('body').innerText(), /raid1/); + assert.equal( + detailRequests.filter((request) => request.width === width).length, + 0, + 'SMART history module must not load before disk expansion', + ); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-collapsed-${width}.png`), + fullPage: true, + }); await page.locator('[data-testid="guest-physical-disk"] summary').focus(); + const detailRequest = page.waitForRequest( + (request) => new URL(request.url()).pathname === '/src/components/Storage/DiskDetail.tsx', + ); await page.keyboard.press('Enter'); + await detailRequest; + assert.equal( + await page.getByText('Reallocated Sectors').count(), + 0, + 'SMART detail must remain suspended while its module is loading', + ); + assert.equal( + await page.getByRole('status').filter({ hasText: 'Loading SMART details…' }).count(), + 1, + 'expanded disk exposes a loading state while the SMART module is pending', + ); + assert.equal( + await page + .locator('[data-testid="guest-physical-disk"] summary') + .evaluate((summary) => summary === document.activeElement), + true, + 'keyboard focus remains on disk summary', + ); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-loading-${width}.png`), + fullPage: true, + }); + releaseDetail(); await page.getByText('Reallocated Sectors').waitFor(); - await page.screenshot({ path: path.join(artifacts, `guest-storage-2263-${width}.png`), fullPage: true }); + assert.equal( + detailRequests.filter((request) => request.width === width).length, + 1, + 'SMART history module loads once after disk expansion', + ); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-expanded-${width}.png`), + fullPage: true, + }); + await page.getByRole('tab', { name: 'History' }).click(); + assert.equal( + await page.getByRole('tab', { name: 'History' }).getAttribute('aria-selected'), + 'true', + ); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-history-${width}.png`), + fullPage: true, + }); + await page.getByRole('tab', { name: 'Overview' }).click(); + assert.equal( + await page.getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected'), + 'true', + ); + await page.locator('[data-testid="guest-physical-disk"] summary').click(); + await page.getByText('Reallocated Sectors').waitFor({ state: 'hidden' }); + assert.equal( + await page.getByText('Reallocated Sectors').isVisible(), + false, + 'pointer closes the expanded SMART detail', + ); + await page.locator('[data-testid="guest-physical-disk"] summary').click(); + await page.getByText('Reallocated Sectors').waitFor(); + assert.equal( + detailRequests.filter((request) => request.width === width).length, + 1, + 'reopening a disk does not reload the SMART module', + ); const beforeNoAgent = requests.length; await page.getByRole('button', { name: 'No agent' }).click(); assert.equal(await page.locator('[data-testid="guest-physical-disks"]').count(), 0); - assert.equal(requests.length, beforeNoAgent, 'uninstrumented guest must not fetch physical disks'); + assert.equal( + requests.length, + beforeNoAgent, + 'uninstrumented guest must not fetch physical disks', + ); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-no-agent-${width}.png`), + fullPage: true, + }); await page.getByRole('button', { name: 'Unavailable query' }).click(); await page.getByText('Physical disk details are unavailable.').waitFor(); assert.equal(await page.locator('[data-testid="guest-physical-disks"]').count(), 0); - const dimensions = await page.evaluate(() => ({ scroll: document.documentElement.scrollWidth, - inner: window.innerWidth })); - assert.ok(dimensions.scroll <= dimensions.inner + 1, `${width}px horizontal overflow: ${JSON.stringify(dimensions)}`); + await page.screenshot({ + path: path.join(artifacts, `guest-storage-2263-error-${width}.png`), + fullPage: true, + }); + const dimensions = await page.evaluate(() => ({ + scroll: document.documentElement.scrollWidth, + inner: window.innerWidth, + })); + assert.ok( + dimensions.scroll <= dimensions.inner + 1, + `${width}px horizontal overflow: ${JSON.stringify(dimensions)}`, + ); await page.close(); } assert.equal(pageErrors.length, 0, `page errors: ${pageErrors.join('; ')}`); - assert.deepEqual(requests.map(({ type, parent, page, limit }) => ({ type, parent, page, limit })), [ - { type: 'physical_disk', parent: 'vm-resource-101', page: '1', limit: '100' }, - { type: 'physical_disk', parent: 'vm-resource-error', page: '1', limit: '100' }, - { type: 'physical_disk', parent: 'vm-resource-101', page: '1', limit: '100' }, - { type: 'physical_disk', parent: 'vm-resource-error', page: '1', limit: '100' }, - ]); - console.log(JSON.stringify({ result: 'passed', browser: browser.version(), - viewports: [1440, 390], route: '/browser-tests/guest-storage-2263.html', - requests, pageErrors })); + assert.deepEqual( + requests.map(({ type, parent, page, limit }) => ({ type, parent, page, limit })), + [ + { type: 'physical_disk', parent: 'vm-resource-101', page: '1', limit: '100' }, + { type: 'physical_disk', parent: 'vm-resource-error', page: '1', limit: '100' }, + { type: 'physical_disk', parent: 'vm-resource-101', page: '1', limit: '100' }, + { type: 'physical_disk', parent: 'vm-resource-error', page: '1', limit: '100' }, + ], + ); + console.log( + JSON.stringify({ + result: 'passed', + browser: browser.version(), + viewports: [1440, 390], + route: '/browser-tests/guest-storage-2263.html', + requests, + detailRequests, + pageErrors, + }), + ); } finally { if (browser) await browser.close(); await server.close(); } -})().catch((error) => { console.error(error); process.exitCode = 1; }); +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 2074a17d0b..738c89c07e 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,22 +1,13 @@ { "version": 1, - "base_sha": "3829d91456fe4a121e4e6f0cca6a6ef1fe0c7f2a", - "verified_at": "2026-09-26T19:37:23Z", + "base_sha": "cd43b6c692383865592c17a0853d33a229ae46d6", + "verified_at": "2026-09-27T00:26:36Z", "result": "passed", - "changed_paths": [ - "frontend-modern/src/components/Settings/SSOProvidersPanel.tsx", - "frontend-modern/src/components/Settings/ssoProvidersModel.ts", - "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx" - ], + "changed_paths": ["frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx"], "content_sha256": { - "frontend-modern/src/components/Settings/SSOProvidersPanel.tsx": "781cbb8913f42008456afa7701351c4828aabc140c9e35595c47fd2330bca0cf", - "frontend-modern/src/components/Settings/ssoProvidersModel.ts": "9f10845e553633d73bff7ac5fb295d4ce5cc0c4250d0eef1bac2ce18bb31ef2c", - "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "83dfb9eefe57064111244b2fef8e059b02000e5b07844335c0577516b599bf44" + "frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx": "1a0b15141ee41e45dc884406bc4689828b034704c80f914fc0d2baa93d0b6edd" }, - "routes": [ - "/browser-tests/sso-mappings-2266.html", - "/browser-tests/patrol-off-attention-2257.html" - ], + "routes": ["/browser-tests/guest-storage-2263.html"], "viewports": [ { "width": 1440, @@ -28,18 +19,21 @@ } ], "states": [ - "OIDC provider edit dialog with Server Access=admin, Everyone=viewer preserved after save and reopen", - "Patrol off with model setup blocked: setup task and independent attention decision visible together", - "Selected Database VM disk-pressure detail with impact and next step, then return to attention list", - "Patrol page remounted: setup task and attention decision still visible", - "Desktop and phone screenshots inspected: mapping field and Patrol list/detail visible without clipping or horizontal overflow" + "Linked Proxmox guest with one physical disk and RAID array: disk row collapsed, SMART detail module not requested", + "Keyboard-expanded disk while the SMART module request is held: visible Loading SMART details status, summary retains focus", + "Loaded SMART overview: temperature, reallocated sectors, pending sectors and CRC errors visible; module requested once", + "SMART History tab selected with empty fixture chart data: chart collection placeholders and range control visible", + "Pointer-collapsed and reopened disk: overview restored without reloading the SMART module", + "Guest without a linked agent: no physical-disk card and no child-resource request", + "Linked guest child-resource request failing with HTTP 503: unavailable message visible, no misleading disk card", + "Desktop and phone screenshots inspected for placement, clipping, stacking, scrolling and horizontal overflow" ], "interactions": [ - "Edit and save OIDC group mappings containing a multi-word group and newline separator, reopen and verify exact map; retain OIDC scopes", - "Open access restrictions and role mapping, focus the mapping field, inspect desktop and phone dialogs, dismiss with Escape", - "With Patrol off, open Start review, inspect affected resource, consequence and next step, then close detail via desktop close or phone Back to list", - "Remount Patrol fixture to verify setup task and independent attention decision persist; inspect four responsive list/detail screenshots" + "Focus disk summary and press Enter; hold then release its lazy module request, verifying loading status, focus retention and loaded SMART attributes", + "Click History and Overview tabs, verifying selection and the chart/range layout at desktop and phone widths", + "Click the summary to close and reopen detail, verifying no second lazy-module request", + "Switch to No agent and Unavailable query fixture states; verify exact parent-filtered API requests and absence of unscoped disk fetches" ], - "command": "pulse-worker-browser frontend-modern/browser-tests/sso-mappings-2266.cjs and pulse-worker-browser frontend-modern/browser-tests/patrol-off-attention-2257.cjs (Playwright 1.56.1, Chromium 141.0.7390.37)", - "notes": "Both browser scripts ran on the combined candidate source with fixture API responses. Logs and six inspected screenshots are retained in this lane output directory. No deployed installation, provider acceptance, release or field retest is claimed." + "command": "pulse-worker-browser tmp/browser-proof/frontend-modern/browser-tests/guest-storage-2263.cjs (Playwright 1.56.1, Chromium 141.0.7390.37)", + "notes": "The browser ran an owned git-archive source copy with final GuestPhysicalDisks.tsx bytes and a bounded #2263 Vite fixture; this is not installed acceptance or a reporter field retest. Passed log and twelve inspected screenshots: /var/lib/pulse-maintainer/worker-outputs/product-intelligence-zf_afcvy/guest-storage-browser-formatted.log and browser-proof/." } diff --git a/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx b/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx index e487da5254..2862228f12 100644 --- a/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx +++ b/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx @@ -1,5 +1,4 @@ -import { For, Show, createSignal, type Component } from 'solid-js'; -import { DiskDetail } from '@/components/Storage/DiskDetail'; +import { For, Show, Suspense, createSignal, lazy, type Component } from 'solid-js'; import { InfoCardFrame } from '@/components/shared/InfoCardFrame'; import { useUnifiedResources } from '@/hooks/useUnifiedResources'; import { @@ -10,6 +9,12 @@ import type { Resource } from '@/types/resource'; import { formatBytes } from '@/utils/format'; import { formatTemperature } from '@/utils/temperature'; +// SMART history and chart controls are only needed after a disk row is opened. +// Keep their Storage detail module out of the initial Workloads surface. +const LazyDiskDetail = lazy(() => + import('@/components/Storage/DiskDetail').then(({ DiskDetail }) => ({ default: DiskDetail })), +); + const GuestPhysicalDiskRow: Component<{ disk: Resource }> = (props) => { const [expanded, setExpanded] = createSignal(false); const data = () => extractPhysicalDiskPresentationData(props.disk); @@ -39,7 +44,15 @@ const GuestPhysicalDiskRow: Component<{ disk: Resource }> = (props) => {
- + + Loading SMART details… +

+ } + > + +
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx index 45dfc75287..0fa8f2c11a 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx @@ -49,7 +49,7 @@ describe('GuestPhysicalDisks', () => { }); afterEach(cleanup); - it('shows the linked guest disk and opens its existing SMART detail', () => { + it('shows the linked guest disk and loads its existing SMART detail on expansion', async () => { queryState.resources = [disk, { ...disk, id: 'host-disk', parentId: 'pve-node-1' }]; render(() => ); expect(queryState.query).toBe('type=physical_disk&parent=vm-resource-101'); @@ -64,7 +64,7 @@ describe('GuestPhysicalDisks', () => { const row = within(card).getByTestId('guest-physical-disk') as HTMLDetailsElement; row.open = true; fireEvent(row, new Event('toggle')); - expect(within(card).getByText('Reallocated Sectors')).toBeInTheDocument(); + expect(await within(card).findByText('Reallocated Sectors')).toBeInTheDocument(); expect(within(card).getByText('Pending Sectors')).toBeInTheDocument(); expect(within(card).getByText('CRC Errors')).toBeInTheDocument(); expect(within(card).getAllByTestId('guest-physical-disk')).toHaveLength(1);