mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Retire e2e specs that asserted the removed unified-route contracts
The standalone /infrastructure, /workloads, /storage, and /recovery routes
were deliberately retired with the platform-first navigation (abb6f86ae);
these specs existed to pin those routes' filter, handoff, and scoped-routing
contracts, so there is no platform-first surface for them to assert. The
subjects that do survive the rework are covered elsewhere: workload drawers
and filters by the platform-page and embedded-workloads specs, and refresh
resilience by the workloads stability spec being ported to the Proxmox page.
This commit is contained in:
parent
d88248363e
commit
005100432b
7 changed files with 0 additions and 1322 deletions
|
|
@ -1,146 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-workloads-platform-filter.png';
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-workloads-platform-filter-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS workloads platform filter', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps canonical platform scoping on the workloads route', async ({ page }) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (
|
||||
requestUrl.pathname !== '/api/resources' ||
|
||||
requestUrl.searchParams.get('type') !== 'vm,system-container,app-container,pod'
|
||||
) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'app-container:truenas-main:nextcloud',
|
||||
type: 'app-container',
|
||||
name: 'nextcloud',
|
||||
status: 'running',
|
||||
lastSeen: '2026-03-29T21:00:00Z',
|
||||
node: 'truenas-main',
|
||||
sources: ['truenas'],
|
||||
parentName: 'truenas-main',
|
||||
metrics: {
|
||||
cpu: { value: 12, percent: 12 },
|
||||
memory: { total: 8 * 1024 * 1024 * 1024, used: 2 * 1024 * 1024 * 1024 },
|
||||
disk: { total: 100 * 1024 * 1024 * 1024, used: 40 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 2048 },
|
||||
netOut: { value: 1024 },
|
||||
diskRead: { value: 512 },
|
||||
diskWrite: { value: 256 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'ix-nextcloud',
|
||||
hostname: 'truenas-main',
|
||||
imageName: 'nextcloud:29',
|
||||
runtime: 'docker',
|
||||
hostSourceId: 'truenas-main',
|
||||
updateStatus: {
|
||||
updateAvailable: true,
|
||||
currentDigest: 'sha256:1111111111111111111111111111111111111111111111111111111111111111',
|
||||
latestDigest: 'sha256:2222222222222222222222222222222222222222222222222222222222222222',
|
||||
lastChecked: 1743362400000,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-container:docker-main:grafana',
|
||||
type: 'app-container',
|
||||
name: 'grafana',
|
||||
status: 'running',
|
||||
lastSeen: '2026-03-29T21:00:00Z',
|
||||
node: 'docker-main',
|
||||
sources: ['docker'],
|
||||
parentName: 'docker-main',
|
||||
metrics: {
|
||||
cpu: { value: 8, percent: 8 },
|
||||
memory: { total: 4 * 1024 * 1024 * 1024, used: 1 * 1024 * 1024 * 1024 },
|
||||
disk: { total: 40 * 1024 * 1024 * 1024, used: 10 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 1024 },
|
||||
netOut: { value: 512 },
|
||||
diskRead: { value: 128 },
|
||||
diskWrite: { value: 64 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'docker-grafana',
|
||||
hostname: 'docker-main',
|
||||
imageName: 'grafana:11',
|
||||
runtime: 'docker',
|
||||
hostSourceId: 'docker-main',
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/workloads?type=app-container&platform=truenas&agent=truenas-main', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.waitForURL(/\/workloads\?type=app-container&platform=truenas&agent=.*truenas-main/);
|
||||
await expect(page.locator('#dashboard-type-filter')).toHaveValue('app-container');
|
||||
await expect(page.locator('#workloads-platform-filter')).toHaveValue('truenas');
|
||||
await expect(page.locator('#workloads-node-filter')).toHaveValue(/truenas-main/);
|
||||
|
||||
const workloadTable = page.locator('table').first();
|
||||
await expect(workloadTable).toContainText('nextcloud');
|
||||
await expect(workloadTable).not.toContainText('grafana');
|
||||
await expect(
|
||||
page.locator(
|
||||
'tr[data-guest-id="app-container:truenas-main:nextcloud"] [data-prevent-toggle]',
|
||||
),
|
||||
).toHaveCount(0);
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-storage-source-filter.png';
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-storage-source-filter-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS storage source filter', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('restores the canonical TrueNAS storage handoff on the storage route', async ({ page }) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.pathname !== '/api/resources') {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'truenas-main',
|
||||
type: 'agent',
|
||||
name: 'truenas-main',
|
||||
displayName: 'TrueNAS Main',
|
||||
platformId: 'truenas-main',
|
||||
platformType: 'truenas',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['agent', 'truenas'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'TrueNAS Main',
|
||||
hostname: 'truenas-main',
|
||||
platformId: 'truenas-main',
|
||||
},
|
||||
agent: {
|
||||
hostname: 'truenas-main',
|
||||
platform: 'TrueNAS SCALE',
|
||||
uptimeSeconds: 86400,
|
||||
},
|
||||
platformData: {
|
||||
sources: ['agent', 'truenas'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pve-main',
|
||||
type: 'agent',
|
||||
name: 'pve-main',
|
||||
displayName: 'PVE Main',
|
||||
platformId: 'pve-main',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['agent', 'proxmox-pve'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'PVE Main',
|
||||
hostname: 'pve-main',
|
||||
platformId: 'pve-main',
|
||||
},
|
||||
agent: {
|
||||
hostname: 'pve-main',
|
||||
platform: 'Debian',
|
||||
uptimeSeconds: 86400,
|
||||
},
|
||||
platformData: {
|
||||
sources: ['agent', 'proxmox-pve'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'storage-truenas-display',
|
||||
type: 'storage',
|
||||
name: 'tank',
|
||||
displayName: 'tank',
|
||||
parentId: 'truenas-main',
|
||||
parentName: 'truenas-main',
|
||||
platformId: 'truenas-1',
|
||||
platformType: 'truenas',
|
||||
sourceType: 'api',
|
||||
sources: ['truenas'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'tank',
|
||||
platformId: 'truenas-1',
|
||||
},
|
||||
metrics: {
|
||||
disk: {
|
||||
total: 2_000 * 1024 * 1024 * 1024,
|
||||
used: 840 * 1024 * 1024 * 1024,
|
||||
percent: 42,
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
platform: 'truenas',
|
||||
type: 'zfs-pool',
|
||||
topology: 'pool',
|
||||
isZfs: true,
|
||||
},
|
||||
platformData: {
|
||||
sources: ['truenas'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'storage-pve-local',
|
||||
type: 'storage',
|
||||
name: 'local-zfs',
|
||||
displayName: 'local-zfs',
|
||||
parentId: 'pve-main',
|
||||
parentName: 'pve-main',
|
||||
platformId: 'cluster-a',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'api',
|
||||
sources: ['proxmox-pve'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'local-zfs',
|
||||
platformId: 'cluster-a',
|
||||
},
|
||||
metrics: {
|
||||
disk: {
|
||||
total: 1_000 * 1024 * 1024 * 1024,
|
||||
used: 400 * 1024 * 1024 * 1024,
|
||||
percent: 40,
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
platform: 'proxmox-pve',
|
||||
type: 'zfspool',
|
||||
topology: 'pool',
|
||||
isZfs: true,
|
||||
},
|
||||
platformData: {
|
||||
node: 'pve-main',
|
||||
sources: ['proxmox-pve'],
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 4,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/storage?source=truenas&node=truenas-main', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/storage\?source=truenas&node=truenas-main/);
|
||||
await expect(page.locator('#storage-source-filter')).toHaveValue('truenas');
|
||||
await expect(page.getByLabel('Node')).toHaveValue('truenas-main');
|
||||
|
||||
const sourceOptions = await page.locator('#storage-source-filter option').evaluateAll((options) =>
|
||||
options.map((option) => ({
|
||||
value: option.getAttribute('value'),
|
||||
label: option.textContent?.trim(),
|
||||
})),
|
||||
);
|
||||
expect(sourceOptions).toEqual([
|
||||
{ value: 'all', label: 'All Sources' },
|
||||
{ value: 'proxmox-pve', label: 'PVE' },
|
||||
{ value: 'truenas', label: 'TrueNAS' },
|
||||
]);
|
||||
|
||||
const storageTable = page.locator('table').first();
|
||||
await expect(storageTable).toContainText('tank');
|
||||
await expect(storageTable).not.toContainText('local-zfs');
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test as base } from '@playwright/test';
|
||||
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-recovery-route-filter.png';
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-recovery-route-filter-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS recovery route filters', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps route-owned platform and node filters visible while recovery data warms', async ({
|
||||
page,
|
||||
}) => {
|
||||
const recoveryRequests: string[] = [];
|
||||
let releaseRecoveryResponses: (() => void) | null = null;
|
||||
const recoveryResponseGate = new Promise<void>((resolve) => {
|
||||
releaseRecoveryResponses = resolve;
|
||||
});
|
||||
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'truenas-main',
|
||||
type: 'agent',
|
||||
name: 'tower',
|
||||
displayName: 'TrueNAS Tower',
|
||||
platformId: 'truenas-main',
|
||||
platformType: 'truenas',
|
||||
sourceType: 'api',
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
platformData: {
|
||||
sources: ['truenas'],
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 200, total: 1, totalPages: 1 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/recovery/rollups*', async (route) => {
|
||||
recoveryRequests.push(route.request().url());
|
||||
await recoveryResponseGate;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
rollupId: 'ext:truenas-1',
|
||||
itemRef: { type: 'truenas-dataset', name: 'tank/apps', id: 'tank/apps' },
|
||||
display: { itemType: 'dataset', subjectLabel: 'tank/apps', nodeHostLabel: 'tower' },
|
||||
lastAttemptAt: '2026-03-29T09:00:00.000Z',
|
||||
lastSuccessAt: '2026-03-29T09:00:00.000Z',
|
||||
lastOutcome: 'success',
|
||||
platforms: ['truenas'],
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 500, total: 1, totalPages: 1 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/recovery/points*', async (route) => {
|
||||
recoveryRequests.push(route.request().url());
|
||||
await recoveryResponseGate;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'truenas-point-1',
|
||||
platform: 'truenas',
|
||||
kind: 'snapshot',
|
||||
mode: 'snapshot',
|
||||
outcome: 'success',
|
||||
completedAt: '2026-03-29T09:00:00.000Z',
|
||||
node: 'tower',
|
||||
itemRef: {
|
||||
type: 'truenas-dataset',
|
||||
name: 'tank/apps',
|
||||
id: 'tank/apps',
|
||||
},
|
||||
display: {
|
||||
itemType: 'dataset',
|
||||
subjectType: 'truenas-dataset',
|
||||
subjectLabel: 'tank/apps',
|
||||
nodeHostLabel: 'tower',
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 200, total: 1, totalPages: 1 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/recovery/facets*', async (route) => {
|
||||
recoveryRequests.push(route.request().url());
|
||||
await recoveryResponseGate;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
clusters: [],
|
||||
nodesAgents: ['tower'],
|
||||
namespaces: [],
|
||||
itemTypes: ['dataset'],
|
||||
hasSize: false,
|
||||
hasVerification: false,
|
||||
hasEntityId: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/recovery/series*', async (route) => {
|
||||
recoveryRequests.push(route.request().url());
|
||||
await recoveryResponseGate;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [{ day: '2026-03-29', total: 1, snapshot: 1, local: 0, remote: 0 }],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/recovery?view=events&platform=truenas&node=tower', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/recovery\?view=events&platform=truenas&node=tower/);
|
||||
await expect.poll(() => recoveryRequests.length).toBeGreaterThan(0);
|
||||
|
||||
releaseRecoveryResponses?.();
|
||||
|
||||
await expect(page.getByTestId('recovery-page')).toBeVisible();
|
||||
await expect(page.getByLabel('Platform')).toHaveValue('truenas');
|
||||
await expect(page.getByText('Host / Agent')).toBeVisible();
|
||||
await expect(page.getByText('tower')).toBeVisible();
|
||||
await expect(page.getByText('tank/apps')).toBeVisible();
|
||||
|
||||
expect(
|
||||
recoveryRequests.some(
|
||||
(url) => url.includes('/api/recovery/rollups') && url.includes('platform=truenas'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
recoveryRequests.some(
|
||||
(url) =>
|
||||
url.includes('/api/recovery/points') &&
|
||||
url.includes('platform=truenas') &&
|
||||
url.includes('node=tower'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
recoveryRequests.some(
|
||||
(url) =>
|
||||
url.includes('/api/recovery/facets') &&
|
||||
url.includes('platform=truenas') &&
|
||||
url.includes('node=tower'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-workloads-resource-deeplink.png';
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-workloads-resource-deeplink-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS workloads resource deep links', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('opens the canonical workload drawer without inventing a node scope', async ({ page }) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (
|
||||
requestUrl.pathname !== '/api/resources' ||
|
||||
requestUrl.searchParams.get('type') !== 'vm,system-container,app-container,pod'
|
||||
) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'app-container:truenas-main:nextcloud',
|
||||
type: 'app-container',
|
||||
name: 'nextcloud',
|
||||
status: 'running',
|
||||
lastSeen: '2026-03-29T21:00:00Z',
|
||||
node: 'truenas-main',
|
||||
instance: 'truenas-main',
|
||||
sources: ['truenas'],
|
||||
parentName: 'truenas-main',
|
||||
metrics: {
|
||||
cpu: { value: 12, percent: 12 },
|
||||
memory: { total: 8 * 1024 * 1024 * 1024, used: 2 * 1024 * 1024 * 1024 },
|
||||
disk: { total: 100 * 1024 * 1024 * 1024, used: 40 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 2048 },
|
||||
netOut: { value: 1024 },
|
||||
diskRead: { value: 512 },
|
||||
diskWrite: { value: 256 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'ix-nextcloud',
|
||||
hostname: 'truenas-main',
|
||||
imageName: 'nextcloud:29',
|
||||
runtime: 'docker',
|
||||
hostSourceId: 'truenas-main',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cluster-a:pve1:101',
|
||||
type: 'vm',
|
||||
name: 'vm-101',
|
||||
status: 'running',
|
||||
lastSeen: '2026-03-29T21:00:00Z',
|
||||
node: 'pve1',
|
||||
instance: 'cluster-a',
|
||||
vmid: 101,
|
||||
sources: ['proxmox'],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
'/workloads?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud',
|
||||
{
|
||||
waitUntil: 'domcontentloaded',
|
||||
},
|
||||
);
|
||||
|
||||
await page.waitForURL(
|
||||
/\/workloads\?type=app-container&platform=truenas&agent=truenas-main&resource=app-container%3Atruenas-main%3Anextcloud/,
|
||||
);
|
||||
await expect(page.locator('#dashboard-type-filter')).toHaveValue('app-container');
|
||||
await expect(page.locator('#workloads-platform-filter')).toHaveValue('truenas');
|
||||
await expect(page.locator('#workloads-node-filter')).toHaveValue(/truenas-main/);
|
||||
await expect(
|
||||
page.locator('tr[data-guest-id="app-container:truenas-main:nextcloud"]'),
|
||||
).toContainText('nextcloud');
|
||||
await expect(page.getByText('Open related infrastructure', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Overview' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Discovery' })).toHaveCount(0);
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test as base } from '@playwright/test';
|
||||
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-infrastructure-route-filter.png';
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-infrastructure-route-filter-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS infrastructure route filter', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps a route-owned TrueNAS source selection visible when current infrastructure data has no TrueNAS resources', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.pathname !== '/api/resources') {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'pve-main',
|
||||
type: 'agent',
|
||||
name: 'pve-main',
|
||||
displayName: 'PVE Main',
|
||||
platformId: 'pve-main',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['agent', 'proxmox-pve'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'PVE Main',
|
||||
hostname: 'pve-main',
|
||||
platformId: 'pve-main',
|
||||
},
|
||||
agent: {
|
||||
hostname: 'pve-main',
|
||||
platform: 'Debian',
|
||||
uptimeSeconds: 86400,
|
||||
},
|
||||
platformData: {
|
||||
sources: ['agent', 'proxmox-pve'],
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/infrastructure?source=truenas', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/infrastructure\?source=truenas/);
|
||||
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
|
||||
await expect(page.locator('#infra-source-filter')).toHaveValue('truenas');
|
||||
|
||||
const sourceOptions = await page.locator('#infra-source-filter option').evaluateAll((options) =>
|
||||
options.map((option) => ({
|
||||
value: option.getAttribute('value'),
|
||||
label: option.textContent?.trim(),
|
||||
})),
|
||||
);
|
||||
expect(sourceOptions).toEqual([
|
||||
{ value: '', label: 'All' },
|
||||
{ value: 'proxmox-pve', label: 'PVE' },
|
||||
{ value: 'agent', label: 'Agent' },
|
||||
{ value: 'truenas', label: 'TrueNAS' },
|
||||
]);
|
||||
|
||||
await expect(page.getByText('No resources match filters')).toBeVisible();
|
||||
await expect(page.getByText('Try adjusting the search, source, or status filters.')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
import { expect, test as base, type Page } from '@playwright/test';
|
||||
|
||||
import { getMockMode, setMockMode } from './helpers';
|
||||
|
||||
type InfrastructureHeaderMetric = {
|
||||
text: string;
|
||||
width: number;
|
||||
attrWidth: number | null;
|
||||
};
|
||||
|
||||
type InfrastructureTableMetric = {
|
||||
kind: 'host' | 'pbs' | 'pmg' | 'unknown';
|
||||
wrapperClientWidth: number;
|
||||
wrapperScrollWidth: number;
|
||||
tableScrollWidth: number;
|
||||
headers: InfrastructureHeaderMetric[];
|
||||
};
|
||||
|
||||
type InfrastructureColumnLayout = {
|
||||
tables: InfrastructureTableMetric[];
|
||||
};
|
||||
|
||||
let mockModeWasEnabled: boolean | null = null;
|
||||
|
||||
const HTTP_CREDENTIALS = {
|
||||
username: 'admin',
|
||||
password: 'adminadminadmin',
|
||||
};
|
||||
|
||||
const test = base;
|
||||
|
||||
test.use({
|
||||
httpCredentials: HTTP_CREDENTIALS,
|
||||
});
|
||||
|
||||
async function ensureMockModeEnabled(page: Page): Promise<void> {
|
||||
const state = await getMockMode(page);
|
||||
if (mockModeWasEnabled === null) {
|
||||
mockModeWasEnabled = state.enabled;
|
||||
}
|
||||
if (!state.enabled) {
|
||||
await setMockMode(page, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function readInfrastructureColumnLayout(page: Page): Promise<InfrastructureColumnLayout> {
|
||||
return page.evaluate(() => {
|
||||
const tables = Array.from(
|
||||
document.querySelectorAll<HTMLTableElement>('[data-testid="infrastructure-table-surface"] table'),
|
||||
);
|
||||
|
||||
const classifyTable = (headers: string[]): InfrastructureTableMetric['kind'] => {
|
||||
if (headers.includes('CPU') && headers.includes('Net I/O')) return 'host';
|
||||
if (headers.includes('Datastores') && headers.includes('Jobs')) return 'pbs';
|
||||
if (headers.includes('Queue') && headers.includes('Deferred')) return 'pmg';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
return {
|
||||
tables: tables.map((table) => {
|
||||
const wrapper = table.closest<HTMLDivElement>('div.overflow-x-auto');
|
||||
const headers = Array.from(table.querySelectorAll<HTMLTableCellElement>('thead th')).map(
|
||||
(header) => {
|
||||
const rawAttrWidth = header.getAttribute('width');
|
||||
return {
|
||||
text: header.textContent?.replace(/\s+/g, ' ').trim() ?? '',
|
||||
width: Math.round(header.getBoundingClientRect().width),
|
||||
attrWidth:
|
||||
rawAttrWidth === null || Number.isNaN(Number(rawAttrWidth))
|
||||
? null
|
||||
: Number(rawAttrWidth),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
kind: classifyTable(headers.map((header) => header.text)),
|
||||
wrapperClientWidth: wrapper?.clientWidth ?? 0,
|
||||
wrapperScrollWidth: wrapper?.scrollWidth ?? 0,
|
||||
tableScrollWidth: table.scrollWidth,
|
||||
headers,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test.describe.serial('Infrastructure column layout', () => {
|
||||
test.afterAll(async ({ browser }) => {
|
||||
if (mockModeWasEnabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await browser.newContext({ httpCredentials: HTTP_CREDENTIALS });
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
const current = await getMockMode(page);
|
||||
if (current.enabled !== mockModeWasEnabled) {
|
||||
await setMockMode(page, mockModeWasEnabled);
|
||||
}
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('distributes full-width desktop space across infrastructure columns', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof');
|
||||
|
||||
await page.setViewportSize({ width: 1920, height: 1200 });
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('fullWidthMode', 'full-width');
|
||||
});
|
||||
|
||||
await ensureMockModeEnabled(page);
|
||||
await page.goto('/infrastructure', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.pulse-shell--full-width').first()).toBeVisible();
|
||||
await expect(page.getByTestId('infrastructure-table-surface')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="infrastructure-table-surface"] [data-summary-series-id]').first()).toBeVisible();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const layout = await readInfrastructureColumnLayout(page);
|
||||
const hostTable = layout.tables.find((table) => table.kind === 'host');
|
||||
|
||||
expect(hostTable, 'Expected the infrastructure host table to be visible in mock mode').toBeDefined();
|
||||
expect(layout.tables.length, 'Expected visible infrastructure tables to validate').toBeGreaterThan(0);
|
||||
|
||||
for (const table of layout.tables) {
|
||||
const resourceHeader = table.headers[0];
|
||||
const fixedPeerHeaders = table.headers.slice(1).filter((header) => header.attrWidth !== null);
|
||||
const widestPeerWidth = Math.max(...fixedPeerHeaders.map((header) => header.width));
|
||||
|
||||
expect(
|
||||
table.tableScrollWidth,
|
||||
`${table.kind} table should fit the full-width desktop shell without horizontal scrolling`,
|
||||
).toBeLessThanOrEqual(table.wrapperClientWidth + 1);
|
||||
|
||||
expect(
|
||||
table.wrapperScrollWidth,
|
||||
`${table.kind} wrapper should not need extra horizontal scroll room in full-width mode`,
|
||||
).toBeLessThanOrEqual(table.wrapperClientWidth + 1);
|
||||
|
||||
expect(
|
||||
fixedPeerHeaders.length,
|
||||
`Expected ${table.kind} table to expose fixed-width peer columns for validation`,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
expect(
|
||||
resourceHeader.width / widestPeerWidth,
|
||||
`${table.kind} resource column should not monopolize the full-width layout`,
|
||||
).toBeLessThanOrEqual(1.8);
|
||||
|
||||
const underExpandedPeers = fixedPeerHeaders.filter(
|
||||
(header) => header.width <= (header.attrWidth ?? 0) + 5,
|
||||
);
|
||||
|
||||
expect(
|
||||
underExpandedPeers,
|
||||
`${table.kind} table should distribute extra full-width space across peer columns: ${JSON.stringify(table.headers)}`,
|
||||
).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
import { expect, test as base, type Page } from "@playwright/test";
|
||||
|
||||
import {
|
||||
apiRequest,
|
||||
ensureAuthenticated,
|
||||
getMockMode,
|
||||
setMockMode,
|
||||
} from "./helpers";
|
||||
|
||||
type APIResource = {
|
||||
id: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
sources?: string[];
|
||||
parentId?: string;
|
||||
parentName?: string;
|
||||
proxmox?: {
|
||||
nodeName?: string;
|
||||
instance?: string;
|
||||
vmid?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const test = base;
|
||||
|
||||
let mockModeWasEnabled: boolean | null = null;
|
||||
|
||||
const isProxmoxResource = (resource: APIResource): boolean =>
|
||||
Boolean(resource.proxmox) ||
|
||||
(resource.sources ?? []).some((source) => source === "proxmox");
|
||||
|
||||
const isProxmoxWorkload = (resource: APIResource): boolean =>
|
||||
(resource.type === "vm" || resource.type === "system-container") &&
|
||||
isProxmoxResource(resource);
|
||||
|
||||
const cssAttrValue = (value: string): string =>
|
||||
value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
|
||||
async function ensureMockModeEnabled(page: Page): Promise<void> {
|
||||
const state = await getMockMode(page);
|
||||
if (mockModeWasEnabled === null) {
|
||||
mockModeWasEnabled = state.enabled;
|
||||
}
|
||||
if (!state.enabled) {
|
||||
await setMockMode(page, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function readResources(page: Page): Promise<APIResource[]> {
|
||||
const response = await apiRequest(
|
||||
page,
|
||||
"/api/resources?type=agent,vm,system-container&page=1&limit=500",
|
||||
);
|
||||
expect(
|
||||
response.ok(),
|
||||
`Expected /api/resources to succeed, got ${response.status()}`,
|
||||
).toBe(true);
|
||||
const payload = (await response.json()) as { data?: APIResource[] };
|
||||
return payload.data ?? [];
|
||||
}
|
||||
|
||||
function buildProxmoxWorkloadRouteId(resource: APIResource): string {
|
||||
const instance = (resource.proxmox?.instance ?? "").trim();
|
||||
const node = (resource.proxmox?.nodeName ?? "").trim();
|
||||
const vmid = resource.proxmox?.vmid ?? 0;
|
||||
expect(
|
||||
instance,
|
||||
`Expected Proxmox workload ${resource.id} to expose an instance`,
|
||||
).not.toBe("");
|
||||
expect(
|
||||
node,
|
||||
`Expected Proxmox workload ${resource.id} to expose a node name`,
|
||||
).not.toBe("");
|
||||
expect(
|
||||
vmid,
|
||||
`Expected Proxmox workload ${resource.id} to expose a VMID`,
|
||||
).toBeGreaterThan(0);
|
||||
return `${instance}:${node}:${vmid}`;
|
||||
}
|
||||
|
||||
function buildProxmoxNodeFilterValue(resource: APIResource): string {
|
||||
const instance = (resource.proxmox?.instance ?? "").trim();
|
||||
const node = (resource.proxmox?.nodeName ?? "").trim();
|
||||
expect(
|
||||
instance,
|
||||
`Expected Proxmox workload ${resource.id} to expose an instance`,
|
||||
).not.toBe("");
|
||||
expect(
|
||||
node,
|
||||
`Expected Proxmox workload ${resource.id} to expose a node name`,
|
||||
).not.toBe("");
|
||||
return `${instance}-${node}`;
|
||||
}
|
||||
|
||||
function isWorkloadsResourcesRequest(requestUrl: URL): boolean {
|
||||
if (requestUrl.pathname !== "/api/resources") return false;
|
||||
const typeParam = requestUrl.searchParams.get("type") ?? "";
|
||||
return (
|
||||
typeParam.includes("vm") &&
|
||||
typeParam.includes("system-container") &&
|
||||
typeParam.includes("app-container")
|
||||
);
|
||||
}
|
||||
|
||||
async function assertSurfaceDoesNotBlank(
|
||||
page: Page,
|
||||
testId: string,
|
||||
durationMs: number,
|
||||
): Promise<void> {
|
||||
const result = await page.evaluate(
|
||||
async ({ durationMs: sampleDurationMs, testId: targetTestId }) => {
|
||||
const failures: Array<{
|
||||
elapsedMs: number;
|
||||
bodyText: string;
|
||||
rootChildren: number;
|
||||
}> = [];
|
||||
const startedAt = performance.now();
|
||||
let samples = 0;
|
||||
|
||||
while (performance.now() - startedAt < sampleDurationMs) {
|
||||
const root = document.getElementById("root");
|
||||
const target = document.querySelector<HTMLElement>(
|
||||
`[data-testid="${targetTestId}"]`,
|
||||
);
|
||||
const rect = target?.getBoundingClientRect();
|
||||
const targetVisible = Boolean(
|
||||
rect && rect.width > 0 && rect.height > 0,
|
||||
);
|
||||
const rootChildren = root?.childElementCount ?? 0;
|
||||
|
||||
if (!root || rootChildren === 0 || !target || !targetVisible) {
|
||||
failures.push({
|
||||
elapsedMs: Math.round(performance.now() - startedAt),
|
||||
bodyText: (document.body?.innerText ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 120),
|
||||
rootChildren,
|
||||
});
|
||||
}
|
||||
|
||||
samples += 1;
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 250));
|
||||
}
|
||||
|
||||
return { failures, samples };
|
||||
},
|
||||
{ durationMs, testId },
|
||||
);
|
||||
|
||||
expect(
|
||||
result.samples,
|
||||
"Expected browser stability sampling to run",
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
result.failures,
|
||||
`Expected ${testId} to stay mounted and visible without a blank route flash`,
|
||||
).toEqual([]);
|
||||
}
|
||||
|
||||
test.describe.serial("Infrastructure and workloads resource coherence", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
if (mockModeWasEnabled === null) return;
|
||||
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
const current = await getMockMode(page);
|
||||
if (current.enabled !== mockModeWasEnabled) {
|
||||
await setMockMode(page, mockModeWasEnabled);
|
||||
}
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps a Proxmox workload visible from its Infrastructure parent through scoped Workloads routing", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("mobile-"),
|
||||
"Desktop runtime proof",
|
||||
);
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
await ensureMockModeEnabled(page);
|
||||
|
||||
const resources = await readResources(page);
|
||||
const agents = resources.filter((resource) => resource.type === "agent");
|
||||
const workload = resources.find(
|
||||
(resource) =>
|
||||
isProxmoxWorkload(resource) &&
|
||||
Boolean(resource.parentId) &&
|
||||
Boolean(resource.proxmox?.nodeName) &&
|
||||
Boolean(resource.proxmox?.instance) &&
|
||||
Boolean(resource.proxmox?.vmid),
|
||||
);
|
||||
|
||||
expect(
|
||||
workload,
|
||||
"Expected mock/runtime data to include a parented Proxmox workload",
|
||||
).toBeDefined();
|
||||
const selectedWorkload = workload as APIResource;
|
||||
const parent = agents.find(
|
||||
(resource) => resource.id === selectedWorkload.parentId,
|
||||
);
|
||||
|
||||
expect(
|
||||
parent,
|
||||
`Expected Proxmox workload ${selectedWorkload.id} to resolve to one Infrastructure parent`,
|
||||
).toBeDefined();
|
||||
|
||||
const selectedParent = parent as APIResource;
|
||||
const duplicateParentIds = agents.filter(
|
||||
(resource) => resource.id === selectedParent.id,
|
||||
);
|
||||
expect(
|
||||
duplicateParentIds,
|
||||
`Expected parent ${selectedParent.id} to appear once in the resource API`,
|
||||
).toHaveLength(1);
|
||||
|
||||
const parentName = (
|
||||
selectedWorkload.parentName ||
|
||||
selectedParent.name ||
|
||||
selectedParent.id
|
||||
).trim();
|
||||
const nodeName = (selectedWorkload.proxmox?.nodeName ?? "").trim();
|
||||
const workloadRouteId = buildProxmoxWorkloadRouteId(selectedWorkload);
|
||||
const nodeFilterValue = buildProxmoxNodeFilterValue(selectedWorkload);
|
||||
|
||||
await page.goto(
|
||||
`/infrastructure?source=proxmox-pve&q=${encodeURIComponent(nodeName)}`,
|
||||
{
|
||||
waitUntil: "domcontentloaded",
|
||||
},
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("infrastructure-page")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("infrastructure-table-surface"),
|
||||
).toContainText(parentName);
|
||||
|
||||
const parentRows = page.locator(
|
||||
`[data-testid="infrastructure-table-surface"] [data-summary-series-id="${cssAttrValue(selectedParent.id)}"]`,
|
||||
);
|
||||
await expect(
|
||||
parentRows,
|
||||
`Expected Infrastructure to render parent ${parentName} exactly once`,
|
||||
).toHaveCount(1);
|
||||
|
||||
await assertSurfaceDoesNotBlank(
|
||||
page,
|
||||
"infrastructure-table-surface",
|
||||
3_000,
|
||||
);
|
||||
await expect(
|
||||
parentRows,
|
||||
`Expected Infrastructure parent ${parentName} to remain visible`,
|
||||
).toHaveCount(1);
|
||||
|
||||
await page.goto(
|
||||
`/workloads?type=${encodeURIComponent(selectedWorkload.type ?? "")}` +
|
||||
`&platform=proxmox-pve&agent=${encodeURIComponent(nodeFilterValue)}` +
|
||||
`&resource=${encodeURIComponent(workloadRouteId)}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("workloads-table-surface")).toBeVisible();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("platform"))
|
||||
.toBe("proxmox-pve");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("agent"))
|
||||
.toBe(nodeFilterValue);
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Platform:\s*PVE/ }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: new RegExp(`Node:\\s*${nodeName}`) }),
|
||||
).toBeVisible();
|
||||
|
||||
const workloadRows = page.locator(
|
||||
`tr[data-guest-id="${cssAttrValue(workloadRouteId)}"]`,
|
||||
);
|
||||
await expect(
|
||||
workloadRows,
|
||||
`Expected Workloads to render ${selectedWorkload.name || workloadRouteId} from parent ${parentName}`,
|
||||
).toHaveCount(1);
|
||||
|
||||
await assertSurfaceDoesNotBlank(page, "workloads-table-surface", 5_000);
|
||||
await expect(workloadRows).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("keeps Workloads mounted with the last good inventory when resource refresh fails", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("mobile-"),
|
||||
"Desktop runtime proof",
|
||||
);
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
await ensureMockModeEnabled(page);
|
||||
|
||||
let workloadResourceRequests = 0;
|
||||
await page.route("**/api/resources**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (!isWorkloadsResourcesRequest(requestUrl)) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
workloadResourceRequests += 1;
|
||||
if (workloadResourceRequests === 1) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
error: "simulated transient workload inventory gap",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/workloads", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("workloads-table-surface")).toBeVisible();
|
||||
|
||||
const firstWorkloadRow = page.locator("tr[data-guest-id]").first();
|
||||
await expect(firstWorkloadRow).toBeVisible();
|
||||
const firstGuestId = await firstWorkloadRow.getAttribute("data-guest-id");
|
||||
expect(firstGuestId, "Expected first workload row to expose a stable guest id").toBeTruthy();
|
||||
|
||||
await expect
|
||||
.poll(() => workloadResourceRequests, {
|
||||
message: "Expected the Workloads resource poll to retry after the first render",
|
||||
timeout: 8_000,
|
||||
})
|
||||
.toBeGreaterThanOrEqual(2);
|
||||
|
||||
await assertSurfaceDoesNotBlank(page, "workloads-table-surface", 2_000);
|
||||
await expect(
|
||||
page.locator(`tr[data-guest-id="${cssAttrValue(firstGuestId ?? "")}"]`),
|
||||
).toHaveCount(1);
|
||||
await expect(page.getByText("Loading view...")).toHaveCount(0);
|
||||
await expect(page.getByRole("heading", { name: "Welcome to Pulse" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue