验收完成40%
This commit is contained in:
@@ -19,6 +19,7 @@ $root = Split-Path -Parent $PSScriptRoot
|
||||
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
$paths = @($pages.pages.path)
|
||||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||||
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g01-empty-state-runtime-smoke.js') -Raw -Encoding utf8
|
||||
$panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g01-empty-panel.png'
|
||||
|
||||
if (-not ($paths -contains 'pages/genealogy/g01-my-genealogies')) { throw 'G01 route missing' }
|
||||
@@ -40,11 +41,28 @@ foreach ($required in @(
|
||||
if ($g01 -notmatch [regex]::Escape($required)) { throw "Missing G01 empty-state contract: $required" }
|
||||
}
|
||||
|
||||
foreach ($required in @(
|
||||
'class="genealogy-fixed-zone"',
|
||||
'class="genealogy-list-scroll"',
|
||||
'scroll-y',
|
||||
':scroll-top="listScrollCommand"',
|
||||
'@scroll="handleListScroll"',
|
||||
'const isListLayout = computed(',
|
||||
'const resetListScroll = async () =>'
|
||||
)) {
|
||||
if ($g01 -notmatch [regex]::Escape($required)) { throw "Missing G01 split-scroll contract: $required" }
|
||||
}
|
||||
|
||||
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
|
||||
|
||||
foreach ($className in @('empty-panel', 'empty-create-action', 'empty-search-action')) {
|
||||
Assert-NoCssSurface -Content $g01 -ClassName $className
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G01 requires its final raster empty-panel asset' }
|
||||
Assert-Match -Content $runtimeSmoke -Pattern '\{\s*width:\s*412,\s*height:\s*1000\s*\}' -Message 'G01 runtime smoke must include the approved 412x1000 tall-screen stress viewport'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$panel = [System.Drawing.Bitmap]::FromFile($panelPath)
|
||||
try {
|
||||
|
||||
@@ -98,7 +98,7 @@ const run = async () => {
|
||||
|
||||
await valueOf(send, "document.querySelector('.create-action').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.add-dialog-layer'))", 'G01 add action did not open the custom add dialog')
|
||||
if ((await valueOf(send, "document.querySelectorAll('.add-dialog .dialog-primary, .add-dialog .dialog-secondary, .add-dialog .dialog-create').length")) !== 3) {
|
||||
if ((await valueOf(send, "document.querySelectorAll('.add-dialog .app-button').length")) !== 3) {
|
||||
throw new Error('G01 add dialog must keep search, invite, and create visible together')
|
||||
}
|
||||
await valueOf(send, "document.querySelector('.dialog-close').click()")
|
||||
@@ -115,15 +115,96 @@ const run = async () => {
|
||||
await waitFor(send, `Boolean(document.querySelector('.state-panel--${state}'))`, `G01 did not render ${state}`)
|
||||
}
|
||||
|
||||
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
|
||||
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 640 }, { width: 360, height: 800 }, { width: 412, height: 915 }, { width: 412, height: 1000 }]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
|
||||
const responsiveUrl = nextUrl()
|
||||
await send('Page.navigate', { url: responsiveUrl })
|
||||
await waitFor(send, "Boolean(document.querySelector('.current-slip'))", `G01 did not render at ${size.width}x${size.height}`)
|
||||
const width = await valueOf(send, 'document.documentElement.scrollWidth')
|
||||
if (width > size.width + 1) throw new Error(`G01 has horizontal overflow at ${size.width}x${size.height}`)
|
||||
|
||||
const before = await valueOf(send, `(() => {
|
||||
const host = document.querySelector('.genealogy-list-scroll')
|
||||
if (!host) return null
|
||||
const scrollNodes = host.querySelectorAll('.uni-scroll-view')
|
||||
const scroller = scrollNodes[scrollNodes.length - 1] || host.shadowRoot?.querySelector('.uni-scroll-view') || host
|
||||
const fixedSelectors = ['.page-header', '.current-slip', '.shortcut-grid', '.section-divider', '.app-tabbar']
|
||||
return {
|
||||
fixed: Object.fromEntries(fixedSelectors.map((selector) => [selector, document.querySelector(selector)?.getBoundingClientRect().top])),
|
||||
lowerTop: document.querySelector('.section-heading')?.getBoundingClientRect().top,
|
||||
clientHeight: scroller.clientHeight,
|
||||
scrollHeight: scroller.scrollHeight
|
||||
}
|
||||
})()`)
|
||||
if (!before) throw new Error(`G01 has no independent list scroller at ${size.width}x${size.height}`)
|
||||
if (before.clientHeight <= 0 || before.scrollHeight <= before.clientHeight) {
|
||||
throw new Error(`G01 independent list scroller has no usable range at ${size.width}x${size.height}: ${JSON.stringify(before)}`)
|
||||
}
|
||||
|
||||
await valueOf(send, `(() => {
|
||||
const host = document.querySelector('.genealogy-list-scroll')
|
||||
const scrollNodes = host.querySelectorAll('.uni-scroll-view')
|
||||
const scroller = scrollNodes[scrollNodes.length - 1] || host.shadowRoot?.querySelector('.uni-scroll-view') || host
|
||||
scroller.scrollTop = Math.min(180, scroller.scrollHeight - scroller.clientHeight)
|
||||
scroller.dispatchEvent(new Event('scroll', { bubbles: true }))
|
||||
})()`)
|
||||
await sleep(100)
|
||||
|
||||
const after = await valueOf(send, `(() => {
|
||||
const fixedSelectors = ['.page-header', '.current-slip', '.shortcut-grid', '.section-divider', '.app-tabbar']
|
||||
return {
|
||||
fixed: Object.fromEntries(fixedSelectors.map((selector) => [selector, document.querySelector(selector)?.getBoundingClientRect().top])),
|
||||
lowerTop: document.querySelector('.section-heading')?.getBoundingClientRect().top
|
||||
}
|
||||
})()`)
|
||||
for (const selector of Object.keys(before.fixed)) {
|
||||
if (Math.abs(after.fixed[selector] - before.fixed[selector]) > 1) {
|
||||
throw new Error(`G01 fixed selector moved while list scrolled at ${size.width}x${size.height}: ${selector}`)
|
||||
}
|
||||
}
|
||||
if (!(after.lowerTop < before.lowerTop - 2)) {
|
||||
throw new Error(`G01 lower list did not move independently at ${size.width}x${size.height}`)
|
||||
}
|
||||
}
|
||||
|
||||
const readListScrollTop = `(() => {
|
||||
const host = document.querySelector('.genealogy-list-scroll')
|
||||
const scrollNodes = host.querySelectorAll('.uni-scroll-view')
|
||||
const scroller = scrollNodes[scrollNodes.length - 1] || host.shadowRoot?.querySelector('.uni-scroll-view') || host
|
||||
return scroller.scrollTop
|
||||
})()`
|
||||
await valueOf(send, `(() => {
|
||||
const host = document.querySelector('.genealogy-list-scroll')
|
||||
const scrollNodes = host.querySelectorAll('.uni-scroll-view')
|
||||
const scroller = scrollNodes[scrollNodes.length - 1] || host.shadowRoot?.querySelector('.uni-scroll-view') || host
|
||||
scroller.scrollTop = scroller.scrollHeight
|
||||
})()`)
|
||||
await sleep(100)
|
||||
const endGeometry = await valueOf(send, `(() => {
|
||||
const action = document.querySelector('.create-action').getBoundingClientRect()
|
||||
const tabbar = document.querySelector('.app-tabbar').getBoundingClientRect()
|
||||
return { actionTop: action.top, actionBottom: action.bottom, tabbarTop: tabbar.top }
|
||||
})()`)
|
||||
if (endGeometry.actionTop >= endGeometry.tabbarTop || endGeometry.actionBottom > endGeometry.tabbarTop + 1) {
|
||||
throw new Error(`G01 add action is not fully reachable above the fixed tabbar: ${JSON.stringify(endGeometry)}`)
|
||||
}
|
||||
const scrollTopBeforeDialog = await valueOf(send, readListScrollTop)
|
||||
if (scrollTopBeforeDialog <= 0) throw new Error('G01 list must be scrolled before testing position preservation')
|
||||
await valueOf(send, "document.querySelector('.create-action').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.add-dialog-layer'))", 'G01 add dialog did not open over the independent list')
|
||||
await valueOf(send, "document.querySelector('.dialog-close').click()")
|
||||
await waitFor(send, "!document.querySelector('.add-dialog-layer')", 'G01 add dialog did not close over the independent list')
|
||||
const scrollTopAfterDialog = await valueOf(send, readListScrollTop)
|
||||
if (Math.abs(scrollTopAfterDialog - scrollTopBeforeDialog) > 1) {
|
||||
throw new Error('G01 add dialog changed the independent list position')
|
||||
}
|
||||
|
||||
await valueOf(send, "document.querySelector('.current-slip').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.genealogy-switcher-layer'))", 'G01 switcher did not open over the independent list')
|
||||
await valueOf(send, "document.querySelectorAll('.switcher-item')[1].click()")
|
||||
await waitFor(send, "document.querySelector('.current-name')?.textContent.includes('汤氏宗谱')", 'G01 switcher did not change the current genealogy after scrolling')
|
||||
await waitFor(send, `${readListScrollTop} <= 1`, 'G01 independent list did not return to the top after switching genealogy')
|
||||
|
||||
if (exceptions.length) throw new Error(`G01 raised browser exceptions: ${exceptions.join('; ')}`)
|
||||
|
||||
process.stdout.write('G01-EMPTY-STATE-RUNTIME-SMOKE PASS\n')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$requiredAssets = @(
|
||||
@@ -25,8 +26,7 @@ $requiredAssets = @(
|
||||
'static/assets/modules/genealogy/transparent/current-slip-frame.png',
|
||||
'static/assets/modules/genealogy/transparent/list-slip-frame.png',
|
||||
'static/assets/foundation/opaque/root-header-cinnabar.jpg',
|
||||
'static/assets/foundation/opaque/page-paper.jpg',
|
||||
'static/assets/foundation/transparent/footer-mountain-bamboo.png',
|
||||
'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png',
|
||||
'static/assets/modules/genealogy/transparent/section-divider.png'
|
||||
)
|
||||
|
||||
@@ -76,12 +76,25 @@ foreach ($token in @('current-summary', 'current-meta-item', 'current-seal-frame
|
||||
if ($page -notmatch $token) { throw "G-01 current panel is missing $token" }
|
||||
}
|
||||
if ($page -notmatch 'width:\s*82rpx') { throw 'G-01 shortcut icons remain below the reference visual size.' }
|
||||
foreach ($asset in @('page-paper.jpg', 'footer-mountain-bamboo.png', 'section-divider.png')) {
|
||||
if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume the approved background asset: $asset" }
|
||||
if ($page -notmatch '<GenealogyPageBackground\s*/>') { throw 'G-01 does not consume the shared genealogy background component.' }
|
||||
if ($page -notmatch 'section-divider\.png') { throw 'G-01 does not consume the approved section divider asset.' }
|
||||
foreach ($legacyBackground in @('page-paper.jpg', 'footer-mountain-bamboo.png', 'page-paper-texture', 'page-footer-landscape')) {
|
||||
if ($page -match [regex]::Escape($legacyBackground)) { throw "G-01 must replace the legacy layered background: $legacyBackground" }
|
||||
}
|
||||
if ($page -match 'footer-mountain-bamboo-v2\.png') { throw 'G-01 still uses the solid mountain footer asset instead of the translucent watercolor landscape.' }
|
||||
if ($page -notmatch 'top:\s*calc\(124rpx\s*\+\s*var\(--status-bar-height,\s*0px\)\)') { throw 'G-01 paper texture does not begin below the system-safe root header.' }
|
||||
if ($page -notmatch '(?s)\.page-footer-landscape\s*\{.*?opacity:\s*0\.5') { throw 'G-01 footer landscape does not use the approved expanded watercolor contrast.' }
|
||||
$backgroundComponent = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyPageBackground.vue')
|
||||
if ($backgroundComponent -notmatch 'mode="widthFix"') { throw 'G shared background must preserve the complete C artwork without horizontal cropping.' }
|
||||
if ($backgroundComponent -match 'aspectFill|scaleToFill') { throw 'G shared background must not crop or stretch the selected C artwork.' }
|
||||
if ($backgroundComponent -notmatch '(?s)\.genealogy-page-background\s*\{.*?position:\s*fixed;.*?inset:\s*0;.*?background:\s*#e7ded1;') { throw 'G shared background must extend the selected C artwork with the approved paper color.' }
|
||||
$backgroundPath = Join-Path $root 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png'
|
||||
$backgroundImage = [System.Drawing.Image]::FromFile($backgroundPath)
|
||||
try {
|
||||
if ($backgroundImage.Width -ne 1440 -or $backgroundImage.Height -ne 3600) { throw "G-01 approved long runtime background must be 1440x3600, got $($backgroundImage.Width)x$($backgroundImage.Height)." }
|
||||
}
|
||||
finally {
|
||||
$backgroundImage.Dispose()
|
||||
}
|
||||
$backgroundHash = (Get-FileHash -LiteralPath $backgroundPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($backgroundHash -ne '9aa9ebdd231eddbc151e238ba340a00f8603f23143117ad8c93009947dcca2e2') { throw 'G-01 runtime background must be the user-selected flagship long output.' }
|
||||
if ($page -match 'background:\s*rgba\(255,\s*249,\s*238,\s*\.88\)') { throw 'G-01 current genealogy card retains a rectangular pale backing behind its transparent corners.' }
|
||||
|
||||
$card = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyCard.vue')
|
||||
@@ -118,6 +131,10 @@ if ($card -notmatch '(?s)\.card-meta\s*\{.*?font-size:\s*26rpx') { throw 'G-01 l
|
||||
if ($card -notmatch '(?s)\.card-updated\s*\{.*?font-size:\s*22rpx') { throw 'G-01 list update date is below the approved readable size.' }
|
||||
if ($page -notmatch '(?s)\.create-action\s*>\s*text\s*\{.*?white-space:\s*nowrap') { throw 'G-01 create action text can wrap onto multiple lines.' }
|
||||
if ($page -notmatch '(?s)\.application-record\s*\+\s*\.application-record\s*\{[^}]*margin-top:\s*12rpx') { throw 'G-01 adjacent application cards must keep the approved 12rpx separation.' }
|
||||
if ($page -notmatch '(?s)\.application-record__copy\s*\{[^}]*color:\s*#62584c;[^}]*font-size:\s*28rpx;[^}]*font-weight:\s*500;[^}]*line-height:\s*1\.4;') { throw 'G-01 application descriptions do not use the approved readable style.' }
|
||||
if ($page -notmatch '(?s)\.application-record__status\s*\{[^}]*color:\s*#7f4f16;[^}]*font-size:\s*27rpx;[^}]*font-weight:\s*600;') { throw 'G-01 application statuses do not use the approved readable base style.' }
|
||||
if ($page -notmatch '(?s)\.application-record__status--rejected\s*\{[^}]*color:\s*#a7160c;') { throw 'G-01 rejected status must preserve the approved semantic red.' }
|
||||
if ($page -notmatch '(?s)\.application-record__status--muted\s*\{[^}]*color:\s*#62584c;') { throw 'G-01 muted status does not use the approved readable color.' }
|
||||
|
||||
$manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json')
|
||||
if ($manifest -notmatch '"statusbar"\s*:\s*\{\s*"immersed"\s*:\s*"supportedDevice"\s*,\s*"style"\s*:\s*"light"\s*,\s*"background"\s*:\s*"#B52E22"') { throw 'Android status bar is not configured for the cinnabar root header.' }
|
||||
|
||||
@@ -38,7 +38,7 @@ foreach ($required in @(
|
||||
'/pages/genealogy/g05-genealogy-overview?genealogyId=',
|
||||
'g03-create-flow-panel.png',
|
||||
'root-header-cinnabar.jpg',
|
||||
'a01-primary-button.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'create-flow-panel',
|
||||
'flow-primary-action'
|
||||
)) {
|
||||
|
||||
@@ -46,7 +46,9 @@ const open = async (send, query, selector) => {
|
||||
const url = `${origin}/#${route}${query}`
|
||||
await send('Page.navigate', { url })
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, 'G05 navigation failed')
|
||||
const documentTimeOrigin = await valueOf(send, 'performance.timeOrigin')
|
||||
await send('Page.reload')
|
||||
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(documentTimeOrigin)}`, `G05 ${query} did not reload into a fresh document`)
|
||||
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `G05 ${query} did not render`)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ $catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Ra
|
||||
|
||||
foreach ($page in @($g08, $g09, $g10)) {
|
||||
if ($page -match '<ModulePage') { throw 'G08-G10 must not retain the generic ModulePage shell' }
|
||||
Assert-Contains $page 'page-paper.jpg' 'G08-G10 must use the accepted genealogy paper background'
|
||||
Assert-Contains $page '<GenealogyPageBackground />' 'G08-G10 must use the accepted shared genealogy background'
|
||||
if ($page -match "@/utils/api\.js|\bappApi\b") { throw 'G08-G10 design phase must not connect the API layer' }
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ foreach ($required in @(
|
||||
'join-state--success',
|
||||
'join-state--error',
|
||||
'g03-create-flow-panel.png',
|
||||
'a01-primary-button.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'const genealogyPreview',
|
||||
'query.genealogyId',
|
||||
"const source = ref('search')",
|
||||
@@ -58,7 +58,7 @@ foreach ($required in @(
|
||||
'const applicationSamples',
|
||||
'const actionFor = (item) =>',
|
||||
'const withdrawTarget = ref(null)',
|
||||
'class="withdraw-layer"',
|
||||
'<AppDialog',
|
||||
'/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId='
|
||||
)) { Assert-Contains $g09 $required "Missing G09 application contract: $required" }
|
||||
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
|
||||
@@ -72,12 +72,12 @@ foreach ($required in @(
|
||||
'review-state--error',
|
||||
'application-record-card.png',
|
||||
'application-status-card.png',
|
||||
'a01-primary-button.png',
|
||||
'a01-secondary-button.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'a01-scroll-secondary-v3.png',
|
||||
'const reviewSamples',
|
||||
'const confirmation = ref(null)',
|
||||
'const helpVisible = ref(false)',
|
||||
'class="review-dialog-layer"',
|
||||
'<AppDialog',
|
||||
'review-state--no-permission'
|
||||
)) { Assert-Contains $g10 $required "Missing G10 review contract: $required" }
|
||||
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
|
||||
|
||||
@@ -98,21 +98,21 @@ const run = async () => {
|
||||
await open(send, '/pages/genealogy/g09-my-applications', '', '.application-state--list')
|
||||
if ((await valueOf(send, "document.querySelectorAll('.application-card__action').length")) !== 3) throw new Error('G09 list did not render the three status-specific actions')
|
||||
await valueOf(send, "document.querySelector('.application-card__action').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.withdraw-layer'))", 'G09 pending action did not open custom withdrawal confirmation')
|
||||
await valueOf(send, "document.querySelector('.withdraw-action--danger').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.app-dialog-layer'))", 'G09 pending action did not open custom withdrawal confirmation')
|
||||
await valueOf(send, "document.querySelector('.app-dialog__actions .app-button:last-child').click()")
|
||||
await waitFor(send, "document.querySelector('.application-card__status')?.textContent.includes('已撤回')", 'G09 withdrawal did not update the local status')
|
||||
await valueOf(send, "document.querySelectorAll('.application-card__action')[1].click()")
|
||||
await waitFor(send, "location.href.includes('/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=')", 'G09 rejected action did not reopen G08 search source')
|
||||
await open(send, '/pages/genealogy/g09-my-applications', '?state=empty', '.application-state--empty')
|
||||
await open(send, '/pages/genealogy/g10-application-review', '?genealogyId=1001', '.review-state--list')
|
||||
await valueOf(send, "document.querySelectorAll('.review-action')[1].click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.review-dialog-layer'))", 'G10 approve action did not open custom confirmation')
|
||||
await valueOf(send, "document.querySelector('.review-dialog__action--primary').click()")
|
||||
await waitFor(send, "Boolean(document.querySelector('.app-dialog-layer'))", 'G10 approve action did not open custom confirmation')
|
||||
await valueOf(send, "document.querySelector('.app-dialog__actions .app-button:last-child').click()")
|
||||
await waitFor(send, "document.querySelector('.application-card__status')?.textContent.includes('已通过')", 'G10 approval did not update the card')
|
||||
await waitFor(send, "Boolean(document.querySelector('.review-feedback'))", 'G10 approval did not show custom feedback')
|
||||
await valueOf(send, "document.querySelector('.header-action').click()")
|
||||
await waitFor(send, "document.querySelector('.review-dialog__title')?.textContent.includes('审核说明')", 'G10 help did not use the custom dialog')
|
||||
await valueOf(send, "document.querySelector('.review-dialog__single').click()")
|
||||
await waitFor(send, "document.querySelector('.app-dialog__title')?.textContent.includes('审核说明')", 'G10 help did not use the custom dialog')
|
||||
await valueOf(send, "document.querySelector('.app-dialog__actions .app-button').click()")
|
||||
await open(send, '/pages/genealogy/g10-application-review', '?state=empty&genealogyId=1001', '.review-state--empty')
|
||||
await open(send, '/pages/genealogy/g10-application-review', '?state=no-permission&genealogyId=1001', '.review-state--no-permission')
|
||||
if (exceptions.length) throw new Error(`G08-G10 raised browser exceptions: ${exceptions.join('; ')}`)
|
||||
|
||||
@@ -21,7 +21,7 @@ $catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Ra
|
||||
|
||||
foreach ($page in @($g11, $g12)) {
|
||||
if ($page -match '<ModulePage') { throw 'G11-G12 must not retain the generic ModulePage shell' }
|
||||
Assert-Contains $page 'page-paper.jpg' 'G11-G12 must use the accepted genealogy paper background'
|
||||
Assert-Contains $page '<GenealogyPageBackground />' 'G11-G12 must use the accepted shared genealogy background'
|
||||
Assert-Contains $page 'g03-create-flow-panel.png' 'G11-G12 forms must reuse the complete genealogy paper panel'
|
||||
if ($page -match "@/utils/api\.js|\bappApi\b") { throw 'G11-G12 design phase must not connect the API layer' }
|
||||
}
|
||||
@@ -35,7 +35,7 @@ foreach ($required in @(
|
||||
'const nameError = ref',
|
||||
'class="settings-feedback"',
|
||||
'g06-search-input-wide.png',
|
||||
'a01-primary-button.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'const genealogyDraft',
|
||||
'query.genealogyId',
|
||||
"'MEMBER_ONLY'",
|
||||
@@ -52,8 +52,8 @@ foreach ($required in @(
|
||||
'const poemError = ref',
|
||||
'class="poem-feedback"',
|
||||
'g06-search-input-wide.png',
|
||||
'a01-primary-button.png',
|
||||
'a01-secondary-button.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'a01-scroll-secondary-v3.png',
|
||||
'const poemRows',
|
||||
'stopMissingOldGeneration',
|
||||
'query.genealogyId'
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$decode = {
|
||||
param([string]$value)
|
||||
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($value))
|
||||
function ConvertFrom-Utf8Base64 {
|
||||
param([string]$Value)
|
||||
return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value))
|
||||
}
|
||||
|
||||
$handoff = Get-Content -Raw -Encoding UTF8 (& $decode 'ZG9jcy/kuqTmjqXorrDlvZUubWQ=')
|
||||
$manual = Get-Content -Raw -Encoding UTF8 (& $decode 'ZG9jcy9kZXNpZ24v6KeG6KeJ6K6+6K6h5Lqk5o6l5omL5YaMLm1k')
|
||||
$planning = Get-Content -Raw -Encoding UTF8 (& $decode 'ZG9jcy/op4TliJIubWQ=')
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$handoffPath = ConvertFrom-Utf8Base64 'ZG9jcy/kuqTmjqXorrDlvZUubWQ='
|
||||
$manualPath = ConvertFrom-Utf8Base64 'ZG9jcy9kZXNpZ24v6KeG6KeJ6K6+6K6h5Lqk5o6l5omL5YaMLm1k'
|
||||
$planningPath = ConvertFrom-Utf8Base64 'ZG9jcy/pqozmlLbop4TliJIubWQ='
|
||||
$handoff = Get-Content -Raw -Encoding UTF8 (Join-Path $root $handoffPath)
|
||||
$manual = Get-Content -Raw -Encoding UTF8 (Join-Path $root $manualPath)
|
||||
$planning = Get-Content -Raw -Encoding UTF8 (Join-Path $root $planningPath)
|
||||
|
||||
foreach ($required in @(
|
||||
(& $decode 'NTQg5p2h5pyA57uI6Lev55Sx'),
|
||||
(& $decode 'NyDkuKrlt7LpqozmlLY='),
|
||||
(& $decode 'NDcg5Liq5b6F5a6h5qC45YCZ6YCJ'),
|
||||
(ConvertFrom-Utf8Base64 'NTIg5p2h5rS75Yqo6Lev55Sx'),
|
||||
(ConvertFrom-Utf8Base64 'NTIg5Liq5b6F5a6h5qC4'),
|
||||
(ConvertFrom-Utf8Base64 'MCDkuKrlt7LpqozmlLY='),
|
||||
'product-design:index',
|
||||
'product-design:audit',
|
||||
'imagegen',
|
||||
'superpowers:test-driven-development',
|
||||
'superpowers:systematic-debugging',
|
||||
'superpowers:verification-before-completion',
|
||||
(& $decode 'ZG9jcy9kZXNpZ24v6KeG6KeJ6K+B5o2u57Si5byVLm1k')
|
||||
(ConvertFrom-Utf8Base64 'ZG9jcy9kZXNpZ24v6KeG6KeJ6K+B5o2u57Si5byVLm1k')
|
||||
)) {
|
||||
if ($handoff -notmatch [regex]::Escape($required)) {
|
||||
throw "handoff missing: $required"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($stale in @(
|
||||
(& $decode '5YWt5Liq5bey6aqM5pS2'),
|
||||
(& $decode '57uT5p6c5oCB5ZKM5peg57uT5p6c5oCB55qE5ZCM54mI6YeN5paw5oiq5Zu+5bCa5pyq5a6M5oiQ'),
|
||||
(& $decode '57uT5p6c5oCBL+aXoOe7k+aenOaAgeeahOWQjOeJiOaIquWbvuWSjOeUqOaIt+Wuoee+juehruiupOWwmuacquWujOaIkA=='),
|
||||
(& $decode '5LiL5LiA6aG55oyJIEbjgIFS44CBTuOAgU0g5o6o6L+b')
|
||||
foreach ($encodedStale in @(
|
||||
'5YWt5Liq5bey6aqM5pS2',
|
||||
'57uT5p6c5oCB5ZKM5peg57uT5p6c5oCB55qE5ZCM54mI6YeN5paw5oiq5Zu+5bCa5pyq5a6M5oiQ',
|
||||
'57uT5p6c5oCBL+aXoOe7k+aenOaAgeeahOWQjOeJiOaIquWbvuWSjOeUqOaIt+Wuoee+juehruiupOWwmuacquWujOaIkA==',
|
||||
'5LiL5LiA6aG55oyJIEbjgIFS44CBTuOAgU0g5o6o6L+b'
|
||||
)) {
|
||||
$stale = ConvertFrom-Utf8Base64 $encodedStale
|
||||
if (($handoff + $manual) -match [regex]::Escape($stale)) {
|
||||
throw "stale handoff text remains: $stale"
|
||||
}
|
||||
}
|
||||
|
||||
$approved = ([regex]::Matches($planning, '- \[x\]')).Count
|
||||
$candidate = ([regex]::Matches($planning, '- \[~\]')).Count
|
||||
if ($approved -ne 7 -or $candidate -ne 47) {
|
||||
$approved = ([regex]::Matches($planning, '\|\s*`\[x\]`[^|]*\|\s*\d+\s*\|')).Count
|
||||
$candidate = ([regex]::Matches($planning, '\|\s*`\[~\]`[^|]*\|\s*\d+\s*\|')).Count
|
||||
if ($approved -ne 0 -or $candidate -ne 52) {
|
||||
throw "planning counts changed: approved=$approved candidate=$candidate"
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,15 @@ foreach ($anchor in @(
|
||||
'g03-create-flow-panel.png',
|
||||
'g06-search-input-wide.png',
|
||||
'application-status-card.png',
|
||||
'a01-primary-button.png',
|
||||
'a01-secondary-button.png',
|
||||
'<AppButton',
|
||||
'<AppToast',
|
||||
'<ModulePageBackground',
|
||||
'query.state'
|
||||
)) {
|
||||
if ($component -notmatch [regex]::Escape($anchor)) { throw "ModulePage is missing high-spec anchor: $anchor" }
|
||||
}
|
||||
|
||||
foreach ($className in @('module-panel', 'form-row', 'list-card', 'detail-card', 'timeline-row', 'settings-row', 'status-card', 'primary-action')) {
|
||||
foreach ($className in @('module-panel', 'form-row', 'list-card', 'detail-card', 'timeline-row', 'settings-row', 'status-card')) {
|
||||
foreach ($property in @('border', 'background', 'border-radius', 'box-shadow', 'background-image')) {
|
||||
if ($component -match "(?s)\.$className\s*\{[^}]*\b$property\s*:") {
|
||||
throw "ModulePage must not construct .$className with CSS $property"
|
||||
|
||||
@@ -3,9 +3,9 @@ function Assert-Contains { param([string]$Content,[string]$Expected,[string]$Mes
|
||||
$root=Split-Path -Parent $PSScriptRoot
|
||||
$files=@('pages/family/f01-family-feed.vue','pages/family/f02-publish-feed.vue','pages/notification/n01-message-center.vue','pages/profile/m01-profile-home.vue')
|
||||
$pages=@{}
|
||||
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'page-paper.jpg' "$file must use page paper";$pages[$file]=$content}
|
||||
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'ModulePageBackground' "$file must use its module background";$pages[$file]=$content}
|
||||
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','application-status-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
|
||||
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','a01-primary-button.png')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
|
||||
foreach($required in @('notice-state--list','notice-state--empty','notice-state--error','application-status-card.png','a01-secondary-button.png','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
|
||||
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
|
||||
foreach($required in @('notice-state--list','notice-state--empty','notice-state--error','application-status-card.png','AppButton','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
|
||||
foreach($required in @('profile-state--ready','profile-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
|
||||
Write-Output 'ROOT-PAGES-VISUAL-CONTRACT PASS'
|
||||
|
||||
@@ -17,13 +17,13 @@ $paths = @(
|
||||
$pages = @{}
|
||||
foreach ($path in $paths) {
|
||||
$content = Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding utf8
|
||||
if ($content -match '<ModulePage') { throw "$path must not retain ModulePage" }
|
||||
if ($content -match '<ModulePage(?:\s|>)') { throw "$path must not retain ModulePage" }
|
||||
if ($content -match "@/utils/api\.js|\bappApi\b") { throw "$path must not connect the API layer" }
|
||||
$pages[$path] = $content
|
||||
}
|
||||
|
||||
foreach ($path in @('pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-directory.vue', 'pages/tree/t08-member-states.vue')) {
|
||||
Assert-Contains $pages[$path] 'page-paper.jpg' "$path must use the accepted paper background"
|
||||
Assert-Contains $pages[$path] 'ModulePageBackground' "$path must use the tree module background"
|
||||
}
|
||||
|
||||
$t03 = $pages['pages/tree/t03-member-profile.vue']
|
||||
@@ -38,7 +38,7 @@ if ($form -match "@/utils/api\.js|\bappApi\b") { throw 'TreeMemberForm must not
|
||||
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
|
||||
if ($form -match [regex]::Escape($nativeUi)) { throw "TreeMemberForm must use project-owned feedback instead of $nativeUi" }
|
||||
}
|
||||
foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', 'a01-primary-button.png', 'a01-secondary-button.png')) {
|
||||
foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'AppDialog', 'ModulePageBackground')) {
|
||||
Assert-Contains $form $required "Missing shared form contract: $required"
|
||||
}
|
||||
foreach ($route in @('t04-add-relative.vue', 't05-edit-member.vue', 't06-edit-relationship.vue')) {
|
||||
|
||||
@@ -16,11 +16,11 @@ const connect = async () => {
|
||||
const request = pending.get(message.id)
|
||||
if (!request) return
|
||||
pending.delete(message.id)
|
||||
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
|
||||
message.error ? request.reject(new Error(`${request.method}: ${message.error.message}`)) : request.resolve(message.result)
|
||||
})
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
id += 1
|
||||
pending.set(id, { resolve, reject })
|
||||
pending.set(id, { resolve, reject, method })
|
||||
socket.send(JSON.stringify({ id, method, params }))
|
||||
})
|
||||
return { socket, send }
|
||||
@@ -28,7 +28,11 @@ const connect = async () => {
|
||||
const valueOf = async (send, expression) => (await send('Runtime.evaluate', { expression, returnByValue: true })).result?.value
|
||||
const waitFor = async (send, expression, message) => {
|
||||
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||
if (await valueOf(send, expression)) return
|
||||
try {
|
||||
if (await valueOf(send, expression)) return
|
||||
} catch (error) {
|
||||
if (!String(error.message).includes('Inspected target navigated or closed')) throw error
|
||||
}
|
||||
await sleep(100)
|
||||
}
|
||||
throw new Error(message)
|
||||
|
||||
Reference in New Issue
Block a user