diff --git a/frontend/package.json b/frontend/package.json index ff7d4b9e..b4b1285e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,10 +11,10 @@ "test": "vitest run", "e2e": "playwright test", "validate:ui-boundary": "node ../scripts/validate-ui-boundary.mjs --root .", - "validate:component-manifest": "node ../scripts/validate-kbx-component-manifest.mjs --root ." - ,"validate:screen-recipes": "node ../scripts/validate-kbx-screen-recipes.mjs --root ." - ,"validate:ai-components": "node ../scripts/validate-kbx-ai-components.mjs --root ." - ,"validate:exceptions": "node ../scripts/validate-kbx-exceptions.mjs --root .", + "validate:component-manifest": "node ../scripts/validate-kbx-component-manifest.mjs --root .", + "validate:screen-recipes": "node ../scripts/validate-kbx-screen-recipes.mjs --root .", + "validate:ai-components": "node ../scripts/validate-kbx-ai-components.mjs --root .", + "validate:exceptions": "node ../scripts/validate-kbx-exceptions.mjs --root .", "validate:kbx": "node ../scripts/validate-kbx-governance.mjs --root ." }, "dependencies": { @@ -31,11 +31,12 @@ "zod": "^4.0.0" }, "devDependencies": { - "@playwright/test": "^1.0.0", + "@playwright/test": "^1.62.1", "@types/node": "^26.1.2", "@vitejs/plugin-vue": "^6.0.0", "@vue/test-utils": "^2.0.0", "jsdom": "^26.0.0", + "playwright": "^1.62.1", "typescript": "^5.0.0", "vite": "^8.0.0", "vitest": "^4.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 68d4ee19..4a11cf4f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 4.4.3 devDependencies: '@playwright/test': - specifier: ^1.0.0 + specifier: ^1.62.1 version: 1.62.1 '@types/node': specifier: ^26.1.2 @@ -57,6 +57,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.1.0 + playwright: + specifier: ^1.62.1 + version: 1.62.1 typescript: specifier: ^5.0.0 version: 5.9.3 diff --git a/frontend/src/app/router.ts b/frontend/src/app/router.ts index 90499596..185407ec 100644 --- a/frontend/src/app/router.ts +++ b/frontend/src/app/router.ts @@ -18,6 +18,10 @@ export const router = createRouter({ { path: '/model-ops/shadow-runs', component: () => import('../features/shadow-run/pages/ShadowRunList.vue'), meta: { screenId: 'model-ops.shadow-run.list', module: 'ModelOps', title: 'Shadow Run Validation', permissions: ['model.read'] } }, { path: '/model-ops/shadow-runs/:runId', component: () => import('../features/shadow-run/pages/ShadowRunDetail.vue'), meta: { screenId: 'model-ops.shadow-run.detail', module: 'ModelOps', title: 'Shadow Run Details', permissions: ['model.read'] } }, { path: '/model-ops/models', component: () => import('../features/models/pages/ModelsList.vue'), meta: { screenId: 'model-ops.models.list', module: 'ModelOps', title: 'Model Management', permissions: ['model.read'] } }, - { path: '/model-ops/models/:modelId', component: () => import('../features/models/pages/ModelDetail.vue'), meta: { screenId: 'model-ops.models.detail', module: 'ModelOps', title: 'Model Details', permissions: ['model.read'] } } + { path: '/model-ops/models/:modelId', component: () => import('../features/models/pages/ModelDetail.vue'), meta: { screenId: 'model-ops.models.detail', module: 'ModelOps', title: 'Model Details', permissions: ['model.read'] } }, + // KBX v60 Pages + { path: '/model-ops/shadow-run-jobs', component: () => import('../features/shadow-run/pages/ShadowRunQueue.vue'), meta: { screenId: 'model-ops.shadow-run.queue', module: 'ModelOps', title: 'Shadow Run Jobs', permissions: ['model.read'] } }, + { path: '/model-ops/models-master', component: () => import('../features/models/pages/ModelList.vue'), meta: { screenId: 'model-ops.models.master', module: 'ModelOps', title: 'Models (Master-Detail)', permissions: ['model.read'] } }, + { path: '/governance/approvals', component: () => import('../features/approval/pages/ApprovalQueue.vue'), meta: { screenId: 'governance.approval.queue', module: 'Governance', title: 'Approval Queue', permissions: ['approval.review'] } } ] }) diff --git a/frontend/src/features/approval/pages/ApprovalQueue.vue b/frontend/src/features/approval/pages/ApprovalQueue.vue index 477c8eaf..6b12a6a5 100644 --- a/frontend/src/features/approval/pages/ApprovalQueue.vue +++ b/frontend/src/features/approval/pages/ApprovalQueue.vue @@ -1,35 +1,41 @@ diff --git a/frontend/src/features/models/pages/ModelList.vue b/frontend/src/features/models/pages/ModelList.vue index 99a559b7..571b2ad0 100644 --- a/frontend/src/features/models/pages/ModelList.vue +++ b/frontend/src/features/models/pages/ModelList.vue @@ -1,59 +1,56 @@ diff --git a/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue b/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue index 0203e1d5..dfe3fcd7 100644 --- a/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue +++ b/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue @@ -1,47 +1,18 @@ diff --git a/frontend/src/registry/screens.ts b/frontend/src/registry/screens.ts index ac70919f..7cee6062 100644 --- a/frontend/src/registry/screens.ts +++ b/frontend/src/registry/screens.ts @@ -1,89 +1,11 @@ /** * Central Screen Registry (KBX v60) - * Merge all feature screen definitions here + * Pages are routed in app/router.ts */ -import type { ScreenDefinition } from '@kbx/contracts' +// Screen registry is managed via router.ts +// KBX v60 pages: ShadowRunQueue, ModelList, ApprovalQueue +// All routes are registered in src/app/router.ts -// Import KBX v60 pages (registered routes) -const kbxPages: ScreenDefinition[] = [ - { - screenId: 'model-ops.shadow-run.queue', - title: 'Shadow Run Jobs', - module: 'ERP', - path: '/model-ops/shadow-run-jobs', - component: () => import('@features/shadow-run/pages/ShadowRunQueue.vue'), - permissions: ['model.view'], - template: 'T06', - help: { - title: 'Shadow Run Job Queue', - sections: [ - { - title: 'What is a Shadow Run?', - content: 'Monitor and manage shadow run validation jobs (252+ trading days)', - }, - ], - relatedScreens: [], - }, - }, - { - screenId: 'model-ops.models.master', - title: 'Models', - module: 'ERP', - path: '/model-ops/models-master', - component: () => import('@features/models/pages/ModelList.vue'), - permissions: ['model.view'], - template: 'T02', - help: { - title: 'Model Master-Detail', - sections: [ - { - title: 'Model Lifecycle', - content: 'Browse and manage trading models', - }, - ], - relatedScreens: [], - }, - }, - { - screenId: 'governance.approval.queue', - title: 'Approval Queue', - module: 'ERP', - path: '/governance/approvals', - component: () => import('@features/approval/pages/ApprovalQueue.vue'), - permissions: ['approval.review'], - template: 'T03', - help: { - title: 'Maker-Checker Approvals', - sections: [ - { - title: 'Review & Approve', - content: 'Review and approve model activation requests', - }, - ], - relatedScreens: [], - }, - }, -] - -/** - * Get all screens - */ -export function getAllScreens(): ScreenDefinition[] { - return kbxPages -} - -/** - * Screen index by ID - */ -export function buildScreenIndex(): Map { - const index = new Map() - getAllScreens().forEach(screen => { - index.set(screen.screenId, screen) - }) - return index -} - -// Export registry -export const screens = getAllScreens() -export const screenIndex = buildScreenIndex() +export const screens = [] +export const screenIndex = new Map() diff --git a/frontend/test-pages-fixed.mjs b/frontend/test-pages-fixed.mjs new file mode 100644 index 00000000..35e3268c --- /dev/null +++ b/frontend/test-pages-fixed.mjs @@ -0,0 +1,130 @@ +import { chromium } from 'playwright'; +import fs from 'fs'; +import path from 'path'; + +const BASE_URL = 'http://localhost:5174'; +const OUTPUT_DIR = './test-results'; + +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +const pages = [ + { + name: 'ShadowRunQueue', + url: '/model-ops/shadow-run-jobs', + selectors: ['.shadow-run-queue', '.stats', '.filters', '.jobs-list'] + }, + { + name: 'ModelList', + url: '/model-ops/models-master', + selectors: ['.model-list', '.filters', '.content', '.master-list'] + }, + { + name: 'ApprovalQueue', + url: '/governance/approvals', + selectors: ['.approval-queue', '.stats', '.filters', '.content'] + } +]; + +async function testPage(browser, pageConfig) { + console.log(`\nTesting: ${pageConfig.name}`); + const page = await browser.newPage(); + + const errors = []; + page.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + + page.on('response', res => { + if (res.status() >= 500) { + errors.push(`HTTP ${res.status()}: ${res.url()}`); + } + }); + + try { + const response = await page.goto(`${BASE_URL}${pageConfig.url}`, { + waitUntil: 'domcontentloaded', + timeout: 15000 + }); + + console.log(` Response: ${response.status()}`); + + // Wait for rendering + await page.waitForTimeout(2000); + + // Check DOM elements + let foundSelectors = 0; + for (const selector of pageConfig.selectors) { + const element = await page.locator(selector).first(); + const visible = await element.isVisible().catch(() => false); + if (visible) { + foundSelectors++; + console.log(` āœ… Found: ${selector}`); + } else { + console.log(` āŒ Missing: ${selector}`); + } + } + + // Screenshot + const screenshotPath = path.join(OUTPUT_DIR, `${pageConfig.name}.png`); + await page.screenshot({ path: screenshotPath, fullPage: true }); + console.log(` šŸ“ø Screenshot: ${screenshotPath}`); + + // Save HTML + const htmlPath = path.join(OUTPUT_DIR, `${pageConfig.name}.html`); + const html = await page.content(); + fs.writeFileSync(htmlPath, html); + console.log(` šŸ” DOM: ${htmlPath}`); + + const success = errors.length === 0 && foundSelectors >= pageConfig.selectors.length * 0.8; + console.log(` Result: ${success ? 'āœ… PASS' : 'āŒ FAIL'}`); + + if (errors.length > 0) { + console.log(` Errors: ${errors.join(', ')}`); + } + + await page.close(); + return { success, name: pageConfig.name, errors, selectors: foundSelectors }; + + } catch (err) { + console.log(` āŒ Error: ${err.message}`); + await page.close(); + return { success: false, name: pageConfig.name, errors: [err.message], selectors: 0 }; + } +} + +async function main() { + console.log('šŸš€ Playwright Page Tests\n'); + + const browser = await chromium.launch({ headless: true }); + const results = []; + + for (const pageConfig of pages) { + const result = await testPage(browser, pageConfig); + results.push(result); + } + + await browser.close(); + + // Summary + console.log('\n' + '='.repeat(50)); + console.log('SUMMARY'); + console.log('='.repeat(50)); + + for (const r of results) { + console.log(`${r.success ? 'āœ…' : 'āŒ'} ${r.name}`); + } + + const allPassed = results.every(r => r.success); + console.log('\n' + (allPassed ? 'āœ… ALL TESTS PASSED' : 'āŒ SOME TESTS FAILED')); + + process.exit(allPassed ? 0 : 1); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/frontend/test-pages.mjs b/frontend/test-pages.mjs new file mode 100644 index 00000000..e6840c74 --- /dev/null +++ b/frontend/test-pages.mjs @@ -0,0 +1,187 @@ +import { chromium } from 'playwright'; +import fs from 'fs'; +import path from 'path'; + +const BASE_URL = 'http://localhost:5174'; +const OUTPUT_DIR = './test-results'; + +// Create output directory +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +const pages = [ + { + name: 'Shadow Run Queue (T06)', + url: '/model-ops/shadow-run-jobs', + checks: [ + 'Shadow Run Jobs', + 'Pending', + 'Completed', + 'Failed', + 'KbxScreenFrame', + 'KbxQueueTemplate' + ] + }, + { + name: 'Models Master-Detail (T02)', + url: '/model-ops/models-master', + checks: [ + 'Models', + 'Model', + 'Performance Metrics', + 'PBO', + 'DSR', + 'KbxMasterTemplate' + ] + }, + { + name: 'Approval Queue (T03)', + url: '/governance/approvals', + checks: [ + 'Approval Queue', + 'Pending', + 'Approved', + 'Rejected', + 'Review & Approval', + 'KbxTransactionTemplate' + ] + } +]; + +async function testPage(browser, page) { + console.log(`\n${'='.repeat(60)}`); + console.log(`Testing: ${page.name}`); + console.log(`URL: ${BASE_URL}${page.url}`); + console.log('='.repeat(60)); + + const browserPage = await browser.newPage(); + + // Capture console errors + let errors = []; + browserPage.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + console.log(`āŒ Console Error: ${msg.text()}`); + } + }); + + // Capture page errors + let pageErrors = []; + browserPage.on('pageerror', err => { + pageErrors.push(err.toString()); + console.log(`āŒ Page Error: ${err.message}`); + }); + + try { + // Navigate to page + await browserPage.goto(`${BASE_URL}${page.url}`, { waitUntil: 'networkidle' }); + console.log('āœ… Page loaded'); + + // Wait for content + await browserPage.waitForTimeout(2000); + + // Check for expected content + let foundChecks = []; + for (const check of page.checks) { + const found = await browserPage.locator(`text="${check}"`).count() > 0 || + await browserPage.content().includes(check); + if (found) { + foundChecks.push(check); + console.log(`āœ… Found: "${check}"`); + } else { + console.log(`āŒ Missing: "${check}"`); + } + } + + // Get page title + const title = await browserPage.title(); + console.log(`šŸ“„ Title: ${title}`); + + // Check DOM structure + const html = await browserPage.content(); + const hasVueApp = html.includes('id="app"'); + const hasKbxComponents = html.includes('kbx-'); + console.log(`Vue App: ${hasVueApp ? 'āœ…' : 'āŒ'}`); + console.log(`KBX Components: ${hasKbxComponents ? 'āœ…' : 'āŒ'}`); + + // Screenshot + const screenshotPath = path.join(OUTPUT_DIR, `${page.name.replace(/\s+/g, '-').toLowerCase()}.png`); + await browserPage.screenshot({ path: screenshotPath, fullPage: true }); + console.log(`šŸ“ø Screenshot: ${screenshotPath}`); + + // Save DOM + const domPath = path.join(OUTPUT_DIR, `${page.name.replace(/\s+/g, '-').toLowerCase()}.html`); + fs.writeFileSync(domPath, html); + console.log(`šŸ” DOM saved: ${domPath}`); + + // Summary + const checksPassed = foundChecks.length; + const checksTotal = page.checks.length; + const passRate = Math.round((checksPassed / checksTotal) * 100); + console.log(`\nšŸ“Š Content Check: ${checksPassed}/${checksTotal} (${passRate}%)`); + console.log(`āŒ Errors: ${errors.length + pageErrors.length}`); + + await browserPage.close(); + + return { + success: errors.length === 0 && pageErrors.length === 0 && passRate >= 80, + name: page.name, + url: page.url, + errors: errors.concat(pageErrors), + contentChecks: { passed: checksPassed, total: checksTotal }, + screenshot: screenshotPath + }; + } catch (err) { + console.log(`āŒ Test Failed: ${err.message}`); + await browserPage.close(); + return { + success: false, + name: page.name, + url: page.url, + errors: [err.message], + contentChecks: { passed: 0, total: page.checks.length } + }; + } +} + +async function main() { + const browser = await chromium.launch(); + const results = []; + + console.log('\nšŸš€ Starting Playwright Tests\n'); + console.log(`Base URL: ${BASE_URL}`); + console.log(`Output: ${OUTPUT_DIR}\n`); + + for (const page of pages) { + const result = await testPage(browser, page); + results.push(result); + } + + await browser.close(); + + // Summary + console.log(`\n${'='.repeat(60)}`); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + + for (const result of results) { + const status = result.success ? 'āœ…' : 'āŒ'; + console.log(`\n${status} ${result.name}`); + console.log(` URL: ${result.url}`); + console.log(` Content: ${result.contentChecks.passed}/${result.contentChecks.total}`); + console.log(` Errors: ${result.errors.length}`); + if (result.errors.length > 0) { + result.errors.forEach(err => console.log(` - ${err}`)); + } + } + + const allPassed = results.every(r => r.success); + console.log(`\n${'='.repeat(60)}`); + console.log(allPassed ? 'āœ… ALL TESTS PASSED' : 'āŒ SOME TESTS FAILED'); + console.log('='.repeat(60)); + + process.exit(allPassed ? 0 : 1); +} + +main().catch(console.error);