mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
Gate Proxmox tabs on observed capabilities
Avoid showing unsupported Replication, Ceph and Mail Gateway sections while Proxmox resource counts are unknown, while preserving bookmarked-route hydration and legitimate PBS navigation. Cover count transitions in component tests and synthetic production-page desktop/phone browser verification; update the affected subsystem contracts and receipt. Change-source: pulse-maintainer
This commit is contained in:
parent
c54d361acf
commit
3a061d66c4
10 changed files with 305 additions and 51 deletions
|
|
@ -403,6 +403,14 @@ and thermal facts remain discoverable from the PBS surface.
|
|||
`ProxmoxBackupServersTable` belongs to the Proxmox Backups tab. Proxmox
|
||||
Overview must not duplicate that domain table between its node and guest
|
||||
regions; the tab boundary owns PBS server, datastore, and artifact detail.
|
||||
The shared Proxmox section rail renders only tabs backed by current capability
|
||||
evidence. While resource counts are unknown it must not show every optional
|
||||
tab as a loading fallback; an independently fetched positive replication-job
|
||||
count may still expose Replication. A bookmarked section remains the active
|
||||
hydration target until counts can distinguish unavailable from unsupported,
|
||||
then unsupported sections fall back to Overview. This rule applies at desktop
|
||||
and phone widths and must not rewrite the URL or discard a valid PBS-only
|
||||
Backups tab when its count arrives.
|
||||
When that surface receives the provider-owned PBS resource and its host Agent
|
||||
as separate canonical resources, `ProxmoxBackupServersTable` may assemble a
|
||||
presentation-only drawer resource only after one unique normalized host
|
||||
|
|
|
|||
|
|
@ -795,6 +795,13 @@ state effects remain dormant and a forced Proxmox storage scope is not
|
|||
serialized as redundant URL state. Backups reuses the source-scoped Overview
|
||||
guest snapshot and adds only the PBS resource family, so it cannot repeat the
|
||||
large VM/LXC enumeration under a second request owner.
|
||||
Unknown resource aggregations must not advertise optional Proxmox workflows as
|
||||
if their providers are present. The navigation rail may expose Replication
|
||||
from its independent positive job count, while Storage, Backups, Ceph, and Mail
|
||||
wait for positive resource counts. A direct link keeps its requested route
|
||||
hydrating while counts are unknown; after counts arrive, an unsupported route
|
||||
renders Overview instead. Hiding a tab is navigation presentation, not a
|
||||
verdict about backup coverage, artifact recoverability, or stored history.
|
||||
PBS server/datastore rows may display backup counts, but the
|
||||
counts must come from the PBS backup API artifact identity, not from a
|
||||
datastore-capacity approximation. The table owns which PBS artifact count is
|
||||
|
|
|
|||
48
frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts
Normal file
48
frontend-modern/browser-tests/proxmox-tabs-2321-hook.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Browser-only resource-hook seam for #2321. The production page and shared
|
||||
// tab rail stay real; counts/resources are synthetic and switch on command.
|
||||
import { createSignal } from 'solid-js';
|
||||
import type { Resource } from '../src/types/resource';
|
||||
import type { UnifiedResourceAggregations } from '../src/hooks/useUnifiedResources';
|
||||
|
||||
export * from '../src/hooks/useUnifiedResources';
|
||||
|
||||
const pbs = {
|
||||
id: 'pbs-only', type: 'pbs', name: 'pbs-only', displayName: 'pbs-only',
|
||||
platformId: 'pbs-only', platformType: 'proxmox-pbs', sourceType: 'api',
|
||||
sources: ['pbs'], status: 'online', lastSeen: Date.now(),
|
||||
pbs: { instanceId: 'pbs-only', hostname: 'pbs-only', datastores: [] },
|
||||
} as Resource;
|
||||
const pmg = {
|
||||
id: 'pmg-one', type: 'pmg', name: 'pmg-one', displayName: 'pmg-one',
|
||||
platformId: 'pmg-one', platformType: 'proxmox-pmg', sourceType: 'api',
|
||||
sources: ['pmg'], status: 'online', lastSeen: Date.now(),
|
||||
} as Resource;
|
||||
|
||||
const [counts, setCounts] = createSignal<UnifiedResourceAggregations | null>(null);
|
||||
(window as unknown as { __proxmoxTabProof: { setCounts: typeof setCounts } }).__proxmoxTabProof = {
|
||||
setCounts,
|
||||
};
|
||||
|
||||
export function useUnifiedResources(options?: { cacheKey?: string }) {
|
||||
const resources = () => {
|
||||
switch (options?.cacheKey) {
|
||||
case 'proxmox-backups-shell':
|
||||
return [pbs];
|
||||
case 'proxmox-mail':
|
||||
return [pmg];
|
||||
default:
|
||||
return [] as Resource[];
|
||||
}
|
||||
};
|
||||
return {
|
||||
resources,
|
||||
aggregations: counts,
|
||||
facets: () => null,
|
||||
policyPosture: () => null,
|
||||
resourceSnapshotChange: () => ({ version: 0, changedIds: null }),
|
||||
loading: () => false,
|
||||
error: () => null,
|
||||
refetch: async () => resources(),
|
||||
mutate: () => resources(),
|
||||
};
|
||||
}
|
||||
1
frontend-modern/browser-tests/proxmox-tabs-2321-run.json
Normal file
1
frontend-modern/browser-tests/proxmox-tabs-2321-run.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"width":1365}
|
||||
119
frontend-modern/browser-tests/proxmox-tabs-2321.cjs
Normal file
119
frontend-modern/browser-tests/proxmox-tabs-2321.cjs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// #2321 browser proof: production Proxmox page and tab rail with a synthetic
|
||||
// reactive resource-hook seam. No live API, credentials or reporter data.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const root = '/workspace/frontend-modern';
|
||||
const artifacts = path.join(root, 'browser-tests');
|
||||
const width = JSON.parse(fs.readFileSync(path.join(artifacts, 'proxmox-tabs-2321-run.json'), 'utf8')).width;
|
||||
assert.ok(width === 1365 || width === 390);
|
||||
process.chdir(root);
|
||||
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
|
||||
const { default: solid } = await import(path.join(root, 'node_modules/vite-plugin-solid/dist/esm/index.mjs'));
|
||||
const server = await createServer({
|
||||
root,
|
||||
configFile: false,
|
||||
plugins: [solid()],
|
||||
resolve: { alias: [
|
||||
{ find: /^@\/hooks\/useUnifiedResources$/, replacement: path.join(artifacts, 'proxmox-tabs-2321-hook.ts') },
|
||||
{ find: '@', replacement: path.join(root, 'src') },
|
||||
] },
|
||||
optimizeDeps: { noDiscovery: true, include: [], esbuildOptions: { target: 'esnext' } },
|
||||
server: { host: '127.0.0.1', port: 5201, strictPort: true },
|
||||
});
|
||||
let browser;
|
||||
try {
|
||||
await server.listen();
|
||||
browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage({ viewport: { width, height: width === 390 ? 844 : 900 } });
|
||||
const pageErrors = [];
|
||||
const apiRequests = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
await page.routeWebSocket(/\/ws(?:\?|$)/, () => {});
|
||||
await page.route('**/*', (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.origin !== 'http://127.0.0.1:5201') return route.abort();
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
apiRequests.push(url.pathname);
|
||||
if (url.pathname === '/api/replication/jobs') return route.fulfill({ json: { data: [] } });
|
||||
return route.fulfill({ json: { data: [] } });
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
await page.goto('http://127.0.0.1:5201/browser-tests/proxmox-tabs-2321.html?tab=mail', {
|
||||
waitUntil: 'domcontentloaded', timeout: 120_000,
|
||||
});
|
||||
await page.getByTestId('proxmox-page').waitFor();
|
||||
const nav = page.getByRole('navigation', { name: 'Proxmox sections' });
|
||||
const labels = async () => (await nav.locator('a').allTextContents()).map((label) => label.trim());
|
||||
const setCounts = async (byType) => page.evaluate((value) => window.__proxmoxTabProof.setCounts({ total: 1, byType: value }), byType);
|
||||
const screenshot = async (state) => {
|
||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
const rail = await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="Proxmox sections"] a')].map((link) => ({
|
||||
label: link.textContent?.trim(), current: link.getAttribute('aria-current'),
|
||||
selectedClass: link.classList.contains('border-blue-500'),
|
||||
})));
|
||||
assert.ok(rail.every((link) => link.selectedClass === (link.current === 'page')), JSON.stringify({ state, rail }));
|
||||
await page.screenshot({ path: path.join(artifacts, `proxmox-tabs-2321-${state}-${width}.png`), fullPage: true });
|
||||
};
|
||||
|
||||
assert.equal(new URL(page.url()).pathname, '/proxmox/mail');
|
||||
assert.equal(await nav.count(), 0, 'unknown counts must not show ghost tabs');
|
||||
await screenshot('unknown-direct-mail');
|
||||
|
||||
await setCounts({ pbs: 1 });
|
||||
await nav.waitFor();
|
||||
assert.deepEqual(await labels(), ['Overview', 'Backups']);
|
||||
assert.equal(await nav.getByRole('link', { name: 'Overview' }).getAttribute('aria-current'), 'page');
|
||||
assert.equal(new URL(page.url()).pathname, '/proxmox/mail', 'fallback keeps the bookmarked URL');
|
||||
await screenshot('pbs-only-fallback');
|
||||
|
||||
const backups = nav.getByRole('link', { name: 'Backups' });
|
||||
if (width === 390) {
|
||||
await backups.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
} else {
|
||||
await backups.click();
|
||||
}
|
||||
try {
|
||||
await page.waitForURL('**/proxmox/backups/date', { timeout: 5_000 });
|
||||
} catch (error) {
|
||||
throw new Error(JSON.stringify({ href: await backups.getAttribute('href'), url: page.url(), body: await page.locator('body').innerText(), pageErrors }), { cause: error });
|
||||
}
|
||||
assert.equal(new URL(page.url()).pathname, '/proxmox/backups/date');
|
||||
assert.equal(await backups.getAttribute('aria-current'), 'page');
|
||||
await screenshot('pbs-only-backups');
|
||||
|
||||
await setCounts({ pbs: 1, storage: 1, ceph: 1, pmg: 1 });
|
||||
assert.deepEqual(await labels(), ['Overview', 'Storage', 'Backups', 'Ceph', 'Mail Gateway']);
|
||||
await nav.getByRole('link', { name: 'Mail Gateway' }).click();
|
||||
await page.waitForURL('**/proxmox/mail');
|
||||
assert.equal(new URL(page.url()).pathname, '/proxmox/mail');
|
||||
assert.equal(await nav.getByRole('link', { name: 'Mail Gateway' }).getAttribute('aria-current'), 'page');
|
||||
await screenshot('available-mail');
|
||||
|
||||
await setCounts({ pbs: 1 });
|
||||
assert.deepEqual(await labels(), ['Overview', 'Backups']);
|
||||
assert.equal(await nav.getByRole('link', { name: 'Overview' }).getAttribute('aria-current'), 'page');
|
||||
await screenshot('removed-mail');
|
||||
|
||||
await setCounts({});
|
||||
assert.equal(await nav.count(), 0);
|
||||
await screenshot('no-optional-services');
|
||||
|
||||
const dimensions = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, innerWidth: window.innerWidth }));
|
||||
assert.ok(dimensions.scrollWidth <= dimensions.innerWidth + 1, JSON.stringify(dimensions));
|
||||
assert.deepEqual(pageErrors, []);
|
||||
console.log(JSON.stringify({ result: 'passed', source: 'production ProxmoxPageSurface and PlatformSectionTabs; synthetic useUnifiedResources', width, browser: browser.version(), playwright: '1.56.1', apiRequests, pageErrors, dimensions }));
|
||||
await page.close();
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
12
frontend-modern/browser-tests/proxmox-tabs-2321.html
Normal file
12
frontend-modern/browser-tests/proxmox-tabs-2321.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Proxmox tabs browser fixture</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/browser-tests/proxmox-tabs-2321.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
frontend-modern/browser-tests/proxmox-tabs-2321.tsx
Normal file
25
frontend-modern/browser-tests/proxmox-tabs-2321.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Mount the production page and router; the browser runner replaces only the
|
||||
// resource and replication API responses for #2321's PBS-only estate.
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { render } from 'solid-js/web';
|
||||
import { ProxmoxPageSurface } from '../src/features/proxmox/ProxmoxPageSurface';
|
||||
import { DarkModeContext, WebSocketContext } from '../src/contexts/appRuntime';
|
||||
import { getGlobalWebSocketStore } from '../src/stores/websocket-global';
|
||||
import '../src/index.css';
|
||||
|
||||
const requested = new URLSearchParams(window.location.search).get('tab');
|
||||
const tab = requested === 'mail' || requested === 'backups' ? requested : 'overview';
|
||||
window.history.replaceState(null, '', `/proxmox/${tab}`);
|
||||
|
||||
render(
|
||||
() => (
|
||||
<WebSocketContext.Provider value={getGlobalWebSocketStore()}>
|
||||
<DarkModeContext.Provider value={() => false}>
|
||||
<Router>
|
||||
<Route path="/proxmox/*" component={ProxmoxPageSurface} />
|
||||
</Router>
|
||||
</DarkModeContext.Provider>
|
||||
</WebSocketContext.Provider>
|
||||
),
|
||||
document.getElementById('root')!,
|
||||
);
|
||||
|
|
@ -1,62 +1,47 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "8619ab2ec8f5d239d045dec1f1f719ff3c5d687e",
|
||||
"verified_at": "2026-09-29T06:55:46Z",
|
||||
"base_sha": "c54d361acf0d38d3f90767022513f718e05e1b69",
|
||||
"verified_at": "2026-09-29T11:23:17Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/api/resourceActions.ts",
|
||||
"frontend-modern/src/features/actions/ActionDecisionPacket.tsx",
|
||||
"frontend-modern/src/features/actions/ActionReviewDialog.tsx",
|
||||
"frontend-modern/src/features/actions/actionPresentation.ts",
|
||||
"frontend-modern/src/pages/Actions.tsx"
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/api/resourceActions.ts": "60e65faa74d6fffd43f97dfa6ab2eff946c666893b5201217bd3e886312ac47b",
|
||||
"frontend-modern/src/features/actions/ActionDecisionPacket.tsx": "2839a0be877b906bbc717bae7ef829aaffc2e46de8242413617d078087799cb1",
|
||||
"frontend-modern/src/features/actions/ActionReviewDialog.tsx": "ff35ee1912d45b3a0546bd31ed498ecf94f01a493c0a86b93252b0e5d7a529d0",
|
||||
"frontend-modern/src/features/actions/actionPresentation.ts": "723a40b5b41f8645c76f54e2c0ffac3ae60757b885ee0bee6b299dc97146ebc1",
|
||||
"frontend-modern/src/pages/Actions.tsx": "c43ae74844fb8d31b904fa273d4a35c0344084ef7b6e227e55d8924dfa7b248c"
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "37ce3942c563770c3bdd494a692ac9fe22cf367479b4f788136ad5d8faaaf625"
|
||||
},
|
||||
"routes": [
|
||||
"/action-recovery-proof.html (mock-backed shared /actions review component)"
|
||||
"/proxmox/mail (direct link, synthetic capability fixture)",
|
||||
"/proxmox/backups/date (tab navigation, synthetic capability fixture)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1365,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
{ "width": 1365, "height": 900 },
|
||||
{ "width": 390, "height": 844 }
|
||||
],
|
||||
"states": [
|
||||
"aged admin",
|
||||
"terminal inconclusive",
|
||||
"recent",
|
||||
"read-only",
|
||||
"settled",
|
||||
"non-admin",
|
||||
"stale re-read"
|
||||
"unknown resource counts on direct Mail Gateway link",
|
||||
"PBS-only resource counts with Mail Gateway unsupported",
|
||||
"PBS Backups tab active",
|
||||
"positive Storage, Ceph and Mail Gateway counts",
|
||||
"Mail Gateway tab active",
|
||||
"Mail Gateway count removed after navigation",
|
||||
"no optional service counts"
|
||||
],
|
||||
"interactions": [
|
||||
"open disclosure",
|
||||
"reason and acknowledgement gates",
|
||||
"force-fail post",
|
||||
"refresh outcome",
|
||||
"stale skip",
|
||||
"phone layout"
|
||||
"hydrate bookmarked Mail Gateway route before counts settle",
|
||||
"reveal PBS-only Backups while hiding unsupported Replication, Ceph and Mail Gateway tabs",
|
||||
"activate Backups by pointer on desktop and keyboard on phone",
|
||||
"activate Mail Gateway after positive counts",
|
||||
"fall back to Overview when Mail Gateway count disappears without rewriting the URL",
|
||||
"verify selected-tab semantics, desktop and phone rail layout, and no document overflow"
|
||||
],
|
||||
"command": "pulse-worker-browser web-1891-proof.cjs from assigned workspace root; offline Vite API preview and Chromium 141.0.7390.37 / Playwright 1.56.1",
|
||||
"command": "pulse-worker-browser frontend-modern/browser-tests/proxmox-tabs-2321.cjs from assigned root, with browser-tests/proxmox-tabs-2321-run.json set to 1365 then 390; Chromium 141.0.7390.37 / Playwright 1.56.1",
|
||||
"artifacts": [
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-result.json",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-desktop-before.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-desktop-after.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-mobile.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/browser-1891-mobile-dialog.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/web-1891-proof.cjs",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/action-recovery-proof.html",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-paaa_868/action-recovery-proof.tsx"
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-desktop.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-phone.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-unknown-direct-mail-1365.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-pbs-only-backups-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-available-mail-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-removed-mail-1365.png"
|
||||
],
|
||||
"notes": "Shared ActionReviewDialog exercised with mock-backed API/status at exact staged runtime content; no live backend, container outcome, installed release or reporter acceptance. Component tests cover Actions history state. Screenshots visually inspected after transition."
|
||||
"notes": "Production ProxmoxPageSurface and PlatformSectionTabs were mounted in a Vite/Chromium fixture. Only useUnifiedResources and API responses were synthetic; no installed server, agent, reporter topology or release was exercised. Both runs exited 0 with no page errors or horizontal overflow. Final screenshots were inspected for selected rail, table placement and narrow reachability."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,18 +116,22 @@ export function ProxmoxPageSurface() {
|
|||
replicationJobs.error ? 0 : (replicationJobs() ?? []).length,
|
||||
);
|
||||
const visibleTabs = createMemo(() => {
|
||||
const aggregations = resourceAggregations();
|
||||
if (aggregations) {
|
||||
return buildVisibleProxmoxTabSpecsFromCounts(aggregations.byType, replicationJobCount());
|
||||
}
|
||||
return PROXMOX_TAB_SPECS;
|
||||
// An unknown snapshot is not evidence that every optional integration is
|
||||
// present. Keep only sections backed by an independent signal until the
|
||||
// resource counts arrive (or if an older response omits them).
|
||||
return buildVisibleProxmoxTabSpecsFromCounts(
|
||||
resourceAggregations()?.byType ?? {},
|
||||
replicationJobCount(),
|
||||
);
|
||||
});
|
||||
const visibleTabIds = createMemo(
|
||||
() => new Set<ProxmoxPageTabId>(visibleTabs().map((tab) => tab.id)),
|
||||
);
|
||||
const activeTab = createMemo<ProxmoxPageTabId>(() => {
|
||||
const requested = requestedTab();
|
||||
return visibleTabIds().has(requested) ? requested : 'overview';
|
||||
// Do not discard a direct link while counts are still unknown: its own
|
||||
// resource query must be allowed to hydrate before deciding it is absent.
|
||||
return !resourceAggregations() || visibleTabIds().has(requested) ? requested : 'overview';
|
||||
});
|
||||
const shouldHydrateTab = (tab: ProxmoxPageTabId) => activeTab() === tab;
|
||||
const overviewResources = useUnifiedResources({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { UnifiedResourceAggregations } from '@/hooks/useUnifiedResources';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { ProxmoxPageSurface } from '../ProxmoxPageSurface';
|
||||
import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw';
|
||||
|
|
@ -577,6 +579,49 @@ describe('ProxmoxPageSurface contract', () => {
|
|||
expect(screen.queryByRole('list', { name: 'Proxmox Patrol coverage' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not advertise optional sections before resource counts arrive', () => {
|
||||
setResourcesSnapshot(undefined, true);
|
||||
|
||||
renderSurface();
|
||||
|
||||
expect(screen.getByTestId('platform-section-tabs')).toHaveAttribute('data-tabs', 'overview');
|
||||
expect(screen.getByTestId('platform-table-loading-state')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hydrates a direct link while counts are unknown, then gates it on actual capabilities', async () => {
|
||||
mockPathname.mockReturnValue('/proxmox/mail');
|
||||
const [aggregations, setAggregations] = createSignal<UnifiedResourceAggregations | null>(null);
|
||||
mockUseUnifiedResources.mockReturnValue({
|
||||
resources: () => [],
|
||||
aggregations,
|
||||
loading: () => false,
|
||||
error: () => null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
renderSurface();
|
||||
|
||||
const tabs = screen.getByTestId('platform-section-tabs');
|
||||
expect(tabs).toHaveAttribute('data-tabs', 'overview');
|
||||
expect(tabs).toHaveAttribute('data-active', 'mail');
|
||||
const options = mockUseUnifiedResources.mock.calls.map(
|
||||
([value]) => value as { cacheKey: string; enabled: () => boolean },
|
||||
);
|
||||
expect(options.find((option) => option.cacheKey === 'proxmox-mail')?.enabled()).toBe(true);
|
||||
|
||||
setAggregations({ total: 2, byType: { pbs: 1, pmg: 1 } });
|
||||
await waitFor(() => {
|
||||
expect(tabs).toHaveAttribute('data-tabs', 'overview,backups,mail');
|
||||
expect(tabs).toHaveAttribute('data-active', 'mail');
|
||||
});
|
||||
|
||||
setAggregations({ total: 1, byType: { pbs: 1 } });
|
||||
await waitFor(() => {
|
||||
expect(tabs).toHaveAttribute('data-tabs', 'overview,backups');
|
||||
expect(tabs).toHaveAttribute('data-active', 'overview');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not surface stale-agent notices for development builds without an agent target', () => {
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: '6.0.0-rc.6+git.172.g2c360f779.dirty',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue