feat: 增進 app-map
This commit is contained in:
@@ -4,7 +4,7 @@ HTML Transform 是 prototype evidence 工具。現行 MVP 只做兩件事:
|
|||||||
|
|
||||||
- `doctor`:檢查執行 `scan` 需要的前置條件。
|
- `doctor`:檢查執行 `scan` 需要的前置條件。
|
||||||
- `scan`:讀取 HTML prototype,產生瀏覽器證據與頁面級 UI Contract。
|
- `scan`:讀取 HTML prototype,產生瀏覽器證據與頁面級 UI Contract。
|
||||||
- `app-map.json`:推論每個 prototype 的頁面角色與 layout 使用策略。
|
- `app-map.json`:推論每個 prototype 的頁面角色與 layout 使用策略,並用 `prototype/*.md` 的人工 domain guide 輔助補上舊系統對照。
|
||||||
|
|
||||||
不提供 `plan`、`run`、`diff`、`verify`、`go`、`status`。這些舊骨架已移除,避免把尚未完成的自動化流程誤認成可用功能。
|
不提供 `plan`、`run`、`diff`、`verify`、`go`、`status`。這些舊骨架已移除,避免把尚未完成的自動化流程誤認成可用功能。
|
||||||
|
|
||||||
@@ -83,27 +83,23 @@ pnpm --filter html-transform exec playwright-cli install-browser chromium
|
|||||||
|
|
||||||
## 設定檔
|
## 設定檔
|
||||||
|
|
||||||
沒有設定檔時,預設讀取:
|
設定檔不是工具執行的絕對必要條件;沒有設定檔時,工具會使用內建預設並讀取:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
packages/prototype/
|
packages/prototype/
|
||||||
```
|
```
|
||||||
|
|
||||||
本 repo 根目錄目前使用:
|
本 repo 的 HTML prototype 位於根目錄 `./prototype`,不是 `./packages/prototype`。因此從 repo 根目錄執行 `node packages/html-transform/src/cli.js scan` 時,需要根目錄 `ht.config.mjs` 覆寫 `prototype` 路徑。
|
||||||
|
|
||||||
|
本 repo 根目錄目前使用最小設定:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default {
|
export default {
|
||||||
prototype: './prototype',
|
prototype: './prototype'
|
||||||
frontend: './apps/frontend/hwu-re',
|
|
||||||
backend: './apps/backend',
|
|
||||||
output: './output',
|
|
||||||
plan: {
|
|
||||||
interactiveReview: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
目前 MVP 只使用 `prototype`。其他欄位可以留著給外部工作流參考,但 `packages/html-transform` 不會執行 frontend/backend/output 相關步驟。
|
目前 MVP 只使用 `prototype`。`scan` 會讀取其中的 HTML prototype,也會讀取 `prototype/*.md` 作為 app-map 的輔助 domain guide。`frontend`、`backend`、`output`、`plan` 等欄位不會被 `doctor` 或 `scan` 使用,不需要為本 MVP 加入設定檔。
|
||||||
|
|
||||||
## Doctor
|
## Doctor
|
||||||
|
|
||||||
@@ -135,7 +131,8 @@ node packages/html-transform/src/cli.js scan
|
|||||||
- 記錄 resource failure 與 console error/warning。
|
- 記錄 resource failure 與 console error/warning。
|
||||||
- 建立頁面級 `pageContract`。
|
- 建立頁面級 `pageContract`。
|
||||||
- 驗證 contract 與 DOM evidence 是否明顯衝突。
|
- 驗證 contract 與 DOM evidence 是否明顯衝突。
|
||||||
- 產出 `.ht/app-map.json`,供通用 prompt 判斷 auth、legacy shell、feature page 與 layout 策略。
|
- 讀取 `prototype/*.md` 的對照表,擷取 prototype 檔、舊 JSP、舊 PB、對應 Vue view 或功能描述。
|
||||||
|
- 產出 `.ht/app-map.json`,供通用 prompt 判斷 auth、legacy shell、feature page、layout 策略與舊系統對照。
|
||||||
|
|
||||||
## 產物
|
## 產物
|
||||||
|
|
||||||
@@ -161,6 +158,26 @@ node packages/html-transform/src/cli.js scan
|
|||||||
|
|
||||||
`.ht/app-map.json` 是跨頁面的應用結構推論。通用 prompt 應先讀它,再決定每個 prototype 是 `auth`、`legacy-shell-reference`、`feature-page` 或其他角色。MVP 固定策略是 template layout/style 優先,prototype 只提供內容與功能證據。
|
`.ht/app-map.json` 是跨頁面的應用結構推論。通用 prompt 應先讀它,再決定每個 prototype 是 `auth`、`legacy-shell-reference`、`feature-page` 或其他角色。MVP 固定策略是 template layout/style 優先,prototype 只提供內容與功能證據。
|
||||||
|
|
||||||
|
若 `prototype/*.md` 內有 markdown table 對照 prototype HTML,例如 `venue/query-room.html`,`scan` 會把匹配結果寫進 route:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prototype": "venue/query-room.html",
|
||||||
|
"guide": {
|
||||||
|
"source": "venue.md",
|
||||||
|
"legacyJsp": "zte_pro/zte451_02.jsp + zte451_02_1.jsp",
|
||||||
|
"legacyPb": "n_zte451.of_zte451_02 / of_zte451_02_1",
|
||||||
|
"targetView": "RoomQueryView.vue",
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"prototypeGuide": "venue.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
這些 guide 欄位只輔助 route 與舊系統對照理解;HTML capture、DOM summary、UI contract 與 screenshot 仍是畫面內容的主要 evidence。
|
||||||
|
|
||||||
## 驗證
|
## 驗證
|
||||||
|
|
||||||
修改本 package 後至少執行:
|
修改本 package 後至少執行:
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Improve `.ht` Layout Evidence
|
||||||
|
|
||||||
|
Current `scan` evidence is useful for finding page structure, labels, inputs, buttons, tables, screenshots, and app-map hints, but it is still too weak as an implementation constraint for visual layout.
|
||||||
|
|
||||||
|
Observed problem:
|
||||||
|
|
||||||
|
- Generated Vue/Vuetify pages can preserve fields, buttons, routes, and APIs while drifting far from prototype layout.
|
||||||
|
- Reports may pass pages because functional elements exist, even when form density, alignment, spacing, table structure, and first-screen information density are wrong.
|
||||||
|
- `desktop-default.png` contains this evidence visually, but the generated JSON/Markdown artifacts do not expose enough layout metrics for deterministic checks.
|
||||||
|
|
||||||
|
Future work:
|
||||||
|
|
||||||
|
- Capture bounding boxes for important visible elements:
|
||||||
|
- headings
|
||||||
|
- labels
|
||||||
|
- inputs/selects/textareas
|
||||||
|
- buttons/links
|
||||||
|
- tables
|
||||||
|
- section containers
|
||||||
|
- Group form fields into likely rows and columns using bounding boxes.
|
||||||
|
- Detect table/list layout:
|
||||||
|
- table count
|
||||||
|
- table order
|
||||||
|
- header labels
|
||||||
|
- whether multiple tables are stacked vertically
|
||||||
|
- Capture first-viewport density:
|
||||||
|
- visible section count
|
||||||
|
- visible form field count
|
||||||
|
- visible table/header count
|
||||||
|
- approximate occupied content area
|
||||||
|
- Emit layout hints into `.ht/spec/{page}.spec.json`, for example:
|
||||||
|
- `layoutEvidence.sections`
|
||||||
|
- `layoutEvidence.formRows`
|
||||||
|
- `layoutEvidence.tables`
|
||||||
|
- `layoutEvidence.primaryActions`
|
||||||
|
- `layoutEvidence.firstViewport`
|
||||||
|
- Render layout hints into `.ht/spec/{page}.ui-contract.md` so LLMs see layout constraints without manually inferring everything from screenshots.
|
||||||
|
- Add validation warnings for likely layout-critical structures:
|
||||||
|
- stacked tables
|
||||||
|
- dense row-based forms
|
||||||
|
- query toolbar with adjacent submit button
|
||||||
|
- multi-row detail tables
|
||||||
|
|
||||||
|
Non-goals:
|
||||||
|
|
||||||
|
- Do not require pixel-perfect HTML/JSP reproduction.
|
||||||
|
- Do not copy legacy CSS.
|
||||||
|
- Do not encode exact colors/fonts as hard constraints unless needed for functional recognition.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- Preserve information architecture, form density, field alignment, table/list relationships, and operation flow while still allowing a modern Vue/Vuetify implementation.
|
||||||
@@ -194,10 +194,16 @@ export function validatePageContract(contract, evidence = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildAppMap(layoutSpecs, prototypeDir) {
|
export function buildAppMap(layoutSpecs, prototypeDir) {
|
||||||
|
return buildAppMapWithGuides(layoutSpecs, prototypeDir, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAppMapWithGuides(layoutSpecs, prototypeDir, prototypeGuides = []) {
|
||||||
|
const guideIndex = buildPrototypeGuideIndex(prototypeGuides)
|
||||||
const routes = layoutSpecs.map((spec) => {
|
const routes = layoutSpecs.map((spec) => {
|
||||||
const relativeSource = spec.source.startsWith(prototypeDir)
|
const relativeSource = spec.source.startsWith(prototypeDir)
|
||||||
? spec.source.slice(prototypeDir.length).replace(/^\/+/, '')
|
? spec.source.slice(prototypeDir.length).replace(/^\/+/, '')
|
||||||
: spec.source
|
: spec.source
|
||||||
|
const guideEntry = guideIndex.get(relativeSource)
|
||||||
const route = inferRoute(spec, relativeSource)
|
const route = inferRoute(spec, relativeSource)
|
||||||
return {
|
return {
|
||||||
prototype: relativeSource,
|
prototype: relativeSource,
|
||||||
@@ -210,9 +216,17 @@ export function buildAppMap(layoutSpecs, prototypeDir) {
|
|||||||
usePrototypeStyle: false,
|
usePrototypeStyle: false,
|
||||||
usePrototypeContent: route.usePrototypeContent,
|
usePrototypeContent: route.usePrototypeContent,
|
||||||
routeHint: route.routeHint,
|
routeHint: route.routeHint,
|
||||||
|
guide: guideEntry ? {
|
||||||
|
source: guideEntry.source,
|
||||||
|
legacyJsp: guideEntry.legacyJsp,
|
||||||
|
legacyPb: guideEntry.legacyPb,
|
||||||
|
targetView: guideEntry.targetView,
|
||||||
|
description: guideEntry.description
|
||||||
|
} : null,
|
||||||
evidence: {
|
evidence: {
|
||||||
uiContract: `.ht/spec/${relativeSource.replace(/\.html$/, '.ui-contract.md')}`,
|
uiContract: `.ht/spec/${relativeSource.replace(/\.html$/, '.ui-contract.md')}`,
|
||||||
spec: `.ht/spec/${relativeSource.replace(/\.html$/, '.spec.json')}`,
|
spec: `.ht/spec/${relativeSource.replace(/\.html$/, '.spec.json')}`,
|
||||||
|
prototypeGuide: guideEntry?.source ?? null,
|
||||||
screenshot: spec.pageContract.screenshot,
|
screenshot: spec.pageContract.screenshot,
|
||||||
textSamples: spec.pageContract.textSamples,
|
textSamples: spec.pageContract.textSamples,
|
||||||
actions: spec.pageContract.actions.map((action) => action.label),
|
actions: spec.pageContract.actions.map((action) => action.label),
|
||||||
@@ -230,11 +244,86 @@ export function buildAppMap(layoutSpecs, prototypeDir) {
|
|||||||
prototypeStylePolicy: 'content-only',
|
prototypeStylePolicy: 'content-only',
|
||||||
prototypeOuterFramePolicy: 'ignore'
|
prototypeOuterFramePolicy: 'ignore'
|
||||||
},
|
},
|
||||||
|
guideSources: prototypeGuides.map((guide) => ({
|
||||||
|
source: guide.source,
|
||||||
|
title: guide.title,
|
||||||
|
entryCount: guide.entries.length
|
||||||
|
})),
|
||||||
modules: buildModules(routes),
|
modules: buildModules(routes),
|
||||||
routes
|
routes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePrototypeGuide(source, markdown) {
|
||||||
|
const title = matchText(markdown, /^#\s+(.+)$/m)
|
||||||
|
const entries = []
|
||||||
|
let headers = []
|
||||||
|
for (const line of markdown.split('\n')) {
|
||||||
|
if (!/^\s*\|/.test(line)) continue
|
||||||
|
const cells = splitMarkdownTableRow(line)
|
||||||
|
if (cells.length === 0 || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) continue
|
||||||
|
if (cells.some((cell) => /雛型檔|prototype/i.test(cell))) {
|
||||||
|
headers = cells.map(cleanMarkdownCell)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prototypeIndex = cells.findIndex((cell) => /\.html\b/i.test(cell))
|
||||||
|
if (prototypeIndex === -1) continue
|
||||||
|
const prototype = extractPrototypePath(cells[prototypeIndex])
|
||||||
|
if (!prototype) continue
|
||||||
|
const values = new Map(headers.map((header, index) => [header, cleanMarkdownCell(cells[index] ?? '')]))
|
||||||
|
entries.push({
|
||||||
|
prototype,
|
||||||
|
source,
|
||||||
|
legacyJsp: findGuideValue(values, ['舊 JSP']),
|
||||||
|
legacyPb: findGuideValue(values, ['舊 PB', 'PB NVO', 'PB']),
|
||||||
|
targetView: findGuideValue(values, ['Vue view', '對應 Vue']),
|
||||||
|
description: findGuideValue(values, ['功能'])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
title: title ? cleanMarkdownCell(title) : null,
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrototypeGuideIndex(prototypeGuides) {
|
||||||
|
const index = new Map()
|
||||||
|
for (const guide of prototypeGuides) {
|
||||||
|
for (const entry of guide.entries) {
|
||||||
|
index.set(entry.prototype, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMarkdownTableRow(line) {
|
||||||
|
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPrototypePath(cell) {
|
||||||
|
const linkTarget = cell.match(/\]\(([^)]+\.html)\)/i)?.[1]
|
||||||
|
const path = linkTarget ?? cell.match(/([A-Za-z0-9_./-]+\.html)\b/i)?.[1]
|
||||||
|
return path?.replace(/^\.\//, '') ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanMarkdownCell(cell) {
|
||||||
|
const value = cell
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||||
|
.replace(/`([^`]+)`/g, '$1')
|
||||||
|
.replace(/\*\*/g, '')
|
||||||
|
.replace(/<br\s*\/?>/gi, ' / ')
|
||||||
|
.trim()
|
||||||
|
return value === '' || value === '—' ? null : value
|
||||||
|
}
|
||||||
|
|
||||||
|
function findGuideValue(values, needles) {
|
||||||
|
for (const [header, value] of values) {
|
||||||
|
if (value && needles.some((needle) => header?.includes(needle))) return value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function inferRoute(spec, relativeSource) {
|
function inferRoute(spec, relativeSource) {
|
||||||
const path = relativeSource.replace(/\\/g, '/')
|
const path = relativeSource.replace(/\\/g, '/')
|
||||||
const segments = path.split('/')
|
const segments = path.split('/')
|
||||||
|
|||||||
+14
-3
@@ -1,9 +1,9 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { readFile, writeFile } from 'node:fs/promises'
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
import { basename, join } from 'node:path'
|
import { basename, join, relative } from 'node:path'
|
||||||
import { loadConfig } from '../lib/config.js'
|
import { loadConfig } from '../lib/config.js'
|
||||||
import { artifactPath, ensureDir, exists, listFiles, readJson, sha256File, writeJson } from '../lib/files.js'
|
import { artifactPath, ensureDir, exists, listFiles, readJson, sha256File, writeJson } from '../lib/files.js'
|
||||||
import { buildAppMap, buildPageContract, extractRegions, inferRegionSpec, summarizeHtml, validatePageContract } from '../lib/html.js'
|
import { buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../lib/html.js'
|
||||||
import { resolvePlaywrightCli } from '../lib/playwright-cli.js'
|
import { resolvePlaywrightCli } from '../lib/playwright-cli.js'
|
||||||
|
|
||||||
const viewports = [
|
const viewports = [
|
||||||
@@ -19,6 +19,7 @@ export async function scan() {
|
|||||||
const config = await loadConfig()
|
const config = await loadConfig()
|
||||||
const htmlFiles = await listFiles(config.prototypeDir, ['.html'])
|
const htmlFiles = await listFiles(config.prototypeDir, ['.html'])
|
||||||
if (htmlFiles.length === 0) throw new Error(`找不到 HTML prototype:${config.prototypeDir}`)
|
if (htmlFiles.length === 0) throw new Error(`找不到 HTML prototype:${config.prototypeDir}`)
|
||||||
|
const prototypeGuides = await readPrototypeGuides(config)
|
||||||
await ensureDir(config.htDir)
|
await ensureDir(config.htDir)
|
||||||
const plans = []
|
const plans = []
|
||||||
for (const file of htmlFiles) {
|
for (const file of htmlFiles) {
|
||||||
@@ -34,10 +35,20 @@ export async function scan() {
|
|||||||
} finally {
|
} finally {
|
||||||
await server?.close()
|
await server?.close()
|
||||||
}
|
}
|
||||||
await writeJson(join(config.htDir, 'app-map.json'), buildAppMap(layoutSpecs, config.prototypeDir))
|
await writeJson(join(config.htDir, 'app-map.json'), buildAppMapWithGuides(layoutSpecs, config.prototypeDir, prototypeGuides))
|
||||||
console.log(`scan 完成:${htmlFiles.length} 個 HTML 檔案`)
|
console.log(`scan 完成:${htmlFiles.length} 個 HTML 檔案`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readPrototypeGuides(config) {
|
||||||
|
const files = await listFiles(config.prototypeDir, ['.md'])
|
||||||
|
const guides = []
|
||||||
|
for (const file of files) {
|
||||||
|
const source = relative(config.prototypeDir, file).replaceAll('\\', '/')
|
||||||
|
guides.push(parsePrototypeGuide(source, await readFile(file, 'utf8')))
|
||||||
|
}
|
||||||
|
return guides.filter((guide) => guide.entries.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareScanFile(config, file) {
|
async function prepareScanFile(config, file) {
|
||||||
const hash = await sha256File(file)
|
const hash = await sha256File(file)
|
||||||
const name = artifactPath(config.prototypeDir, file)
|
const name = artifactPath(config.prototypeDir, file)
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ test('CLI runs doctor and scan against one prototype', async () => {
|
|||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
`)
|
`)
|
||||||
|
await writeFile(join(cwd, 'packages/prototype/portal.md'), `
|
||||||
|
# Portal Guide
|
||||||
|
|
||||||
|
| 雛型檔 | 舊 JSP 來源 | 功能 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [\`index.html\`](index.html) | \`legacy/index.jsp\` | Customer portal entry |
|
||||||
|
`)
|
||||||
|
|
||||||
const doctor = await exec('node', [cli, 'doctor'], { cwd })
|
const doctor = await exec('node', [cli, 'doctor'], { cwd })
|
||||||
await exec('node', [cli, 'scan'], { cwd })
|
await exec('node', [cli, 'scan'], { cwd })
|
||||||
@@ -45,6 +52,9 @@ test('CLI runs doctor and scan against one prototype', async () => {
|
|||||||
assert.equal(appMap.routes[0].prototype, 'index.html')
|
assert.equal(appMap.routes[0].prototype, 'index.html')
|
||||||
assert.equal(appMap.routes[0].kind, 'feature-page')
|
assert.equal(appMap.routes[0].kind, 'feature-page')
|
||||||
assert.equal(appMap.routes[0].layout, 'template-app')
|
assert.equal(appMap.routes[0].layout, 'template-app')
|
||||||
|
assert.equal(appMap.routes[0].guide.legacyJsp, 'legacy/index.jsp')
|
||||||
|
assert.equal(appMap.routes[0].guide.description, 'Customer portal entry')
|
||||||
|
assert.equal(appMap.guideSources[0].source, 'portal.md')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('CLI help only exposes MVP commands', async () => {
|
test('CLI help only exposes MVP commands', async () => {
|
||||||
|
|||||||
+22
-1
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { buildAppMap, buildPageContract, extractRegions, inferRegionSpec, summarizeHtml, validatePageContract } from '../src/lib/html.js'
|
import { buildAppMap, buildAppMapWithGuides, buildPageContract, extractRegions, inferRegionSpec, parsePrototypeGuide, summarizeHtml, validatePageContract } from '../src/lib/html.js'
|
||||||
|
|
||||||
test('summarizeHtml extracts user-visible contract evidence', () => {
|
test('summarizeHtml extracts user-visible contract evidence', () => {
|
||||||
const summary = summarizeHtml(`
|
const summary = summarizeHtml(`
|
||||||
@@ -97,6 +97,27 @@ test('buildAppMap classifies auth, shell references, and feature pages', () => {
|
|||||||
assert.equal(appMap.modules.find((module) => module.name === 'venue').kind, 'feature-module')
|
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 view(M9) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [\`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')
|
||||||
|
})
|
||||||
|
|
||||||
function buildSpec(source, html) {
|
function buildSpec(source, html) {
|
||||||
const regions = extractRegions(html)
|
const regions = extractRegions(html)
|
||||||
const domSummary = summarizeHtml(html)
|
const domSummary = summarizeHtml(html)
|
||||||
|
|||||||
Reference in New Issue
Block a user