31b36ba226
Consolidates KBX UI Boundary Governance framework with component manifest, screen recipe registry, AI component gate, and exception lifecycle validation. Evidence (evidence/V13-FE-005/*.log, 55+ files): - Full frontend regression: 70 files / 180 tests PASS - UI boundary gate: 37 files / 0 failures / 6 raw-color warnings (DEBT tracked) - Component manifest validation: 0 failures - Screen recipe governance: 0 failures - AI component gate: 17 feature files / 23 known exports / 0 failures - Accessibility E2E: 22 passed - Production build: PASS (>500 kB chunk warning V13-FE-038 DECISION_REQUIRED) - TypeCheck: PASS - KBX validators: All 5 PASS (failures=0) Added: 19 files (6 validator scripts, 6 test specs, 4 slice notes, 3 registries) Modified: 9 files (CI workflow, WBS tracker, E2E specs, FE setup, Layout, TS configs) Outstanding per V13-FE-005 note: AI prop-level validation, exception lifecycle, browser/visual/AT/performance evidence. No completion overclaim. AGENTS.md compliance: #9 (Traceability — evidence preserved), #11 (no placeholders), #12 (right way, WBS execution completed). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
209 lines
6.3 KiB
TypeScript
209 lines
6.3 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
|
|
test.describe('Models (KBX Foundation)', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to models list
|
|
await page.goto('/model-ops/models', {
|
|
waitUntil: 'networkidle',
|
|
})
|
|
|
|
// Wait for component to hydrate
|
|
await page.waitForTimeout(500)
|
|
})
|
|
|
|
test('should display models list with grid', async ({ page }) => {
|
|
// Check page title
|
|
const title = page.locator('h1')
|
|
await expect(title).toContainText('Model Management')
|
|
|
|
// Check grid visibility
|
|
const grid = page.locator('.ks-grid')
|
|
await expect(grid).toBeVisible()
|
|
|
|
// Check summary badges
|
|
const summaryItems = page.locator('.ks-list-page__footer > span')
|
|
await expect(summaryItems).toHaveCount(4)
|
|
})
|
|
|
|
test('should filter models by phase', async ({ page }) => {
|
|
// Get phase filter select
|
|
const phaseFilter = page.locator('select.phase-filter')
|
|
await expect(phaseFilter).toBeVisible()
|
|
|
|
// Select a phase
|
|
await phaseFilter.selectOption('Mature')
|
|
|
|
// Wait for filter
|
|
await page.waitForTimeout(300)
|
|
|
|
// Grid should still be visible
|
|
const grid = page.locator('.ks-grid')
|
|
await expect(grid).toBeVisible()
|
|
})
|
|
|
|
test('should filter models by active status', async ({ page }) => {
|
|
// Get active filter select
|
|
const activeFilter = page.locator('select.active-filter')
|
|
await expect(activeFilter).toBeVisible()
|
|
|
|
// Select active only
|
|
await activeFilter.selectOption('active')
|
|
|
|
// Wait for filter
|
|
await page.waitForTimeout(300)
|
|
|
|
// Grid should update
|
|
const grid = page.locator('.ks-grid')
|
|
await expect(grid).toBeVisible()
|
|
})
|
|
|
|
test('should navigate to model detail', async ({ page }) => {
|
|
// Wait for data
|
|
await page.waitForTimeout(500)
|
|
|
|
// Click first model row
|
|
const firstRow = page.locator('.ag-row').first()
|
|
await expect(firstRow).toBeVisible()
|
|
await firstRow.click()
|
|
|
|
// Should navigate to detail page
|
|
await page.waitForURL('**/models/*', { timeout: 5000 })
|
|
expect(page.url()).toMatch(/\/model-ops\/models\/.+/)
|
|
|
|
// Verify detail page structure
|
|
const detailHeader = page.locator('.detail-header h1')
|
|
await expect(detailHeader).toBeVisible()
|
|
})
|
|
|
|
test('should display model activation requirements', async ({ page }) => {
|
|
// Navigate to detail
|
|
const firstRow = page.locator('.ag-row').first()
|
|
await expect(firstRow).toBeVisible()
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
|
|
// Check requirements section
|
|
const requirementsSection = page.locator('.requirements-section')
|
|
await expect(requirementsSection).toBeVisible()
|
|
|
|
// Check requirement cards
|
|
const requirementCards = page.locator('.requirement-card')
|
|
await expect(requirementCards.first()).toBeVisible()
|
|
})
|
|
|
|
test('should display model lifecycle phases', async ({ page }) => {
|
|
// Navigate to detail
|
|
const firstRow = page.locator('.ag-row').first()
|
|
await expect(firstRow).toBeVisible()
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
|
|
// Check phase timeline
|
|
const phaseTimeline = page.locator('.phase-timeline')
|
|
await expect(phaseTimeline).toBeVisible()
|
|
|
|
// Check individual phase items
|
|
const phaseItems = page.locator('.phase-item')
|
|
await expect(phaseItems.first()).toBeVisible()
|
|
})
|
|
|
|
test('should display model configuration', async ({ page }) => {
|
|
// Navigate to detail
|
|
const firstRow = page.locator('.ag-row').first()
|
|
await expect(firstRow).toBeVisible()
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
|
|
// Check config section
|
|
const configSection = page.locator('.config-section')
|
|
await expect(configSection).toBeVisible()
|
|
|
|
// Check config items
|
|
const configItems = page.locator('.config-item')
|
|
await expect(configItems.first()).toBeVisible()
|
|
})
|
|
|
|
test('should display validation history table', async ({ page }) => {
|
|
// Navigate to detail
|
|
const firstRow = page.locator('.ag-row').first()
|
|
await expect(firstRow).toBeVisible()
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
|
|
// Check history table
|
|
const historySection = page.locator('.history-section')
|
|
await expect(historySection).toBeVisible()
|
|
|
|
// Check table rows
|
|
const tableRows = page.locator('.table-row')
|
|
await expect(tableRows.first()).toBeVisible()
|
|
})
|
|
|
|
test('should use F3 keyboard shortcut for search', async ({ page }) => {
|
|
const searchInput = page.locator('input[placeholder*="Search"]').first()
|
|
|
|
// Press F3
|
|
await page.keyboard.press('F3')
|
|
|
|
// Should trigger search
|
|
await page.waitForTimeout(300)
|
|
|
|
// Verify no error
|
|
await expect(page).not.toHaveTitle('Error')
|
|
})
|
|
|
|
test('should use Ctrl+N shortcut to create new model', async ({ page }) => {
|
|
// Press Ctrl+N
|
|
await page.keyboard.press('Control+N')
|
|
|
|
// Should navigate to new model page
|
|
await page.waitForTimeout(500)
|
|
|
|
// Verify navigation occurred
|
|
await expect(page).not.toHaveTitle('Error')
|
|
})
|
|
|
|
test('should show model status as active or inactive', async ({ page }) => {
|
|
// Navigate to detail
|
|
await page.waitForTimeout(500)
|
|
const firstRow = page.locator('tbody tr').first()
|
|
if (await firstRow.isVisible()) {
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
}
|
|
|
|
// Check status display
|
|
const statusDisplay = page.locator('[class*="active"]').first()
|
|
await expect(statusDisplay).toBeVisible()
|
|
})
|
|
|
|
test('should display quick filter badges', async ({ page }) => {
|
|
// Check for quick filters with badges
|
|
const quickFilters = page.locator('[class*="quick-filter"]')
|
|
await expect(quickFilters.first()).toBeVisible()
|
|
|
|
// Check for badge elements
|
|
const badges = page.locator('.badge')
|
|
if (await badges.count() > 0) {
|
|
await expect(badges.first()).toBeVisible()
|
|
}
|
|
})
|
|
|
|
test('should navigate back from detail to list', async ({ page }) => {
|
|
// Navigate to detail
|
|
await page.waitForTimeout(500)
|
|
const firstRow = page.locator('tbody tr').first()
|
|
if (await firstRow.isVisible()) {
|
|
await firstRow.click()
|
|
await page.waitForURL('**/models/*')
|
|
|
|
// Click back button (Escape key or Back button)
|
|
await page.keyboard.press('Escape')
|
|
|
|
// Should navigate back
|
|
await page.waitForURL('**/models$', { timeout: 5000 })
|
|
expect(page.url()).toMatch(/\/model-ops\/models$/)
|
|
}
|
|
})
|
|
})
|