Files
html-transform/test/html.test.js
T
2026-05-12 11:03:46 +08:00

275 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { buildApiCatalog, matchApiEndpoints } from '../src/lib/api-docs.js'
import { buildMaintenanceContract } from '../src/lib/maintenance.js'
import { buildAppMap, buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../src/lib/html.js'
test('summarizeHtml extracts user-visible contract evidence', () => {
const summary = summarizeHtml(`
<title>Orders</title>
<main>
<h1>Orders</h1>
<form><label>Email</label><input name="email" required><button>Save</button></form>
</main>
`)
assert.equal(summary.title, 'Orders')
assert.deepEqual(summary.labels, ['Email'])
assert.deepEqual(summary.buttons, ['Save'])
assert.equal(summary.inputs[0].name, 'email')
assert.equal(summary.inputs[0].required, true)
})
test('extractRegions falls back to a single page region', () => {
const regions = extractRegions('<h1>Hello</h1><p>World</p>')
assert.equal(regions.length, 1)
assert.equal(regions[0].id, 'page-1')
})
test('inferRegionSpec maps forms to VForm', () => {
const [region] = extractRegions('<main><form><input name="q"><button>Search</button></form></main>')
const spec = inferRegionSpec(region)
assert.equal(spec.vuetifyComponent, '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('buildPageContract infers legacy table field labels and action semantics', () => {
const html = `
<main>
<form>
<table>
<tr><th>場地</th><td><select name="rom_id"><option>R001 大禮堂</option></select></td></tr>
<tr><th>查詢起日</th><td><input name="startDate" maxlength="7"></td></tr>
</table>
<input type="button" value="查詢">
</form>
<table><tr><th>申請單號</th><th>操作</th></tr><tr><td>A001</td><td><input type="button" value="刪除"></td></tr></table>
</main>
`
const contract = buildPageContract({
page: 'query-room.html',
source: '/prototype/query-room.html',
html,
regions: extractRegions(html),
domSummary: summarizeHtml(html),
screenshotPath: null
})
assert.equal(contract.forms[0].fields[0].label, '場地')
assert.deepEqual(contract.forms[0].fields[0].options, ['R001 大禮堂'])
assert.equal(contract.forms[0].fields[1].maxLength, '7')
assert.equal(contract.actions.find((action) => action.label === '查詢').actionType, 'search')
assert.equal(contract.actions.find((action) => action.label === '刪除').scope, 'rowAction')
assert.equal(contract.tables[0].role, 'searchTable')
assert.equal(contract.tables[1].role, 'resultTable')
})
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')
})
test('buildAppMap enriches routes with prototype markdown guide entries', () => {
const prototypeDir = '/repo/prototype'
const guide = parsePrototypeGuide('venue.md', `
# Venue 雛型導覽
| 雛型檔 | 舊 JSP 來源 | 舊 PB NVO 來源 | 對應 Vue viewM9 |
| --- | --- | --- | --- |
| [\`query-room.html\`](venue/query-room.html) | \`zte_pro/zte451_02.jsp\` + \`zte451_02_1.jsp\` | \`n_zte451.of_zte451_02\` / \`of_zte451_02_1\` | \`RoomQueryView.vue\` |
`)
const appMap = buildAppMapWithGuides([
buildSpec('/repo/prototype/venue/query-room.html', '<main><h1>全校場地查詢</h1><button>查詢</button></main>')
], prototypeDir, [guide])
const route = appMap.routes[0]
assert.equal(appMap.guideSources[0].source, 'venue.md')
assert.equal(route.evidence.prototypeGuide, 'venue.md')
assert.equal(route.guide.legacyJsp, 'zte_pro/zte451_02.jsp + zte451_02_1.jsp')
assert.equal(route.guide.legacyPb, 'n_zte451.of_zte451_02 / of_zte451_02_1')
assert.equal(route.guide.targetView, 'RoomQueryView.vue')
})
test('parsePrototypeGuide attaches checklist items to guide entries', () => {
const guide = parsePrototypeGuide('venue.md', `
# Venue
| 雛型檔 | 舊 JSP 來源 | 對應 Vue view |
| --- | --- | --- |
| [\`apply-room.html\`](venue/apply-room.html) | \`legacy/apply.jsp\` | \`RoomApplyView.vue\` |
### apply-room.html vs legacy
- [ ] 活動名稱必填
- [ ] 使用節次:14 個 checkbox
`)
assert.deepEqual(guide.entries[0].checklist, ['活動名稱必填', '使用節次:14 個 checkbox'])
})
test('buildApiCatalog parses generic markdown API docs and matches routes', () => {
const catalog = buildApiCatalog([
{
source: 'apps/backend/API.md',
markdown: `
## Orders Module
| 方法 | 路徑 | 說明 | 授權 |
| --- | --- | --- | --- |
| GET | \`/api/v1/orders\` | 查詢訂單 | Authorize |
`
},
{
source: 'apps/backend/API_Manual.md',
markdown: `
## 5. Orders API
### 5.1 新增訂單
\`\`\`text
POST /api/v1/orders
\`\`\`
Request
\`\`\`json
{ "orderName": "Demo", "items": [{ "sku": "A001", "qty": 1 }] }
\`\`\`
Response
\`\`\`json
{ "orderNo": "O001" }
\`\`\`
欄位規則:
| 欄位 | 規則 |
| --- | --- |
| \`orderName\` | 必填 |
`
}
])
assert.ok(catalog.endpoints.some((endpoint) => endpoint.id === 'POST /api/v1/orders'))
assert.equal(catalog.fieldRules[0].field, 'orderName')
const matches = matchApiEndpoints({
prototype: 'orders/apply.html',
page: 'apply.html',
module: 'orders',
title: '訂單申請',
guide: { targetView: 'OrderApplyView.vue' },
evidence: { actions: ['存檔'], textSamples: ['訂單'] }
}, catalog)
assert.equal(matches[0].path, '/api/v1/orders')
})
test('buildMaintenanceContract recommends generic templates from evidence', () => {
const spec = buildSpec('/repo/prototype/orders/apply.html', `
<main>
<h1>訂單申請</h1>
<form>
<table>
<tr><th>訂單名稱</th><td><input name="orderName"></td></tr>
<tr><th>明細</th><td><select name="items"><option>A001</option></select></td></tr>
</table>
<input type="button" value="存檔">
</form>
</main>
`)
const route = {
kind: 'feature-page',
prototype: 'orders/apply.html',
page: 'apply.html',
guide: { targetView: 'OrderApplyView.vue' }
}
const contract = buildMaintenanceContract({
route,
spec,
apiMatches: [{ method: 'POST', path: '/api/v1/orders', description: '新增訂單' }]
})
assert.equal(contract.pageKind, 'application')
assert.equal(contract.recommendedTemplate, 'master-detail-c')
assert.equal(contract.dataModel.primaryEntity, 'OrderApply')
})
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`
})
}
}