ui progressing

This commit is contained in:
master
2026-02-20 23:32:20 +02:00
parent ca5e7888d6
commit 1ec797d5e8
191 changed files with 32771 additions and 6504 deletions

View File

@@ -76,75 +76,50 @@ interface PackExpectation {
const conformanceFilter = process.env.PACK_CONFORMANCE_FILTER?.trim();
const screenshotDir = process.env.PACK_SCREENSHOT_DIR?.trim();
const screenshotAbsDir = screenshotDir ? path.resolve(process.cwd(), screenshotDir) : null;
const endpointMatrixFile = process.env.PACK_ENDPOINT_MATRIX_FILE?.trim();
const endpointMatrixAbsFile = endpointMatrixFile ? path.resolve(process.cwd(), endpointMatrixFile) : null;
const EXPECTATIONS: PackExpectation[] = [
{ pack: '16', path: '/dashboard', text: /Dashboard/i, canonical: /\/dashboard$/ },
{ pack: '22', path: '/dashboard', text: /Dashboard/i, canonical: /\/dashboard$/ },
{ pack: '8', path: '/release-control/control-plane', text: /Control Plane|Release Control Home|Dashboard/i, canonical: /\/release-control\/control-plane$/ },
{ pack: '12', path: '/release-control/bundles', text: /Bundle/i, canonical: /\/release-control\/bundles$/ },
{ pack: '12', path: '/release-control/bundles/create', text: /Create Bundle|Bundle Builder|Bundle Organizer/i, canonical: /\/release-control\/bundles\/create$/ },
{ pack: '12', path: '/release-control/bundles/platform-release/organizer', text: /Bundle Organizer|Create Bundle Version|Select Components/i, canonical: /\/release-control\/bundles\/platform-release\/organizer$/ },
{ pack: '12', path: '/release-control/bundles/platform-release', text: /Bundle Detail|Latest manifest digest|Version timeline/i, canonical: /\/release-control\/bundles\/platform-release$/ },
{ pack: '12', path: '/release-control/bundles/platform-release/versions/version-3', text: /Bundle manifest digest|Version|Materialization/i, canonical: /\/release-control\/bundles\/platform-release\/versions\/version-3$/ },
{ pack: '13', path: '/release-control/releases', text: /Release/i, canonical: /\/release-control\/releases$/ },
{ pack: '13', path: '/release-control/approvals', text: /Approval/i, canonical: /\/release-control\/approvals$/ },
{ pack: '14', path: '/release-control/runs', text: /Run Timeline|Pipeline Runs|Runs/i, canonical: /\/release-control\/runs$/ },
{ pack: '11', path: '/release-control/regions', text: /Region|Environment/i, canonical: /\/release-control\/regions$/ },
{ pack: '11', path: '/release-control/regions/us-east', text: /Region|us-east|Environment/i, canonical: /\/release-control\/regions\/us-east$/ },
{ pack: '18', path: '/release-control/regions/us-east/environments/staging', text: /Environment|staging|Deploy|SBOM/i, canonical: /\/release-control\/regions\/us-east\/environments\/staging$/ },
{ pack: '11', path: '/release-control/governance', text: /Governance|Policy/i, canonical: /\/release-control\/governance/ },
{ pack: '8', path: '/release-control/hotfixes', text: /Hotfix/i, canonical: /\/release-control\/hotfixes$/ },
{ pack: '21', path: '/release-control/setup', text: /Setup/i, canonical: /\/release-control\/setup$/ },
{ pack: '21', path: '/release-control/setup/environments-paths', text: /Environment|Promotion Path/i, canonical: /\/release-control\/setup\/environments-paths$/ },
{ pack: '21', path: '/release-control/setup/targets-agents', text: /Targets|Agents/i, canonical: /\/release-control\/setup\/targets-agents$/ },
{ pack: '21', path: '/release-control/setup/workflows', text: /Workflow/i, canonical: /\/release-control\/setup\/workflows$/ },
{ pack: '22', path: '/releases', text: /Release|Run/i, canonical: /\/releases\/runs$/ },
{ pack: '22', path: '/releases/new', text: /Create Release|Version|Release/i, canonical: /\/releases\/versions\/new$/ },
{ pack: '22', path: '/releases/activity', text: /Activity|Release|Run/i, canonical: /\/releases\/runs$/ },
{ pack: '22', path: '/releases/approvals', text: /Approval/i, canonical: /\/releases\/approvals$/ },
{ pack: '19', path: '/security-risk', text: /Risk Overview|Security/i, canonical: /\/security-risk$/ },
{ pack: '19', path: '/security-risk/findings', text: /Findings/i, canonical: /\/security-risk\/findings$/ },
{ pack: '19', path: '/security-risk/findings/fnd-001', text: /Finding Detail|Finding/i, canonical: /\/security-risk\/findings\/fnd-001$/ },
{ pack: '19', path: '/security-risk/vulnerabilities', text: /Vulnerab/i, canonical: /\/security-risk\/vulnerabilities$/ },
{ pack: '19', path: '/security-risk/vulnerabilities/CVE-2024-1234', text: /Vulnerability|CVE/i, canonical: /\/security-risk\/vulnerabilities\/CVE-2024-1234$/ },
{ pack: '19', path: '/security-risk/sbom-lake', text: /SBOM Lake|SBOM/i, canonical: /\/security-risk\/sbom-lake$/ },
{ pack: '19', path: '/security-risk/sbom', text: /SBOM Graph|SBOM/i, canonical: /\/security-risk\/sbom$/ },
{ pack: '19', path: '/security-risk/vex', text: /VEX/i, canonical: /\/security-risk\/vex/ },
{ pack: '19', path: '/security-risk/exceptions', text: /Exceptions?/i, canonical: /\/security-risk\/exceptions$/ },
{ pack: '19', path: '/security-risk/advisory-sources', text: /Advisory Sources/i, canonical: /\/security-risk\/advisory-sources$/ },
{ pack: '22', path: '/security', text: /Risk Overview|Security/i, canonical: /\/security\/overview$/ },
{ pack: '22', path: '/security/findings', text: /Findings|Triage/i, canonical: /\/security\/triage$/ },
{ pack: '22', path: '/security/disposition', text: /Disposition|VEX|Exception/i, canonical: /\/security\/advisories-vex$/ },
{ pack: '22', path: '/security/sbom/lake', text: /SBOM|Supply-Chain|Component/i, canonical: /\/security\/supply-chain-data\/lake$/ },
{ pack: '20', path: '/evidence-audit', text: /Find Evidence|Evidence & Audit/i, canonical: /\/evidence-audit$/ },
{ pack: '20', path: '/evidence-audit/packs', text: /Evidence Pack/i, canonical: /\/evidence-audit\/packs$/ },
{ pack: '20', path: '/evidence-audit/bundles', text: /Evidence Bundle/i, canonical: /\/evidence-audit\/bundles$/ },
{ pack: '20', path: '/evidence-audit/evidence/export', text: /Export Center|Profile|Export Runs/i, canonical: /\/evidence-audit\/evidence\/export$/ },
{ pack: '20', path: '/evidence-audit/proofs', text: /Proof Chain/i, canonical: /\/evidence-audit\/proofs/ },
{ pack: '20', path: '/evidence-audit/replay', text: /Replay|Verify/i, canonical: /\/evidence-audit\/replay$/ },
{ pack: '20', path: '/evidence-audit/trust-signing', text: /Trust|Signing/i, canonical: /\/evidence-audit\/trust-signing/ },
{ pack: '20', path: '/evidence-audit/audit-log', text: /Audit Log|Events?/i, canonical: /\/evidence-audit\/audit-log/ },
{ pack: '22', path: '/evidence', text: /Evidence/i, canonical: /\/evidence\/overview$/ },
{ pack: '22', path: '/evidence/packs', text: /Decision Capsule|Capsule|Evidence/i, canonical: /\/evidence\/capsules$/ },
{ pack: '22', path: '/evidence/exports', text: /Export Center|Export/i, canonical: /\/evidence\/exports\/export$/ },
{ pack: '22', path: '/evidence/audit-log', text: /Audit Log|Events?/i, canonical: /\/evidence\/audit-log/ },
{ pack: '21', path: '/integrations', text: /Integration Hub|Integrations/i, canonical: /\/integrations$/ },
{ pack: '21', path: '/integrations/registries', text: /Registries|Registry/i, canonical: /\/integrations\/registries$/ },
{ pack: '21', path: '/integrations/scm', text: /Source Control|SCM/i, canonical: /\/integrations\/scm$/ },
{ pack: '21', path: '/integrations/ci-cd', text: /CI\/CD|CI/i, canonical: /\/integrations\/ci$/ },
{ pack: '21', path: '/integrations/targets', text: /Target|Runtime|Host/i, canonical: /\/integrations\/hosts$/ },
{ pack: '21', path: '/integrations/secrets', text: /Secrets/i, canonical: /\/integrations\/secrets$/ },
{ pack: '21', path: '/integrations/feeds', text: /Feed|Advisory/i, canonical: /\/integrations\/feeds$/ },
{ pack: '22', path: '/topology/regions', text: /Region|Topology/i, canonical: /\/topology\/regions$/ },
{ pack: '22', path: '/topology/environments', text: /Environment|Topology/i, canonical: /\/topology\/environments$/ },
{ pack: '22', path: '/topology/agents', text: /Agent|Topology/i, canonical: /\/topology\/agents$/ },
{ pack: '22', path: '/topology/promotion-paths', text: /Promotion|Path|Topology/i, canonical: /\/topology\/promotion-paths$/ },
{ pack: '15', path: '/platform-ops', text: /Platform Ops|Operations/i, canonical: /\/platform-ops$/ },
{ pack: '15', path: '/platform-ops/data-integrity', text: /Data Integrity|Nightly Ops/i, canonical: /\/platform-ops\/data-integrity$/ },
{ pack: '6', path: '/platform-ops/scheduler', text: /Scheduler/i, canonical: /\/platform-ops\/scheduler/ },
{ pack: '6', path: '/platform-ops/orchestrator', text: /Orchestrator/i, canonical: /\/platform-ops\/orchestrator$/ },
{ pack: '6', path: '/platform-ops/orchestrator/jobs', text: /Jobs|Orchestrator/i, canonical: /\/platform-ops\/orchestrator\/jobs$/ },
{ pack: '6', path: '/platform-ops/health', text: /Platform Health|Health/i, canonical: /\/platform-ops\/health$/ },
{ pack: '6', path: '/platform-ops/quotas', text: /Quota|Limits/i, canonical: /\/platform-ops\/quotas/ },
{ pack: '6', path: '/platform-ops/dead-letter', text: /Dead Letter|Queue/i, canonical: /\/platform-ops\/dead-letter/ },
{ pack: '6', path: '/platform-ops/feeds', text: /Feeds|Mirror|AirGap/i, canonical: /\/platform-ops\/feeds/ },
{ pack: '22', path: '/operations', text: /Operations|Platform Ops/i, canonical: /\/operations$/ },
{ pack: '22', path: '/operations/data-integrity', text: /Data Integrity|Ops/i, canonical: /\/operations\/data-integrity$/ },
{ pack: '22', path: '/operations/orchestrator', text: /Orchestrator/i, canonical: /\/operations\/orchestrator$/ },
{ pack: '22', path: '/operations/feeds', text: /Feeds|Mirror/i, canonical: /\/operations\/feeds/ },
{ pack: '21', path: '/administration', text: /Administration/i, canonical: /\/administration$/ },
{ pack: '21', path: '/administration/identity-access', text: /Identity|Access|Users|Roles/i, canonical: /\/administration\/identity-access/ },
{ pack: '21', path: '/administration/tenant-branding', text: /Tenant|Branding/i, canonical: /\/administration\/tenant-branding/ },
{ pack: '21', path: '/administration/notifications', text: /Notification/i, canonical: /\/administration\/notifications/ },
{ pack: '21', path: '/administration/usage', text: /Usage|Limits/i, canonical: /\/administration\/usage/ },
{ pack: '21', path: '/administration/policy-governance', text: /Policy|Governance/i, canonical: /\/administration\/policy-governance/ },
{ pack: '21', path: '/administration/system', text: /System/i, canonical: /\/administration\/system/ },
{ pack: '21', path: '/administration/offline', text: /Offline/i, canonical: /\/administration\/offline/ },
{ pack: '22', path: '/integrations', text: /Integration Hub|Integrations/i, canonical: /\/integrations$/ },
{ pack: '22', path: '/integrations/feeds', text: /Feed|Advisory/i, canonical: /\/integrations\/feeds$/ },
{ pack: '22', path: '/integrations/vex-sources', text: /VEX|Source/i, canonical: /\/integrations\/vex-sources$/ },
{ pack: '22', path: '/administration', text: /Platform Setup|Administration/i, canonical: /\/platform\/setup/ },
{ pack: '22', path: '/administration/policy-governance', text: /Policy|Governance/i, canonical: /\/administration\/policy-governance/ },
// Legacy roots must continue to resolve into canonical Pack 22 routes.
{ pack: '22', path: '/release-control/releases', text: /Release|Run/i, canonical: /\/releases\/runs$/ },
{ pack: '22', path: '/release-control/setup/environments-paths', text: /Promotion|Path|Topology/i, canonical: /\/topology\/promotion-paths$/ },
{ pack: '22', path: '/security-risk/findings', text: /Findings|Triage/i, canonical: /\/security\/triage$/ },
{ pack: '22', path: '/evidence-audit/packs', text: /Decision Capsule|Capsule|Evidence/i, canonical: /\/evidence\/capsules$/ },
{ pack: '22', path: '/platform-ops/agents', text: /Agent|Topology/i, canonical: /\/topology\/agents$/ },
];
const RUN_EXPECTATIONS = (() => {
@@ -158,6 +133,9 @@ const RUN_EXPECTATIONS = (() => {
if (screenshotAbsDir) {
fs.mkdirSync(screenshotAbsDir, { recursive: true });
}
if (endpointMatrixAbsFile) {
fs.mkdirSync(path.dirname(endpointMatrixAbsFile), { recursive: true });
}
function slugifyRoute(routePath: string): string {
return routePath.replace(/^\/+/, '').replace(/[\/:]+/g, '-').replace(/[^a-zA-Z0-9._-]+/g, '-');
@@ -222,6 +200,19 @@ async function go(page: Page, path: string): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
}
async function goClientSide(page: Page, routePath: string): Promise<void> {
await go(page, '/dashboard');
await page.evaluate((targetPath) => {
window.history.pushState({}, '', targetPath);
window.dispatchEvent(new PopStateEvent('popstate'));
}, routePath);
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
}
function isProxyCapturedDevRoute(routePath: string): boolean {
return routePath.startsWith('/integrations') || routePath.startsWith('/platform-ops');
}
async function ensureShell(page: Page): Promise<void> {
await expect(page.locator('aside.sidebar')).toHaveCount(1, { timeout: 15000 });
}
@@ -244,64 +235,112 @@ test.describe('Pack conformance from docs/modules/ui/v2-rewire/pack-01..21', ()
test.setTimeout(6 * 60_000);
const failures: string[] = [];
const screenshotIndex: Array<{ pack: string; route: string; file: string }> = [];
const endpointRecords: Array<{
pack: string;
route: string;
method: string;
status: number;
path: string;
resourceType: string;
}> = [];
let activeRecord: { pack: string; route: string } | null = null;
const scoped = RUN_EXPECTATIONS;
page.on('response', (response) => {
if (!activeRecord) {
return;
}
const request = response.request();
const resourceType = request.resourceType();
if (resourceType !== 'xhr' && resourceType !== 'fetch') {
return;
}
let responsePath = response.url();
try {
responsePath = new URL(response.url()).pathname;
} catch {
// Keep raw URL if parsing fails.
}
endpointRecords.push({
pack: activeRecord.pack,
route: activeRecord.route,
method: request.method(),
status: response.status(),
path: responsePath,
resourceType,
});
});
for (const item of scoped) {
await test.step(`pack-${item.pack} ${item.path}`, async () => {
activeRecord = { pack: item.pack, route: item.path };
try {
await go(page, item.path);
} catch (error) {
failures.push(`[pack-${item.pack}] ${item.path} -> navigation failed: ${String(error)}`);
return;
}
try {
if (isProxyCapturedDevRoute(item.path)) {
// Dev proxy maps "/integrations" and "/platform*" to backend services.
// Reach these routes via client-side routing to validate SPA shell behavior.
await goClientSide(page, item.path);
} else {
await go(page, item.path);
}
} catch (error) {
failures.push(`[pack-${item.pack}] ${item.path} -> navigation failed: ${String(error)}`);
return;
}
const shellCount = await page.locator('aside.sidebar').count();
if (shellCount !== 1) {
failures.push(`[pack-${item.pack}] ${item.path} -> shell sidebar count=${shellCount}`);
}
const shellCount = await page.locator('aside.sidebar').count();
if (shellCount !== 1) {
failures.push(`[pack-${item.pack}] ${item.path} -> shell sidebar count=${shellCount}`);
}
const main = page.locator('main.shell__outlet').first();
const mainCount = await page.locator('main.shell__outlet').count();
if (mainCount !== 1) {
failures.push(`[pack-${item.pack}] ${item.path} -> shell main count=${mainCount}`);
return;
}
const main = page.locator('main.shell__outlet').first();
const mainCount = await page.locator('main.shell__outlet').count();
if (mainCount !== 1) {
failures.push(`[pack-${item.pack}] ${item.path} -> shell main count=${mainCount}`);
return;
}
const isVisible = await main.isVisible().catch(() => false);
if (!isVisible) {
failures.push(`[pack-${item.pack}] ${item.path} -> main is not visible`);
}
const isVisible = await main.isVisible().catch(() => false);
if (!isVisible) {
failures.push(`[pack-${item.pack}] ${item.path} -> main is not visible`);
}
const mainText = (await main.textContent().catch(() => '')) ?? '';
const compactText = mainText.replace(/\s+/g, '');
const childNodes = await main.locator('*').count().catch(() => 0);
if (!(compactText.length > 12 || childNodes > 4)) {
failures.push(
`[pack-${item.pack}] ${item.path} -> main appears empty (text=${compactText.length}, children=${childNodes})`
);
}
if (item.canonical) {
const currentUrl = page.url();
if (!item.canonical.test(currentUrl)) {
const mainText = (await main.textContent().catch(() => '')) ?? '';
const compactText = mainText.replace(/\s+/g, '');
const childNodes = await main.locator('*').count().catch(() => 0);
if (!(compactText.length > 12 || childNodes > 4)) {
failures.push(
`[pack-${item.pack}] ${item.path} -> canonical mismatch, expected ${item.canonical}, got ${currentUrl}`
`[pack-${item.pack}] ${item.path} -> main appears empty (text=${compactText.length}, children=${childNodes})`
);
}
}
if (!item.text.test(mainText)) {
const preview = mainText.replace(/\s+/g, ' ').trim().slice(0, 220);
failures.push(
`[pack-${item.pack}] ${item.path} -> missing text ${item.text}, preview="${preview}"`
);
}
if (item.canonical) {
const currentUrl = page.url();
if (!item.canonical.test(currentUrl)) {
failures.push(
`[pack-${item.pack}] ${item.path} -> canonical mismatch, expected ${item.canonical}, got ${currentUrl}`
);
}
}
if (screenshotAbsDir) {
const fileName = `pack-${item.pack}_${slugifyRoute(item.path)}.png`;
const absFile = path.join(screenshotAbsDir, fileName);
await page.screenshot({ path: absFile, fullPage: true });
screenshotIndex.push({ pack: item.pack, route: item.path, file: fileName });
if (!item.text.test(mainText)) {
const preview = mainText.replace(/\s+/g, ' ').trim().slice(0, 220);
failures.push(
`[pack-${item.pack}] ${item.path} -> missing text ${item.text}, preview="${preview}"`
);
}
if (screenshotAbsDir) {
const fileName = `pack-${item.pack}_${slugifyRoute(item.path)}.png`;
const absFile = path.join(screenshotAbsDir, fileName);
await page.screenshot({ path: absFile, fullPage: true });
screenshotIndex.push({ pack: item.pack, route: item.path, file: fileName });
}
} finally {
activeRecord = null;
}
});
}
@@ -311,6 +350,33 @@ test.describe('Pack conformance from docs/modules/ui/v2-rewire/pack-01..21', ()
fs.writeFileSync(path.join(screenshotAbsDir, 'index.csv'), `${rows.join('\n')}\n`, 'utf8');
}
if (endpointMatrixAbsFile) {
const dedup = new Map<string, { count: number; row: typeof endpointRecords[number] }>();
for (const row of endpointRecords) {
const key = `${row.pack}|${row.route}|${row.method}|${row.status}|${row.path}|${row.resourceType}`;
const current = dedup.get(key);
if (current) {
current.count += 1;
} else {
dedup.set(key, { count: 1, row });
}
}
const csvRows = [
'pack,route,method,status,path,resourceType,count',
...Array.from(dedup.values())
.sort((a, b) =>
`${a.row.pack} ${a.row.route} ${a.row.path}`.localeCompare(
`${b.row.pack} ${b.row.route} ${b.row.path}`
)
)
.map(({ row, count }) =>
`${row.pack},${row.route},${row.method},${row.status},${row.path},${row.resourceType},${count}`
),
];
fs.writeFileSync(endpointMatrixAbsFile, `${csvRows.join('\n')}\n`, 'utf8');
}
const ledger = failures.length > 0 ? failures.join('\n') : 'All pack routes matched current expectations.';
await testInfo.attach('pack-conformance-ledger', {
body: ledger,