mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Repair stable Core E2E contracts
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect, type Locator, type Page } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
test as base,
|
||||
expect,
|
||||
type Locator,
|
||||
type Page,
|
||||
} from "@playwright/test";
|
||||
import { createAuthenticatedStorageState } from "./helpers";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -14,23 +19,26 @@ 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',
|
||||
`telemetry-disclosure-${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' }],
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"playwright-auth",
|
||||
`telemetry-disclosure-${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" },
|
||||
],
|
||||
});
|
||||
|
||||
async function expectPopupDoc(
|
||||
@@ -39,70 +47,83 @@ async function expectPopupDoc(
|
||||
pathname: string,
|
||||
expectedText: string,
|
||||
) {
|
||||
const [popup] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
link.click(),
|
||||
]);
|
||||
const [popup] = await Promise.all([page.waitForEvent("popup"), link.click()]);
|
||||
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
await popup.waitForLoadState("domcontentloaded");
|
||||
expect(new URL(popup.url()).pathname).toBe(pathname);
|
||||
await expect(popup.locator('body')).toContainText(expectedText);
|
||||
await expect(popup.locator("body")).toContainText(expectedText);
|
||||
await popup.close();
|
||||
}
|
||||
|
||||
async function readTelemetryPreview(page: Page) {
|
||||
const preview = page.locator('pre[aria-label="Telemetry payload preview"]');
|
||||
await expect(preview).toBeVisible();
|
||||
return JSON.parse(await preview.textContent() ?? '{}') as {
|
||||
return JSON.parse((await preview.textContent()) ?? "{}") as {
|
||||
install_id: string;
|
||||
event: string;
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('Telemetry disclosure', () => {
|
||||
test.describe("Telemetry disclosure", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('general settings opens the shipped privacy document', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only telemetry disclosure coverage');
|
||||
test("general settings opens the shipped privacy document", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("mobile-"),
|
||||
"Desktop-only telemetry disclosure coverage",
|
||||
);
|
||||
|
||||
await page.goto('/settings/system-general', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto("/settings/system-general", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForURL(/\/settings/, { timeout: 15_000 });
|
||||
|
||||
const telemetrySummary = page.getByText(
|
||||
/rotating pseudonymous install ID, normalized release identity, runtime platform/i,
|
||||
);
|
||||
await expect(telemetrySummary).toBeVisible();
|
||||
await expect(telemetrySummary).toContainText('Telemetry rows are retained for up to 90 days');
|
||||
await expect(telemetrySummary).toContainText(
|
||||
'are not stored in telemetry rows',
|
||||
"Telemetry rows are retained for up to 90 days",
|
||||
);
|
||||
await expect(telemetrySummary).toContainText(
|
||||
"are not stored in telemetry rows",
|
||||
);
|
||||
|
||||
const disclosureLink = page.getByRole('link', { name: 'Full details' }).first();
|
||||
await expect(disclosureLink).toHaveAttribute('href', '/docs/PRIVACY.md');
|
||||
const disclosureLink = page
|
||||
.getByRole("link", { name: "Full details" })
|
||||
.first();
|
||||
await expect(disclosureLink).toHaveAttribute("href", "/docs/PRIVACY");
|
||||
await expectPopupDoc(
|
||||
page,
|
||||
disclosureLink,
|
||||
'/docs/PRIVACY.md',
|
||||
'Pulse has one outbound usage-data scope',
|
||||
"/docs/PRIVACY",
|
||||
"Pulse has one outbound usage-data scope",
|
||||
);
|
||||
});
|
||||
|
||||
test('general settings lets operators preview and rotate the telemetry payload', async ({
|
||||
test("general settings lets operators preview and rotate the telemetry payload", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only telemetry disclosure coverage');
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("mobile-"),
|
||||
"Desktop-only telemetry disclosure coverage",
|
||||
);
|
||||
|
||||
await page.goto('/settings/system-general', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto("/settings/system-general", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForURL(/\/settings/, { timeout: 15_000 });
|
||||
|
||||
await page.getByRole('button', { name: 'Preview payload' }).click();
|
||||
await page.getByRole("button", { name: "Preview payload" }).click();
|
||||
|
||||
const initialPreview = await readTelemetryPreview(page);
|
||||
expect(initialPreview.event).toBe('heartbeat');
|
||||
expect(initialPreview.event).toBe("heartbeat");
|
||||
expect(initialPreview.install_id).toBeTruthy();
|
||||
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await page.getByRole('button', { name: 'Reset ID' }).click();
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await page.getByRole("button", { name: "Reset ID" }).click();
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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';
|
||||
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));
|
||||
|
||||
@@ -10,80 +10,87 @@ type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-alert-thresholds.png';
|
||||
const SCREENSHOT_PATH = "/tmp/truenas-alert-thresholds.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-alert-thresholds-${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' }],
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"playwright-auth",
|
||||
`truenas-alert-thresholds-${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" },
|
||||
],
|
||||
});
|
||||
|
||||
// The thresholds page renders live websocket state, so REST stubs of
|
||||
// /api/resources get overwritten by the next state frame. The assertions
|
||||
// pin the mock scenario's TrueNAS fixture (truenas-main with pool tank,
|
||||
// tank/* datasets, and physical disks) instead of stubbed payloads.
|
||||
test.describe('TrueNAS alert thresholds', () => {
|
||||
test.describe("TrueNAS alert thresholds", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
// Desktop presentation spec. The mobile layout collapses the platform
|
||||
// scope filter into the FilterBar sheet, so the 'TrueNAS' scope button
|
||||
// this spec clicks does not exist there and the click falls through to
|
||||
// the platform nav tab; mobile coverage lives in 04-mobile.spec.ts.
|
||||
test.skip(({ isMobile }) => Boolean(isMobile), 'desktop-presentation spec');
|
||||
// Desktop presentation spec. Mobile coverage lives in 04-mobile.spec.ts.
|
||||
test.skip(({ isMobile }) => Boolean(isMobile), "desktop-presentation spec");
|
||||
|
||||
test('surfaces TrueNAS systems, pools, datasets, and disks under the TrueNAS thresholds scope', async ({
|
||||
test("surfaces TrueNAS systems, pools, datasets, and disks under the TrueNAS thresholds scope", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Fresh test backends boot with alerts deactivated, which gates the
|
||||
// thresholds route behind the Alerts Overview activation screen. The
|
||||
// config endpoint is REST-fed and stubable (same pattern as the Ceph
|
||||
// thresholds spec); entity data below still comes from live mock state.
|
||||
await page.route('**/api/alerts/config', async (route) => {
|
||||
await page.route("**/api/alerts/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
activationState: 'active',
|
||||
activationState: "active",
|
||||
overrides: {},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/alerts/thresholds/infrastructure', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
await page.goto("/alerts/thresholds/infrastructure", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds/);
|
||||
await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Alert Thresholds" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Threshold scopes are platform-first; TrueNAS entities live under
|
||||
// their own scope instead of the retired neutral groupings.
|
||||
await page.getByRole('button', { name: 'TrueNAS', exact: true }).click();
|
||||
await page
|
||||
.getByRole("tablist", { name: "Threshold platform" })
|
||||
.getByRole("tab", { name: "TrueNAS", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds\/truenas/);
|
||||
await expect(page.getByRole('heading', { name: 'Systems' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Pools' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Datasets' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Disks' })).toBeVisible();
|
||||
await expect(page.getByText('truenas-main', { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText('tank', { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Systems" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Pools" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Datasets" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Disks" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("truenas-main", { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("tank", { exact: true }).first()).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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';
|
||||
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));
|
||||
|
||||
@@ -10,70 +10,74 @@ type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-alert-resource-links.png';
|
||||
const SCREENSHOT_PATH = "/tmp/truenas-alert-resource-links.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-alert-resource-links-${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' }],
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"tmp",
|
||||
"playwright-auth",
|
||||
`truenas-alert-resource-links-${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" },
|
||||
],
|
||||
});
|
||||
|
||||
const ALERT_START = new Date(Date.now() - 45 * 60 * 1000).toISOString();
|
||||
const ALERT_LAST_SEEN = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
||||
|
||||
test.describe('TrueNAS alert resource links', () => {
|
||||
test.describe("TrueNAS alert resource links", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps TrueNAS alert investigation on the scoped resource incidents panel', async ({
|
||||
test("keeps TrueNAS alert investigation on the scoped resource incidents panel", async ({
|
||||
page,
|
||||
isMobile,
|
||||
}) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
await page.route("**/api/resources**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.pathname !== '/api/resources') {
|
||||
if (requestUrl.pathname !== "/api/resources") {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
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',
|
||||
id: "truenas-main",
|
||||
type: "agent",
|
||||
name: "truenas-main",
|
||||
displayName: "TrueNAS Main",
|
||||
platformId: "truenas-main",
|
||||
platformType: "truenas",
|
||||
sourceType: "hybrid",
|
||||
sources: ["agent", "truenas"],
|
||||
status: "online",
|
||||
lastSeen: ALERT_LAST_SEEN,
|
||||
canonicalIdentity: {
|
||||
displayName: 'TrueNAS Main',
|
||||
hostname: 'truenas-main',
|
||||
platformId: 'truenas-main',
|
||||
displayName: "TrueNAS Main",
|
||||
hostname: "truenas-main",
|
||||
platformId: "truenas-main",
|
||||
},
|
||||
platformData: {
|
||||
sources: ['agent', 'truenas'],
|
||||
sources: ["agent", "truenas"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -87,57 +91,57 @@ test.describe('TrueNAS alert resource links', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/config', async (route) => {
|
||||
await page.route("**/api/alerts/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
activationState: 'active',
|
||||
activationState: "active",
|
||||
overrides: {},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/active', async (route) => {
|
||||
await page.route("**/api/alerts/active", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/history**', async (route) => {
|
||||
await page.route("**/api/alerts/history**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'truenas-alert-1',
|
||||
type: 'host-offline',
|
||||
level: 'critical',
|
||||
id: "truenas-alert-1",
|
||||
type: "host-offline",
|
||||
level: "critical",
|
||||
startTime: ALERT_START,
|
||||
lastSeen: ALERT_LAST_SEEN,
|
||||
resourceId: 'truenas-main',
|
||||
resourceName: 'TrueNAS Main',
|
||||
message: 'TrueNAS Main is offline',
|
||||
resourceId: "truenas-main",
|
||||
resourceName: "TrueNAS Main",
|
||||
message: "TrueNAS Main is offline",
|
||||
acknowledged: false,
|
||||
node: 'truenas-main',
|
||||
nodeDisplayName: 'TrueNAS Main',
|
||||
node: "truenas-main",
|
||||
nodeDisplayName: "TrueNAS Main",
|
||||
metadata: {
|
||||
resourceType: 'agent',
|
||||
resourceType: "agent",
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/incidents**', async (route) => {
|
||||
await page.route("**/api/alerts/incidents**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.searchParams.get('resource_id') !== 'truenas-main') {
|
||||
if (requestUrl.searchParams.get("resource_id") !== "truenas-main") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(null),
|
||||
});
|
||||
return;
|
||||
@@ -145,22 +149,22 @@ test.describe('TrueNAS alert resource links', () => {
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: 'incident-truenas-1',
|
||||
alertType: 'Host Offline',
|
||||
level: 'critical',
|
||||
status: 'open',
|
||||
id: "incident-truenas-1",
|
||||
alertType: "Host Offline",
|
||||
level: "critical",
|
||||
status: "open",
|
||||
acknowledged: false,
|
||||
openedAt: ALERT_START,
|
||||
message: 'TrueNAS Main is offline',
|
||||
message: "TrueNAS Main is offline",
|
||||
events: [
|
||||
{
|
||||
id: 'incident-event-1',
|
||||
type: 'opened',
|
||||
id: "incident-event-1",
|
||||
type: "opened",
|
||||
timestamp: ALERT_START,
|
||||
summary: 'Alert opened',
|
||||
summary: "Alert opened",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -168,18 +172,18 @@ test.describe('TrueNAS alert resource links', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/notifications/email', async (route) => {
|
||||
await page.route("**/api/notifications/email", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
provider: '',
|
||||
server: '',
|
||||
provider: "",
|
||||
server: "",
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
username: "",
|
||||
password: "",
|
||||
from: "",
|
||||
to: [],
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
@@ -187,39 +191,48 @@ test.describe('TrueNAS alert resource links', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/notifications/apprise', async (route) => {
|
||||
await page.route("**/api/notifications/apprise", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/alerts/history', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
await page.goto("/alerts/history", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Alert History' })).toBeVisible();
|
||||
const alertRow = page
|
||||
.locator('tr')
|
||||
.filter({ hasText: 'TrueNAS Main' })
|
||||
.filter({ has: page.getByRole('button', { name: 'Resource' }) })
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Alert History" }),
|
||||
).toBeVisible();
|
||||
const alertItem = (
|
||||
isMobile
|
||||
? page.getByTestId("alert-history-mobile-list").locator("article")
|
||||
: page.locator("tr")
|
||||
)
|
||||
.filter({ hasText: "TrueNAS Main" })
|
||||
.filter({ has: page.getByRole("button", { name: "Resource" }) })
|
||||
.first();
|
||||
await expect(alertRow).toBeVisible();
|
||||
await alertRow.getByRole('button', { name: 'Resource' }).click();
|
||||
await expect(alertItem).toBeVisible();
|
||||
await alertItem.getByRole("button", { name: "Resource" }).click();
|
||||
|
||||
// Cross-link affordances into the retired standalone routes were removed
|
||||
// with platform-first navigation; the canonical handoff is the resource
|
||||
// incidents panel scoped to the alerting resource.
|
||||
await expect(page.getByRole('heading', { name: 'Resource incidents' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Resource incidents" }),
|
||||
).toBeVisible();
|
||||
const incidentsPanel = page
|
||||
.locator('div')
|
||||
.filter({ has: page.getByRole('heading', { name: 'Resource incidents' }) })
|
||||
.locator("div")
|
||||
.filter({
|
||||
has: page.getByRole("heading", { name: "Resource incidents" }),
|
||||
})
|
||||
.last();
|
||||
await expect(page.getByText('TrueNAS Main').first()).toBeVisible();
|
||||
await expect(page.getByText('· 1 incident')).toBeVisible();
|
||||
await expect(page.getByText("TrueNAS Main").first()).toBeVisible();
|
||||
await expect(page.getByText("· 1 incident")).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
|
||||
@@ -7,11 +7,14 @@ test.describe("VMware alert history resource incidents", () => {
|
||||
|
||||
test("opens VMware resource incidents through the shared alert history surface", async ({
|
||||
page,
|
||||
isMobile,
|
||||
}) => {
|
||||
// Recent timestamps keep the fixture inside the history view's default
|
||||
// period window; fixed dates silently age out of it.
|
||||
const INCIDENT_START = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
||||
const INCIDENT_LAST_SEEN = new Date(Date.now() - 28 * 60 * 1000).toISOString();
|
||||
const INCIDENT_LAST_SEEN = new Date(
|
||||
Date.now() - 28 * 60 * 1000,
|
||||
).toISOString();
|
||||
await page.route("**/api/alerts/config", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -186,24 +189,33 @@ test.describe("VMware alert history resource incidents", () => {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Alert History" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("app-01")).toBeVisible();
|
||||
const historyRow = page.locator("tr").filter({ hasText: "app-01" }).first();
|
||||
await expect(historyRow).toContainText(
|
||||
const historyItem = (
|
||||
isMobile
|
||||
? page.getByTestId("alert-history-mobile-list").locator("article")
|
||||
: page.locator("tr")
|
||||
)
|
||||
.filter({ hasText: "app-01" })
|
||||
.first();
|
||||
await expect(historyItem).toContainText(
|
||||
"VM vm-201 has VMware alarm VM replication fault (red)",
|
||||
);
|
||||
await historyRow.getByRole("button", { name: "Resource" }).click();
|
||||
await historyItem.getByRole("button", { name: "Resource" }).click();
|
||||
|
||||
// The message renders in both the history table cell and the opened
|
||||
// resource drawer; assert the drawer copy specifically.
|
||||
const incidentsHeading = page.getByRole("heading", {
|
||||
name: "Resource incidents",
|
||||
});
|
||||
const incidentsPanel = incidentsHeading.locator("xpath=../../..");
|
||||
await expect(
|
||||
page
|
||||
.getByRole("paragraph")
|
||||
.filter({ hasText: "VM vm-201 has VMware alarm VM replication fault (red)" }),
|
||||
incidentsPanel.getByText(
|
||||
"VM vm-201 has VMware alarm VM replication fault (red)",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Resource incidents")).toBeVisible();
|
||||
await expect(incidentsHeading).toBeVisible();
|
||||
// "Resource Health" renders as both the incident label and a type badge
|
||||
// inside the drawer.
|
||||
await expect(page.getByText("Resource Health").first()).toBeVisible();
|
||||
await expect(
|
||||
incidentsPanel.getByText("Resource Health").first(),
|
||||
).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
|
||||
@@ -2,6 +2,21 @@ import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
|
||||
type AttentionMode = "active" | "calm" | "failed";
|
||||
|
||||
async function selectAttentionState(page: Page, label: string, value: string) {
|
||||
const queue = page.getByRole("region", { name: "Needs attention" });
|
||||
const segmentedChoice = queue.getByRole("button", {
|
||||
name: label,
|
||||
exact: true,
|
||||
});
|
||||
if (await segmentedChoice.isVisible()) {
|
||||
await segmentedChoice.click();
|
||||
return;
|
||||
}
|
||||
await queue
|
||||
.getByRole("combobox", { name: "Attention state" })
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const pageErrors: string[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
@@ -714,31 +729,23 @@ test("makes active operational work primary and preserves the evidence boundary"
|
||||
page.getByRole("button", { name: "Explain with Assistant" }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await queue
|
||||
.getByRole("button", { name: "Acknowledged 1", exact: true })
|
||||
.click();
|
||||
await selectAttentionState(page, "Acknowledged 1", "acknowledged");
|
||||
await expect(
|
||||
queue.getByText("Memory pressure acknowledged on pve-lab"),
|
||||
).toBeVisible();
|
||||
await queue
|
||||
.getByRole("button", { name: "Suppressed 1", exact: true })
|
||||
.click();
|
||||
await selectAttentionState(page, "Suppressed 1", "suppressed");
|
||||
await expect(
|
||||
queue.getByText("Maintenance alert suppressed on pve-maintenance"),
|
||||
).toBeVisible();
|
||||
await queue
|
||||
.getByRole("button", { name: "Stale or unknown 1", exact: true })
|
||||
.click();
|
||||
await selectAttentionState(page, "Stale or unknown 1", "stale_unknown");
|
||||
await expect(
|
||||
queue.getByText("Connection state unknown for pve-edge"),
|
||||
).toBeVisible();
|
||||
await queue
|
||||
.getByRole("button", { name: "Recent resolved 1", exact: true })
|
||||
.click();
|
||||
await selectAttentionState(page, "Recent resolved 1", "resolved");
|
||||
await expect(
|
||||
queue.getByText("Storage pressure resolved on pve-recovered"),
|
||||
).toBeVisible();
|
||||
await queue.getByRole("button", { name: "Active 2", exact: true }).click();
|
||||
await selectAttentionState(page, "Active 2", "active");
|
||||
|
||||
const itemButton = queue.getByRole("button", {
|
||||
name: "Open CPU pressure on pve-main",
|
||||
|
||||
@@ -130,7 +130,9 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp
|
||||
|
||||
mode = "busy";
|
||||
await page.getByRole("button", { name: "Refresh" }).click();
|
||||
await expect(page.locator("main").getByRole("alert")).toContainText("storage is busy");
|
||||
await expect(page.locator("main").getByRole("alert")).toContainText(
|
||||
"storage is busy",
|
||||
);
|
||||
await expect(page.getByText("initial-row", { exact: true })).toHaveCount(0);
|
||||
|
||||
mode = "recovered";
|
||||
@@ -139,6 +141,7 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp
|
||||
await expect(page.locator("main").getByRole("alert")).toHaveCount(0);
|
||||
|
||||
mode = "page-size";
|
||||
await page.getByRole("button", { name: "View", exact: true }).click();
|
||||
await page.getByLabel("Audit page size").selectOption("25");
|
||||
await expect(page.getByText("Showing 1-25 of 250")).toBeVisible();
|
||||
expect(
|
||||
@@ -159,6 +162,8 @@ test("audit log fails closed, recovers, pages atomically, and ignores stale resp
|
||||
|
||||
mode = "invalid";
|
||||
await page.getByRole("button", { name: "Refresh" }).click();
|
||||
await expect(page.locator("main").getByRole("alert")).toContainText("invalid response");
|
||||
await expect(page.locator("main").getByRole("alert")).toContainText(
|
||||
"invalid response",
|
||||
);
|
||||
await expect(page.getByText("latest-row", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user