Add Playwright proof for onboarding diagnostics

This commit is contained in:
rcourtman
2026-04-22 15:58:11 +01:00
parent ce95ef1fc6
commit f682ec475b
2 changed files with 349 additions and 0 deletions
+4
View File
@@ -58,6 +58,10 @@ End-to-end Playwright tests that validate critical user flows against a running
- Proves the add-infrastructure surface renders the decision-led API-vs-agent onboarding flow in the shared settings shell
- Verifies the onboarding funnel emits `opened`, API catalog handoff, and no-match-to-agent fallback metrics on the real browser runtime
- Verifies the mobile add-infrastructure flow fits the viewport without horizontal overflow
- `tests/69-diagnostics-onboarding.spec.ts` — diagnostics onboarding analytics browser proof:
- Proves the Diagnostics & Health page renders the infrastructure onboarding analytics card after a real diagnostics run
- Verifies the shared diagnostics surface shows onboarding path/platform attribution alongside the existing commercial funnel
- Verifies the populated mobile diagnostics page fits the viewport without horizontal overflow
## Running Tests
@@ -0,0 +1,345 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test as base, type Page } from '@playwright/test';
import { createAuthenticatedStorageState } from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
type WorkerFixtures = {
authStorageStatePath: string;
};
type OverflowAudit = {
viewportWidth: number;
pageWidth: number;
overflowPx: number;
offenders: Array<{ tag: string; className: string; overflow: number }>;
};
const DIAGNOSTICS_PAYLOAD = {
version: '6.0.0',
runtime: 'go',
uptime: 3600,
nodes: [],
pbs: [],
system: {
os: 'linux',
arch: 'amd64',
goVersion: 'go1.25',
numCPU: 8,
numGoroutine: 32,
memoryMB: 128,
},
metricsStore: {
enabled: true,
status: 'healthy',
dbSize: 4 * 1024 * 1024,
rawCount: 12,
minuteCount: 24,
hourlyCount: 36,
dailyCount: 48,
totalPoints: 120,
bufferSize: 0,
notes: [],
},
commercialFunnel: {
enabled: true,
status: 'active',
windowDays: 30,
summary: {
pricing_viewed: 3,
paywall_viewed: 0,
trial_started: 1,
upgrade_clicked: 0,
checkout_clicked: 2,
checkout_started: 2,
checkout_completed: 1,
license_activated: 1,
license_activation_failed: 0,
period: {
from: '2026-03-19T00:00:00Z',
to: '2026-04-18T00:00:00Z',
},
},
daily: [
{
day: '2026-04-17',
pricing_viewed: 1,
paywall_viewed: 0,
trial_started: 0,
upgrade_clicked: 0,
checkout_clicked: 1,
checkout_started: 1,
checkout_completed: 0,
license_activated: 0,
license_activation_failed: 0,
},
],
surfaces: [
{
key: 'settings_self_hosted_billing_compare_prompt',
pricing_viewed: 0,
paywall_viewed: 0,
trial_started: 0,
upgrade_clicked: 0,
checkout_clicked: 2,
checkout_started: 0,
checkout_completed: 0,
license_activated: 0,
license_activation_failed: 0,
},
],
capabilities: [
{
key: 'self_hosted_plan',
pricing_viewed: 3,
paywall_viewed: 0,
trial_started: 0,
upgrade_clicked: 0,
checkout_clicked: 2,
checkout_started: 2,
checkout_completed: 1,
license_activated: 1,
license_activation_failed: 0,
},
],
notes: ['Local pricing and activation events show at least one completed conversion.'],
},
infrastructureOnboarding: {
enabled: true,
status: 'warning',
windowDays: 30,
summary: {
opened: 4,
api_path_selected: 2,
agent_path_selected: 1,
probe_detected: 1,
probe_no_match: 2,
probe_error: 0,
catalog_selected: 2,
credentials_opened: 1,
period: {
from: '2026-03-19T00:00:00Z',
to: '2026-04-18T00:00:00Z',
},
},
daily: [
{
day: '2026-04-17',
opened: 2,
api_path_selected: 1,
agent_path_selected: 1,
probe_detected: 0,
probe_no_match: 1,
probe_error: 0,
catalog_selected: 1,
credentials_opened: 0,
},
{
day: '2026-04-18',
opened: 2,
api_path_selected: 1,
agent_path_selected: 0,
probe_detected: 1,
probe_no_match: 1,
probe_error: 0,
catalog_selected: 1,
credentials_opened: 1,
},
],
paths: [
{ key: 'api', count: 2 },
{ key: 'agent', count: 1 },
],
platforms: [
{ key: 'truenas', catalog_selected: 2, credentials_opened: 1 },
],
notes: ['More probed addresses miss than detect a supported API-backed platform.'],
},
discovery: {
enabled: true,
configuredSubnet: 'auto',
scanInterval: '5m',
lastResultServers: 3,
},
apiTokens: {
enabled: true,
tokenCount: 2,
recommendTokenSetup: false,
unusedTokenCount: 0,
notes: [],
},
dockerAgents: {
agentsTotal: 1,
agentsOnline: 1,
agentsReportingVersion: 1,
agentsWithTokenBinding: 1,
agentsWithoutTokenBinding: 0,
agentsNeedingAttention: 0,
notes: [],
},
alerts: {
missingCooldown: false,
missingGroupingWindow: false,
notes: [],
},
aiChat: {
enabled: true,
running: true,
healthy: true,
mcpConnected: true,
notes: [],
},
errors: [],
nodeSnapshots: [],
guestSnapshots: [],
memorySources: [],
memorySourceBreakdown: [],
};
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',
`diagnostics-onboarding-${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 prepareDiagnosticsRoute(page: Page): Promise<void> {
await page.route('**/api/diagnostics', async (route) => {
const requestUrl = new URL(route.request().url());
if (route.request().method() !== 'GET' || requestUrl.pathname !== '/api/diagnostics') {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(DIAGNOSTICS_PAYLOAD),
});
});
}
async function scrollToBottom(page: Page): Promise<void> {
const viewportHeight = await page.evaluate(() => window.innerHeight || 800);
const step = Math.max(240, Math.floor(viewportHeight * 0.75));
let wheelSupported = true;
for (let i = 0; i < 20; i += 1) {
if (wheelSupported) {
try {
await page.mouse.wheel(0, step);
} catch {
wheelSupported = false;
await page.evaluate((deltaY) => window.scrollBy(0, deltaY), step);
}
} else {
await page.evaluate((deltaY) => window.scrollBy(0, deltaY), step);
}
await page.waitForTimeout(60);
}
}
async function auditHorizontalOverflow(page: Page): Promise<OverflowAudit> {
return page.evaluate(() => {
const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const pageWidth = Math.max(
document.body.scrollWidth,
document.documentElement.scrollWidth,
document.body.offsetWidth,
document.documentElement.offsetWidth,
);
const offenders = Array.from(document.querySelectorAll('body *'))
.map((el) => {
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return null;
const style = window.getComputedStyle(el);
if (style.position === 'fixed' || style.position === 'absolute') return null;
const overflow = rect.right - viewportWidth;
if (overflow <= 1) return null;
return {
tag: el.tagName.toLowerCase(),
className: (el.getAttribute('class') || '').trim().slice(0, 120),
overflow: Number(overflow.toFixed(1)),
};
})
.filter((entry): entry is { tag: string; className: string; overflow: number } => Boolean(entry))
.slice(0, 8);
return {
viewportWidth,
pageWidth,
overflowPx: Number((pageWidth - viewportWidth).toFixed(1)),
offenders,
};
});
}
test.describe('Diagnostics onboarding analytics', () => {
test.setTimeout(180_000);
test('desktop diagnostics renders the onboarding analytics card in the shared settings shell', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only diagnostics shell coverage');
await prepareDiagnosticsRoute(page);
await page.goto('/settings/support/diagnostics', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/settings\/support\/diagnostics$/, { timeout: 15_000 });
await expect(page.getByRole('heading', { name: 'Diagnostics & Health' })).toBeVisible();
await page.getByRole('button', { name: 'Run Diagnostics', exact: true }).first().click();
await expect(page.getByText('Commercial Funnel', { exact: true })).toBeVisible();
await expect(page.getByText('Infrastructure Onboarding', { exact: true })).toBeVisible();
await expect(page.getByText('Credentials Opened', { exact: true })).toBeVisible();
await expect(page.getByText('TrueNAS SCALE', { exact: true })).toBeVisible();
await expect(page.getByText('API', { exact: true })).toBeVisible();
await expect(
page.getByText('More probed addresses miss than detect a supported API-backed platform.', {
exact: true,
}),
).toBeVisible();
});
test('mobile diagnostics keeps the populated onboarding analytics page inside the viewport', async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.startsWith('mobile-'), 'Mobile-only diagnostics overflow coverage');
await prepareDiagnosticsRoute(page);
await page.goto('/settings/support/diagnostics', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/settings\/support\/diagnostics$/, { timeout: 15_000 });
await page.getByRole('button', { name: 'Run', exact: true }).click();
await expect(page.getByText('Infrastructure Onboarding', { exact: true })).toBeVisible();
await scrollToBottom(page);
const audit = await auditHorizontalOverflow(page);
expect(
audit.pageWidth,
`Mobile diagnostics overflow (viewport=${audit.viewportWidth}, page=${audit.pageWidth}, offenders=${JSON.stringify(audit.offenders)})`,
).toBeLessThanOrEqual(audit.viewportWidth + 1);
});
});