feat: renew mvp
This commit is contained in:
+42
-19
@@ -10,11 +10,9 @@ import { promisify } from 'node:util'
|
||||
const exec = promisify(execFile)
|
||||
const cli = new URL('../src/cli.js', import.meta.url).pathname
|
||||
|
||||
test('CLI runs scan, plan, run, diff, and verify against one prototype', async () => {
|
||||
test('CLI runs doctor and scan against one prototype', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'ht-e2e-'))
|
||||
await mkdir(join(cwd, 'packages/prototype'), { recursive: true })
|
||||
await mkdir(join(cwd, 'packages/web/src'), { recursive: true })
|
||||
await mkdir(join(cwd, 'packages/api'), { recursive: true })
|
||||
await writeFile(join(cwd, 'packages/prototype/index.html'), `
|
||||
<main>
|
||||
<h1>Customer Portal</h1>
|
||||
@@ -25,24 +23,49 @@ test('CLI runs scan, plan, run, diff, and verify against one prototype', async (
|
||||
</form>
|
||||
</main>
|
||||
`)
|
||||
await writeFile(join(cwd, 'packages/web/src/App.vue'), '<template><v-app /></template>')
|
||||
await writeFile(join(cwd, 'packages/api/openapi.json'), '{"openapi":"3.0.0","paths":{}}')
|
||||
await writeFile(join(cwd, 'ht.config.js'), `
|
||||
export default {
|
||||
plan: { interactiveReview: false },
|
||||
project: { qualityCommands: {} }
|
||||
}
|
||||
`)
|
||||
|
||||
for (const command of ['scan', 'plan', 'run', 'diff', 'verify']) {
|
||||
await exec('node', [cli, command], { cwd })
|
||||
}
|
||||
const doctor = await exec('node', [cli, 'doctor'], { cwd })
|
||||
await exec('node', [cli, 'scan'], { cwd })
|
||||
|
||||
const component = await readFile(join(cwd, 'packages/result/index/main-1.vue'), 'utf8')
|
||||
const report = JSON.parse(await readFile(join(cwd, '.ht/verify/verification-report.json'), 'utf8'))
|
||||
const contract = await readFile(join(cwd, '.ht/spec/index.ui-contract.md'), 'utf8')
|
||||
const spec = JSON.parse(await readFile(join(cwd, '.ht/spec/index.spec.json'), 'utf8'))
|
||||
const validation = JSON.parse(await readFile(join(cwd, '.ht/spec/index.validation.json'), 'utf8'))
|
||||
const appMap = JSON.parse(await readFile(join(cwd, '.ht/app-map.json'), 'utf8'))
|
||||
|
||||
assert.match(component, /Customer Portal/)
|
||||
assert.match(component, /v-text-field/)
|
||||
assert.equal(report.domChecks.status, 'passed')
|
||||
assert.match(doctor.stdout, /ok prototype directory/)
|
||||
assert.match(contract, /Customer Portal/)
|
||||
assert.equal(spec.pageContract.title, null)
|
||||
assert.deepEqual(spec.pageContract.forms[0].fields[0], {
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
type: 'input',
|
||||
required: true
|
||||
})
|
||||
assert.equal(validation.requiresHumanReview, false)
|
||||
assert.equal(appMap.routes[0].prototype, 'index.html')
|
||||
assert.equal(appMap.routes[0].kind, 'feature-page')
|
||||
assert.equal(appMap.routes[0].layout, 'template-app')
|
||||
})
|
||||
|
||||
test('CLI help only exposes MVP commands', async () => {
|
||||
const result = await exec('node', [cli, 'help'])
|
||||
|
||||
assert.match(result.stdout, /doctor/)
|
||||
assert.match(result.stdout, /scan/)
|
||||
assert.doesNotMatch(result.stdout, /plan/)
|
||||
assert.doesNotMatch(result.stdout, /run/)
|
||||
assert.doesNotMatch(result.stdout, /diff/)
|
||||
assert.doesNotMatch(result.stdout, /verify/)
|
||||
})
|
||||
|
||||
test('CLI rejects removed pipeline commands', async () => {
|
||||
await assert.rejects(
|
||||
exec('node', [cli, 'plan']),
|
||||
(error) => {
|
||||
assert.equal(error.code, 1)
|
||||
assert.match(error.stdout, /doctor/)
|
||||
assert.match(error.stdout, /scan/)
|
||||
return true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
+3
-6
@@ -11,16 +11,13 @@ test('loadConfig supports ht.config.ts defineConfig shape', async () => {
|
||||
import { defineConfig } from 'html-transform'
|
||||
export default defineConfig({
|
||||
prototype: './proto',
|
||||
output: './out',
|
||||
plan: { interactiveReview: true }
|
||||
vision: { captureStates: ['default'] }
|
||||
})
|
||||
`)
|
||||
|
||||
const config = await loadConfig(cwd)
|
||||
|
||||
assert.equal(config.prototypeDir, join(cwd, 'proto'))
|
||||
assert.equal(config.outputDir, join(cwd, 'out'))
|
||||
assert.equal(config.plan.interactiveReview, true)
|
||||
assert.equal(config.project.qualityCommands.lint, 'pnpm lint')
|
||||
assert.equal(config.htDir, join(cwd, '.ht'))
|
||||
assert.deepEqual(config.vision.captureStates, ['default'])
|
||||
})
|
||||
|
||||
|
||||
+82
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { extractRegions, inferRegionSpec, summarizeHtml } from '../src/lib/html.js'
|
||||
import { buildAppMap, buildPageContract, extractRegions, inferRegionSpec, summarizeHtml, validatePageContract } from '../src/lib/html.js'
|
||||
|
||||
test('summarizeHtml extracts user-visible contract evidence', () => {
|
||||
const summary = summarizeHtml(`
|
||||
@@ -33,3 +33,84 @@ test('inferRegionSpec maps forms to VForm', () => {
|
||||
assert.deepEqual(spec.uiContract.primaryActions, ['Search'])
|
||||
})
|
||||
|
||||
test('buildPageContract creates page-level UI contract', () => {
|
||||
const html = `
|
||||
<main>
|
||||
<h1>Orders</h1>
|
||||
<form><label>Email</label><input name="email" required><button>Search</button></form>
|
||||
<table><thead><tr><th>Status</th></tr></thead><tbody><tr><td>Pending</td></tr></tbody></table>
|
||||
</main>
|
||||
`
|
||||
const regions = extractRegions(html)
|
||||
const domSummary = summarizeHtml(html)
|
||||
const contract = buildPageContract({
|
||||
page: 'orders.html',
|
||||
source: '/prototype/orders.html',
|
||||
html,
|
||||
regions,
|
||||
domSummary,
|
||||
screenshotPath: '.ht/cache/prototype/orders/desktop-default.png'
|
||||
})
|
||||
|
||||
assert.equal(contract.page, 'orders.html')
|
||||
assert.equal(contract.forms[0].fields[0].required, true)
|
||||
assert.deepEqual(contract.tables[0].headers, ['Status'])
|
||||
assert.ok(contract.vuetifyComponents.includes('VTable'))
|
||||
})
|
||||
|
||||
test('validatePageContract reports evidence mismatches', () => {
|
||||
const report = validatePageContract({
|
||||
textSamples: ['Missing'],
|
||||
actions: [{ label: 'Save' }],
|
||||
forms: [],
|
||||
vuetifyComponents: ['VContainer']
|
||||
}, {
|
||||
textSamples: ['Orders'],
|
||||
buttons: ['Search'],
|
||||
labels: []
|
||||
})
|
||||
|
||||
assert.equal(report.requiresHumanReview, true)
|
||||
assert.match(report.warnings.join('\n'), /Missing/)
|
||||
assert.match(report.warnings.join('\n'), /Save/)
|
||||
})
|
||||
|
||||
test('buildAppMap classifies auth, shell references, and feature pages', () => {
|
||||
const prototypeDir = '/repo/prototype'
|
||||
const specs = [
|
||||
buildSpec('/repo/prototype/portal/login.html', '<main><input type="password" name="pwd"><button>登入</button></main>'),
|
||||
buildSpec('/repo/prototype/portal/app-layout.html', '<main><button>隱藏選單</button><button>登 出</button></main>'),
|
||||
buildSpec('/repo/prototype/venue/applications-list.html', '<main><h1>我的申請紀錄</h1><table><tr><th>申請單號</th></tr></table><button>查詢</button></main>')
|
||||
]
|
||||
|
||||
const appMap = buildAppMap(specs, prototypeDir)
|
||||
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'portal/login.html').kind, 'auth')
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'portal/login.html').layout, 'template-auth')
|
||||
assert.equal(buildAppMap([
|
||||
buildSpec('/repo/prototype/portal/forget-password.html', '<main><h1>忘記密碼</h1><input name="idno"><button>發送至信箱</button></main>')
|
||||
], prototypeDir).routes[0].targetRole, 'forgot-password')
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'portal/app-layout.html').kind, 'legacy-shell-reference')
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'portal/app-layout.html').usePrototypeContent, false)
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'venue/applications-list.html').kind, 'feature-page')
|
||||
assert.equal(appMap.routes.find((route) => route.prototype === 'venue/applications-list.html').layout, 'template-app')
|
||||
assert.equal(appMap.modules.find((module) => module.name === 'venue').kind, 'feature-module')
|
||||
})
|
||||
|
||||
function buildSpec(source, html) {
|
||||
const regions = extractRegions(html)
|
||||
const domSummary = summarizeHtml(html)
|
||||
const page = source.split('/').at(-1)
|
||||
return {
|
||||
source,
|
||||
page,
|
||||
pageContract: buildPageContract({
|
||||
page,
|
||||
source,
|
||||
html,
|
||||
regions,
|
||||
domSummary,
|
||||
screenshotPath: `.ht/cache/prototype/${page.replace(/\.html$/, '')}/desktop-default.png`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user