feat: BDD Contract
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildApiCatalog, matchApiEndpointCandidates, matchApiEndpoints } from '../src/lib/api-docs.js'
|
||||
import { buildBddContract, renderBddContract } from '../src/lib/bdd.js'
|
||||
import { buildMaintenanceContract } from '../src/lib/maintenance.js'
|
||||
import { buildAppMap, buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../src/lib/html.js'
|
||||
|
||||
@@ -409,6 +410,130 @@ test('buildMaintenanceContract ignores non-vue guide text for primary entity', (
|
||||
assert.deepEqual(contract.capabilities, ['print'])
|
||||
})
|
||||
|
||||
test('buildBddContract creates traceable auth scenarios from page evidence', () => {
|
||||
const spec = buildSpec('/repo/prototype/portal/login.html', `
|
||||
<main>
|
||||
<h1>使用者登入</h1>
|
||||
<form>
|
||||
<label>帳號</label><input name="account" required>
|
||||
<label>密碼</label><input name="password" type="password" required>
|
||||
<button>登入</button>
|
||||
</form>
|
||||
</main>
|
||||
`)
|
||||
spec.maintenanceContract = {
|
||||
pageKind: 'login',
|
||||
capabilities: ['custom'],
|
||||
dataModel: { primaryEntity: 'Login' },
|
||||
businessRules: { validationRules: [{ ruleType: 'required', text: '帳號與密碼必填' }] },
|
||||
rowActions: []
|
||||
}
|
||||
spec.prototypeGuide = {
|
||||
description: '使用者登入',
|
||||
checklist: ['登入失敗時顯示錯誤訊息'],
|
||||
flowRefs: [{ title: '登入流程', nodes: [{ nodeType: 'submit', action: '登入' }] }]
|
||||
}
|
||||
spec.apiContract = {
|
||||
endpoints: [{ method: 'POST', path: '/api/auth/login', usage: 'submit', description: '使用者登入' }]
|
||||
}
|
||||
|
||||
const contract = buildBddContract(spec)
|
||||
const markdown = renderBddContract(spec, contract)
|
||||
|
||||
assert.equal(contract.feature, '使用者登入')
|
||||
assert.equal(contract.scenarios[0].name, '使用者輸入正確帳密登入成功')
|
||||
assert.deepEqual(contract.scenarios[0].given, ['使用者已在使用者登入頁'])
|
||||
assert.deepEqual(contract.scenarios[0].when, ['使用者輸入正確的帳號與密碼並執行「登入」'])
|
||||
assert.ok(contract.scenarios[0].then.includes('系統應送出 POST /api/auth/login'))
|
||||
assert.deepEqual(contract.scenarios[0].evidence.fields, ['帳號', '密碼'])
|
||||
assert.deepEqual(contract.scenarios[0].evidence.checklist, ['登入失敗時顯示錯誤訊息'])
|
||||
assert.equal(contract.requiresHumanReview, true)
|
||||
assert.match(markdown, /Feature: 使用者登入/)
|
||||
assert.match(markdown, /Scenario: 使用者輸入正確帳密登入成功/)
|
||||
assert.match(markdown, /Then 系統應送出 POST \/api\/auth\/login/)
|
||||
assert.match(markdown, /## Evidence/)
|
||||
})
|
||||
|
||||
test('buildBddContract creates query and row action scenarios with review warnings', () => {
|
||||
const spec = buildSpec('/repo/prototype/venue/applications-list.html', `
|
||||
<main>
|
||||
<h1>我的申請紀錄</h1>
|
||||
<form><label>狀態</label><select name="status"><option>審核中</option></select><button>查詢</button></form>
|
||||
<table>
|
||||
<tr><th>申請單號</th><th>狀態</th><th>操作</th></tr>
|
||||
<tr><td>A001</td><td>審核中</td><td><input type="button" value="修改"><input type="button" value="刪除"></td></tr>
|
||||
</table>
|
||||
</main>
|
||||
`)
|
||||
spec.maintenanceContract = {
|
||||
pageKind: 'maintenance',
|
||||
capabilities: ['search', 'edit', 'delete'],
|
||||
dataModel: { primaryEntity: 'ApplicationsList' },
|
||||
businessRules: { validationRules: [], statusRules: [{ ruleType: 'enabled-when-status', field: 'aprvYn', value: 'Z', text: 'aprvYn=Z 才可修改刪除' }] },
|
||||
rowActions: [
|
||||
{ label: '修改', actionType: 'edit', enabledWhen: "aprvYn === 'Z'" },
|
||||
{ label: '刪除', actionType: 'delete', enabledWhen: "aprvYn === 'Z'" }
|
||||
]
|
||||
}
|
||||
spec.apiContract = {
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/api/applications', usage: 'query', description: '查詢申請紀錄' },
|
||||
{ method: 'DELETE', path: '/api/applications/{id}', usage: 'delete', description: '刪除申請' }
|
||||
]
|
||||
}
|
||||
|
||||
const contract = buildBddContract(spec)
|
||||
|
||||
assert.equal(contract.scenarios[0].type, 'query')
|
||||
assert.deepEqual(contract.scenarios[0].then, ['系統應送出 GET /api/applications', '顯示符合條件的查詢結果'])
|
||||
assert.ok(contract.scenarios.some((scenario) => scenario.name === '符合狀態條件時可以修改資料列'))
|
||||
assert.ok(contract.scenarios.some((scenario) => scenario.name === '符合狀態條件時可以刪除資料列'))
|
||||
assert.ok(contract.warnings.some((warning) => warning.includes('Then')))
|
||||
})
|
||||
|
||||
test('buildBddContract reports scenario candidates and uncovered evidence', () => {
|
||||
const spec = buildSpec('/repo/prototype/portal/login.html', `
|
||||
<main>
|
||||
<h1>使用者登入</h1>
|
||||
<form>
|
||||
<label>帳號</label><input name="account" required>
|
||||
<label>密碼</label><input name="password" type="password" required>
|
||||
<button>登入</button>
|
||||
<button>忘記密碼</button>
|
||||
</form>
|
||||
</main>
|
||||
`)
|
||||
spec.maintenanceContract = {
|
||||
pageKind: 'login',
|
||||
capabilities: ['custom'],
|
||||
dataModel: { primaryEntity: 'Login' },
|
||||
businessRules: { validationRules: [{ ruleType: 'required', text: '帳號與密碼必填' }] },
|
||||
rowActions: []
|
||||
}
|
||||
spec.prototypeGuide = {
|
||||
description: '使用者登入',
|
||||
checklist: ['登入失敗時顯示錯誤訊息', '連續錯誤三次鎖定帳號'],
|
||||
flowRefs: [{ title: '登入失敗流程', nodes: [{ nodeType: 'error', action: '顯示錯誤訊息' }] }]
|
||||
}
|
||||
spec.apiContract = {
|
||||
endpoints: [{ method: 'POST', path: '/api/auth/login', usage: 'submit', description: '使用者登入' }],
|
||||
errorHandling: { formats: [{ status: 401, description: '帳號或密碼錯誤' }] }
|
||||
}
|
||||
|
||||
const contract = buildBddContract(spec)
|
||||
const markdown = renderBddContract(spec, contract)
|
||||
|
||||
assert.ok(contract.candidateScenarios.some((candidate) => candidate.type === 'error-path'))
|
||||
assert.ok(contract.candidateScenarios.some((candidate) => candidate.source === 'api-error'))
|
||||
assert.ok(contract.uncoveredEvidence.some((item) => item.source === 'prototypeGuide.checklist' && item.text === '連續錯誤三次鎖定帳號'))
|
||||
assert.ok(contract.uncoveredEvidence.some((item) => item.source === 'pageContract.actions' && item.text === '忘記密碼'))
|
||||
assert.equal(contract.requiresHumanReview, true)
|
||||
assert.match(markdown, /## Candidate Scenarios/)
|
||||
assert.match(markdown, /登入失敗時顯示錯誤訊息/)
|
||||
assert.match(markdown, /## Uncovered Evidence/)
|
||||
assert.match(markdown, /連續錯誤三次鎖定帳號/)
|
||||
})
|
||||
|
||||
function buildSpec(source, html) {
|
||||
const regions = extractRegions(html)
|
||||
const domSummary = summarizeHtml(html)
|
||||
|
||||
Reference in New Issue
Block a user