ui pack redo
This commit is contained in:
327
src/Web/StellaOps.Web/tests/e2e/pack-conformance.scratch.spec.ts
Normal file
327
src/Web/StellaOps.Web/tests/e2e/pack-conformance.scratch.spec.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { policyAuthorSession } from '../../src/app/testing';
|
||||
|
||||
const shellSession = {
|
||||
...policyAuthorSession,
|
||||
scopes: [
|
||||
...new Set([
|
||||
...policyAuthorSession.scopes,
|
||||
'ui.read',
|
||||
'admin',
|
||||
'orch:read',
|
||||
'orch:operate',
|
||||
'orch:quota',
|
||||
'findings:read',
|
||||
'vuln:view',
|
||||
'vuln:investigate',
|
||||
'vuln:operate',
|
||||
'vuln:audit',
|
||||
'authority:tenants.read',
|
||||
'advisory:read',
|
||||
'vex:read',
|
||||
'exceptions:read',
|
||||
'exceptions:approve',
|
||||
'aoc:verify',
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
||||
const mockConfig = {
|
||||
authority: {
|
||||
issuer: 'http://127.0.0.1:4400/authority',
|
||||
clientId: 'stella-ops-ui',
|
||||
authorizeEndpoint: 'http://127.0.0.1:4400/authority/connect/authorize',
|
||||
tokenEndpoint: 'http://127.0.0.1:4400/authority/connect/token',
|
||||
logoutEndpoint: 'http://127.0.0.1:4400/authority/connect/logout',
|
||||
redirectUri: 'http://127.0.0.1:4400/auth/callback',
|
||||
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
|
||||
scope:
|
||||
'openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit',
|
||||
audience: 'http://127.0.0.1:4400/gateway',
|
||||
dpopAlgorithms: ['ES256'],
|
||||
refreshLeewaySeconds: 60,
|
||||
},
|
||||
apiBaseUrls: {
|
||||
authority: '/authority',
|
||||
scanner: '/scanner',
|
||||
policy: '/policy',
|
||||
concelier: '/concelier',
|
||||
attestor: '/attestor',
|
||||
gateway: '/gateway',
|
||||
},
|
||||
quickstartMode: true,
|
||||
setup: 'complete',
|
||||
};
|
||||
|
||||
const oidcConfig = {
|
||||
issuer: mockConfig.authority.issuer,
|
||||
authorization_endpoint: mockConfig.authority.authorizeEndpoint,
|
||||
token_endpoint: mockConfig.authority.tokenEndpoint,
|
||||
jwks_uri: 'http://127.0.0.1:4400/authority/.well-known/jwks.json',
|
||||
response_types_supported: ['code'],
|
||||
subject_types_supported: ['public'],
|
||||
id_token_signing_alg_values_supported: ['RS256'],
|
||||
};
|
||||
|
||||
interface PackExpectation {
|
||||
pack: string;
|
||||
path: string;
|
||||
text: RegExp;
|
||||
canonical?: RegExp;
|
||||
}
|
||||
|
||||
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 EXPECTATIONS: PackExpectation[] = [
|
||||
{ pack: '16', 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: '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: '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: '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: '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: '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/ },
|
||||
];
|
||||
|
||||
const RUN_EXPECTATIONS = (() => {
|
||||
if (!conformanceFilter) {
|
||||
return EXPECTATIONS;
|
||||
}
|
||||
const rx = new RegExp(conformanceFilter, 'i');
|
||||
return EXPECTATIONS.filter((item) => rx.test(`pack-${item.pack} ${item.path}`));
|
||||
})();
|
||||
|
||||
if (screenshotAbsDir) {
|
||||
fs.mkdirSync(screenshotAbsDir, { recursive: true });
|
||||
}
|
||||
|
||||
function slugifyRoute(routePath: string): string {
|
||||
return routePath.replace(/^\/+/, '').replace(/[\/:]+/g, '-').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
||||
}
|
||||
|
||||
async function setupShell(page: Page): Promise<void> {
|
||||
await page.addInitScript((session) => {
|
||||
try {
|
||||
window.sessionStorage.clear();
|
||||
} catch {
|
||||
// ignore storage access errors
|
||||
}
|
||||
(window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session;
|
||||
}, shellSession);
|
||||
|
||||
await page.route('**/platform/envsettings.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockConfig),
|
||||
})
|
||||
);
|
||||
await page.route('**/config.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockConfig),
|
||||
})
|
||||
);
|
||||
await page.route('**/authority/.well-known/openid-configuration', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(oidcConfig),
|
||||
})
|
||||
);
|
||||
await page.route('**/.well-known/openid-configuration', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(oidcConfig),
|
||||
})
|
||||
);
|
||||
await page.route('**/authority/.well-known/jwks.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ keys: [] }),
|
||||
})
|
||||
);
|
||||
await page.route('**/authority/connect/**', (route) =>
|
||||
route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'not-used-in-shell-e2e' }),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function go(page: Page, path: string): Promise<void> {
|
||||
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null);
|
||||
}
|
||||
|
||||
async function ensureShell(page: Page): Promise<void> {
|
||||
await expect(page.locator('aside.sidebar')).toHaveCount(1, { timeout: 15000 });
|
||||
}
|
||||
|
||||
async function assertMainHasContent(page: Page): Promise<void> {
|
||||
const main = page.locator('main');
|
||||
await expect(main).toHaveCount(1);
|
||||
await expect(main).toBeVisible();
|
||||
const text = ((await main.textContent()) ?? '').replace(/\s+/g, '');
|
||||
const childNodes = await main.locator('*').count();
|
||||
expect(text.length > 12 || childNodes > 4).toBe(true);
|
||||
}
|
||||
|
||||
test.describe('Pack conformance from docs/modules/ui/v2-rewire/pack-01..21', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupShell(page);
|
||||
});
|
||||
|
||||
test('canonical screens render and expose expected pack text markers', async ({ page }, testInfo) => {
|
||||
test.setTimeout(6 * 60_000);
|
||||
const failures: string[] = [];
|
||||
const screenshotIndex: Array<{ pack: string; route: string; file: string }> = [];
|
||||
const scoped = RUN_EXPECTATIONS;
|
||||
|
||||
for (const item of scoped) {
|
||||
await test.step(`pack-${item.pack} ${item.path}`, async () => {
|
||||
try {
|
||||
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 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 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)) {
|
||||
failures.push(
|
||||
`[pack-${item.pack}] ${item.path} -> canonical mismatch, expected ${item.canonical}, got ${currentUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (screenshotAbsDir) {
|
||||
const rows = ['pack,route,file', ...screenshotIndex.map((row) => `${row.pack},${row.route},${row.file}`)];
|
||||
fs.writeFileSync(path.join(screenshotAbsDir, 'index.csv'), `${rows.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,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
// Emit full list in test output for quick triage without opening traces.
|
||||
console.error(`Pack conformance mismatches (${failures.length})\n${ledger}`);
|
||||
}
|
||||
|
||||
expect(failures, `Pack conformance mismatches (${failures.length})`).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user