feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,774 @@
|
||||
#!/usr/bin/env node
|
||||
// Validate that diff presentation toggles refresh in place without remounting
|
||||
// the Monaco diff tree or shifting the host geometry.
|
||||
|
||||
import net from 'node:net'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
const demoDir = path.join(repoRoot, 'examples', 'streaming-demo')
|
||||
|
||||
function isPortOpen(port, host = '127.0.0.1') {
|
||||
return new Promise((resolve) => {
|
||||
const sock = net.createConnection({ host, port })
|
||||
sock.on('connect', () => {
|
||||
sock.end()
|
||||
resolve(true)
|
||||
})
|
||||
sock.on('error', () => {
|
||||
sock.destroy()
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function findFreePort(start = 5173, end = 5190) {
|
||||
for (let port = start; port <= end; port++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const open = await isPortOpen(port)
|
||||
if (!open) return port
|
||||
}
|
||||
throw new Error(`No free port found in ${start}-${end}`)
|
||||
}
|
||||
|
||||
async function waitForPort(port, ms = 30000) {
|
||||
const start = Date.now()
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const open = await isPortOpen(port)
|
||||
if (open) return
|
||||
if (Date.now() - start > ms)
|
||||
throw new Error(`Timed out waiting for port ${port}`)
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => setTimeout(r, 150))
|
||||
}
|
||||
}
|
||||
|
||||
function killProcessTree(child) {
|
||||
if (!child || child.killed) return
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (!child.killed) child.kill('SIGKILL')
|
||||
} catch {}
|
||||
}, 3000).unref?.()
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Math.round(value * 1000) / 1000
|
||||
}
|
||||
|
||||
function colorLuminance(color) {
|
||||
const channels = String(color || '').match(/\d+(?:\.\d+)?/g)
|
||||
if (!channels || channels.length < 3) return null
|
||||
const [r, g, b] = channels.slice(0, 3).map(Number)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
||||
function rectDelta(a, b) {
|
||||
if (!a || !b) return Number.POSITIVE_INFINITY
|
||||
return Math.max(
|
||||
Math.abs((a.left ?? 0) - (b.left ?? 0)),
|
||||
Math.abs((a.top ?? 0) - (b.top ?? 0)),
|
||||
Math.abs((a.width ?? 0) - (b.width ?? 0)),
|
||||
Math.abs((a.height ?? 0) - (b.height ?? 0)),
|
||||
)
|
||||
}
|
||||
|
||||
function maxRectDelta(frames, key, baseline) {
|
||||
return frames.reduce((max, frame) => {
|
||||
return Math.max(max, rectDelta(frame[key], baseline[key]))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function allFramesTrue(frames, selector) {
|
||||
return frames.every((frame) => selector(frame) === true)
|
||||
}
|
||||
|
||||
async function invokeDiffTestApi(page, methodName, ...args) {
|
||||
await page.evaluate(
|
||||
({ methodName, args }) => {
|
||||
const api = window.__streamMonacoDiffTestApi
|
||||
const method = api?.[methodName]
|
||||
if (typeof method !== 'function')
|
||||
throw new Error(`Missing diff test API method: ${methodName}`)
|
||||
return method(...args)
|
||||
},
|
||||
{ methodName, args },
|
||||
)
|
||||
}
|
||||
|
||||
async function primeModifiedScroll(page, top = 240) {
|
||||
await invokeDiffTestApi(page, 'setModifiedScrollTop', top)
|
||||
await page.waitForTimeout(180)
|
||||
const actual = await page.evaluate(() => {
|
||||
return window.__streamMonacoDiffTestApi?.getModifiedScrollTop?.() ?? 0
|
||||
})
|
||||
if (actual < Math.min(40, top * 0.2)) {
|
||||
throw new Error(
|
||||
`Failed to prime modified scroll position. expected around ${top}, got ${actual}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function startTransitionProbe(page, frameCount = 42) {
|
||||
await page.evaluate((requestedFrames) => {
|
||||
const rectData = (node) => {
|
||||
if (!(node instanceof HTMLElement)) return null
|
||||
const rect = node.getBoundingClientRect()
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}
|
||||
}
|
||||
|
||||
const isVisible = (node) => {
|
||||
if (!(node instanceof HTMLElement)) return false
|
||||
const style = getComputedStyle(node)
|
||||
if (style.display === 'none' || style.visibility === 'hidden')
|
||||
return false
|
||||
const rect = node.getBoundingClientRect()
|
||||
return rect.width > 0 && rect.height > 0
|
||||
}
|
||||
|
||||
const countVisible = (selector) =>
|
||||
Array.from(document.querySelectorAll(selector)).filter(isVisible).length
|
||||
|
||||
const readVisibleWidgets = (rootSelector) => {
|
||||
const root = document.querySelector(rootSelector)
|
||||
if (!(root instanceof HTMLElement)) return []
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
return Array.from(root.querySelectorAll('.diff-hidden-lines-widget'))
|
||||
.map((node) => {
|
||||
if (!(node instanceof HTMLElement)) return null
|
||||
const rect = node.getBoundingClientRect()
|
||||
if (
|
||||
rect.height <= 0 ||
|
||||
rect.bottom <= rootRect.top + 1 ||
|
||||
rect.top >= rootRect.bottom - 1
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
topOffset: rect.top - rootRect.top,
|
||||
height: rect.height,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.topOffset - b.topOffset)
|
||||
}
|
||||
|
||||
const readVisibleAnchor = (rootSelector) => {
|
||||
const root = document.querySelector(rootSelector)
|
||||
if (!(root instanceof HTMLElement)) return null
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
const anchor = Array.from(root.querySelectorAll('.line-numbers'))
|
||||
.map((node) => {
|
||||
const rect = node.getBoundingClientRect()
|
||||
const lineNumber = Number.parseInt(node.textContent?.trim() || '', 10)
|
||||
return { lineNumber, rect }
|
||||
})
|
||||
.filter(({ lineNumber, rect }) => {
|
||||
return (
|
||||
Number.isFinite(lineNumber) &&
|
||||
rect.height > 0 &&
|
||||
rect.bottom > rootRect.top + 1 &&
|
||||
rect.top < rootRect.bottom - 1
|
||||
)
|
||||
})
|
||||
.sort((a, b) => a.rect.top - b.rect.top)[0]
|
||||
if (!anchor) return null
|
||||
return {
|
||||
lineNumber: anchor.lineNumber,
|
||||
topOffset: anchor.rect.top - rootRect.top,
|
||||
}
|
||||
}
|
||||
|
||||
const readVisibleLineTops = (rootSelector) => {
|
||||
const root = document.querySelector(rootSelector)
|
||||
if (!(root instanceof HTMLElement)) return {}
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
return Object.fromEntries(
|
||||
Array.from(root.querySelectorAll('.line-numbers'))
|
||||
.map((node) => {
|
||||
if (!(node instanceof HTMLElement)) return null
|
||||
const lineNumber = Number.parseInt(
|
||||
node.textContent?.trim() || '',
|
||||
10,
|
||||
)
|
||||
const rect = node.getBoundingClientRect()
|
||||
if (
|
||||
!Number.isFinite(lineNumber) ||
|
||||
rect.height <= 0 ||
|
||||
rect.bottom <= rootRect.top + 1 ||
|
||||
rect.top >= rootRect.bottom - 1
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return [String(lineNumber), rect.top - rootRect.top]
|
||||
})
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
|
||||
const readVisibleSummaryStates = (selector) => {
|
||||
return Array.from(document.querySelectorAll(selector))
|
||||
.filter(isVisible)
|
||||
.map((node) => {
|
||||
if (!(node instanceof HTMLButtonElement)) return null
|
||||
const style = getComputedStyle(node)
|
||||
return {
|
||||
cursor: style.cursor,
|
||||
pointerEvents: style.pointerEvents,
|
||||
disabled: node.disabled,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const baselineNodes = {
|
||||
host: document.querySelector('.editor-card'),
|
||||
diffRoot: document.querySelector('.monaco-diff-editor'),
|
||||
original: document.querySelector('.editor.original'),
|
||||
modified: document.querySelector('.editor.modified'),
|
||||
overlay: document.querySelector('.stream-monaco-diff-unchanged-overlay'),
|
||||
}
|
||||
|
||||
const capture = () => {
|
||||
const host = document.querySelector('.editor-card')
|
||||
const frame = document.querySelector('.line-info-compare-frame')
|
||||
const diffRoot = document.querySelector('.monaco-diff-editor')
|
||||
const original = document.querySelector('.editor.original')
|
||||
const modified = document.querySelector('.editor.modified')
|
||||
const overlay = document.querySelector(
|
||||
'.stream-monaco-diff-unchanged-overlay',
|
||||
)
|
||||
const backgroundProbe =
|
||||
diffRoot?.querySelector('.monaco-editor .monaco-editor-background') ??
|
||||
diffRoot?.querySelector('.monaco-editor-background')
|
||||
const rootContainer = document.querySelector('.stream-monaco-diff-root')
|
||||
return {
|
||||
frameRect: rectData(frame),
|
||||
hostRect: rectData(host),
|
||||
diffRect: rectData(diffRoot),
|
||||
overlayRect: rectData(overlay),
|
||||
modifiedScrollTop:
|
||||
window.__streamMonacoDiffTestApi?.getModifiedScrollTop?.() ?? 0,
|
||||
modifiedAnchor: readVisibleAnchor('.editor.modified'),
|
||||
modifiedVisibleWidgets: readVisibleWidgets('.editor.modified'),
|
||||
modifiedVisibleLineTops: readVisibleLineTops('.editor.modified'),
|
||||
visibleBridgeRailCount: countVisible(
|
||||
'.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-rail',
|
||||
),
|
||||
visibleBridgeRevealCount: countVisible(
|
||||
'.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-reveal',
|
||||
),
|
||||
visibleMetadataSummaryStates: readVisibleSummaryStates(
|
||||
'.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-summary-metadata',
|
||||
),
|
||||
visibleSimpleSummaryStates: readVisibleSummaryStates(
|
||||
'.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-summary-simple',
|
||||
),
|
||||
bridgeCount: countVisible('.stream-monaco-diff-unchanged-bridge'),
|
||||
backgroundColor:
|
||||
backgroundProbe instanceof HTMLElement
|
||||
? getComputedStyle(backgroundProbe).backgroundColor
|
||||
: '',
|
||||
rootClasses:
|
||||
rootContainer instanceof HTMLElement
|
||||
? Array.from(rootContainer.classList)
|
||||
: [],
|
||||
nodes: {
|
||||
hostSame: host === baselineNodes.host,
|
||||
diffRootSame: diffRoot === baselineNodes.diffRoot,
|
||||
originalSame: original === baselineNodes.original,
|
||||
modifiedSame: modified === baselineNodes.modified,
|
||||
overlaySame: overlay === baselineNodes.overlay,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const baseline = capture()
|
||||
window.__streamMonacoTransitionProbe = new Promise((resolve) => {
|
||||
const frames = []
|
||||
let remaining = Math.max(1, requestedFrames)
|
||||
const tick = () => {
|
||||
frames.push(capture())
|
||||
remaining--
|
||||
if (remaining <= 0) {
|
||||
resolve({ baseline, frames })
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
}, frameCount)
|
||||
}
|
||||
|
||||
async function collectTransition(page, trigger) {
|
||||
await startTransitionProbe(page)
|
||||
await trigger()
|
||||
return page.evaluate(() => window.__streamMonacoTransitionProbe)
|
||||
}
|
||||
|
||||
function summarizeTransition(name, result, options = {}) {
|
||||
const {
|
||||
expectedRootClass,
|
||||
geometryBudget = 0.75,
|
||||
requireGeometryStable = true,
|
||||
expectLuminance = null,
|
||||
requireModifiedScrollStable = true,
|
||||
scrollBudget = 1,
|
||||
requireModifiedAnchorStable = false,
|
||||
anchorBudget = 1,
|
||||
expectedVisibleWidgetHeight = null,
|
||||
expectAnyModifiedLineShiftUpAtLeast = null,
|
||||
expectedVisibleRailCount = null,
|
||||
expectedVisibleRevealCount = null,
|
||||
requireMetadataSummaryNonInteractive = false,
|
||||
requireSimpleSummaryNonInteractive = false,
|
||||
} = options
|
||||
|
||||
const baseline = result.baseline
|
||||
const frames = result.frames
|
||||
const finalFrame = frames.at(-1) ?? baseline
|
||||
const hostMaxDelta = maxRectDelta(frames, 'hostRect', baseline)
|
||||
const diffMaxDelta = maxRectDelta(frames, 'diffRect', baseline)
|
||||
const frameMaxDelta = maxRectDelta(frames, 'frameRect', baseline)
|
||||
const overlayMaxDelta =
|
||||
baseline.overlayRect && finalFrame.overlayRect
|
||||
? maxRectDelta(frames, 'overlayRect', baseline)
|
||||
: 0
|
||||
const modifiedScrollMaxDelta = frames.reduce((max, frame) => {
|
||||
return Math.max(
|
||||
max,
|
||||
Math.abs(
|
||||
(frame.modifiedScrollTop ?? 0) - (baseline.modifiedScrollTop ?? 0),
|
||||
),
|
||||
)
|
||||
}, 0)
|
||||
const anchorLineStable = frames.every((frame) => {
|
||||
return (
|
||||
frame.modifiedAnchor?.lineNumber != null &&
|
||||
frame.modifiedAnchor?.lineNumber === baseline.modifiedAnchor?.lineNumber
|
||||
)
|
||||
})
|
||||
const modifiedAnchorMaxDelta = frames.reduce((max, frame) => {
|
||||
if (
|
||||
baseline.modifiedAnchor?.lineNumber == null ||
|
||||
frame.modifiedAnchor?.lineNumber !== baseline.modifiedAnchor?.lineNumber
|
||||
) {
|
||||
return Number.POSITIVE_INFINITY
|
||||
}
|
||||
return Math.max(
|
||||
max,
|
||||
Math.abs(
|
||||
(frame.modifiedAnchor?.topOffset ?? 0) -
|
||||
(baseline.modifiedAnchor?.topOffset ?? 0),
|
||||
),
|
||||
)
|
||||
}, 0)
|
||||
const finalWidgetHeights = (finalFrame.modifiedVisibleWidgets ?? []).map(
|
||||
(widget) => round(widget.height),
|
||||
)
|
||||
const widgetHeightOk =
|
||||
expectedVisibleWidgetHeight == null
|
||||
? true
|
||||
: finalWidgetHeights.length > 0 &&
|
||||
finalWidgetHeights.every(
|
||||
(height) => Math.abs(height - expectedVisibleWidgetHeight) <= 1,
|
||||
)
|
||||
const baselineVisibleLineTops = baseline.modifiedVisibleLineTops ?? {}
|
||||
const finalVisibleLineTops = finalFrame.modifiedVisibleLineTops ?? {}
|
||||
const sharedLineShiftDeltas = Object.keys(baselineVisibleLineTops)
|
||||
.filter((lineNumber) => lineNumber in finalVisibleLineTops)
|
||||
.map((lineNumber) => {
|
||||
return round(
|
||||
finalVisibleLineTops[lineNumber] - baselineVisibleLineTops[lineNumber],
|
||||
)
|
||||
})
|
||||
const minSharedLineShift =
|
||||
sharedLineShiftDeltas.length > 0 ? Math.min(...sharedLineShiftDeltas) : null
|
||||
const maxSharedLineShift =
|
||||
sharedLineShiftDeltas.length > 0 ? Math.max(...sharedLineShiftDeltas) : null
|
||||
const lineShiftOk =
|
||||
expectAnyModifiedLineShiftUpAtLeast == null
|
||||
? true
|
||||
: sharedLineShiftDeltas.some(
|
||||
(delta) => delta <= -expectAnyModifiedLineShiftUpAtLeast,
|
||||
)
|
||||
const visibleRailCountOk =
|
||||
expectedVisibleRailCount == null
|
||||
? true
|
||||
: (finalFrame.visibleBridgeRailCount ?? 0) === expectedVisibleRailCount
|
||||
const visibleRevealCountOk =
|
||||
expectedVisibleRevealCount == null
|
||||
? true
|
||||
: (finalFrame.visibleBridgeRevealCount ?? 0) ===
|
||||
expectedVisibleRevealCount
|
||||
const metadataSummaryStates = finalFrame.visibleMetadataSummaryStates ?? []
|
||||
const metadataSummaryNonInteractiveOk = requireMetadataSummaryNonInteractive
|
||||
? metadataSummaryStates.length > 0 &&
|
||||
metadataSummaryStates.every((state) => {
|
||||
return (
|
||||
state.cursor !== 'pointer' &&
|
||||
state.pointerEvents === 'none' &&
|
||||
state.disabled === true
|
||||
)
|
||||
})
|
||||
: true
|
||||
const simpleSummaryStates = finalFrame.visibleSimpleSummaryStates ?? []
|
||||
const simpleSummaryNonInteractiveOk = requireSimpleSummaryNonInteractive
|
||||
? simpleSummaryStates.length > 0 &&
|
||||
simpleSummaryStates.every((state) => {
|
||||
return (
|
||||
state.cursor !== 'pointer' &&
|
||||
state.pointerEvents === 'none' &&
|
||||
state.disabled === true
|
||||
)
|
||||
})
|
||||
: true
|
||||
const nodeStable = {
|
||||
host: allFramesTrue(frames, (frame) => frame.nodes.hostSame),
|
||||
diffRoot: allFramesTrue(frames, (frame) => frame.nodes.diffRootSame),
|
||||
original: allFramesTrue(frames, (frame) => frame.nodes.originalSame),
|
||||
modified: allFramesTrue(frames, (frame) => frame.nodes.modifiedSame),
|
||||
overlay: allFramesTrue(frames, (frame) => frame.nodes.overlaySame),
|
||||
}
|
||||
const finalLuminance = colorLuminance(finalFrame.backgroundColor)
|
||||
const luminanceOk =
|
||||
expectLuminance == null
|
||||
? true
|
||||
: expectLuminance === 'dark'
|
||||
? finalLuminance != null && finalLuminance < 120
|
||||
: finalLuminance != null && finalLuminance > 150
|
||||
const geometryOk = requireGeometryStable
|
||||
? hostMaxDelta <= geometryBudget &&
|
||||
diffMaxDelta <= geometryBudget &&
|
||||
frameMaxDelta <= geometryBudget
|
||||
: true
|
||||
const scrollOk = requireModifiedScrollStable
|
||||
? modifiedScrollMaxDelta <= scrollBudget
|
||||
: true
|
||||
const anchorOk = requireModifiedAnchorStable
|
||||
? anchorLineStable && modifiedAnchorMaxDelta <= anchorBudget
|
||||
: true
|
||||
const rootClassOk = expectedRootClass
|
||||
? finalFrame.rootClasses.includes(expectedRootClass)
|
||||
: true
|
||||
const ok =
|
||||
Object.values(nodeStable).every(Boolean) &&
|
||||
geometryOk &&
|
||||
scrollOk &&
|
||||
anchorOk &&
|
||||
widgetHeightOk &&
|
||||
lineShiftOk &&
|
||||
visibleRailCountOk &&
|
||||
visibleRevealCountOk &&
|
||||
metadataSummaryNonInteractiveOk &&
|
||||
simpleSummaryNonInteractiveOk &&
|
||||
rootClassOk &&
|
||||
luminanceOk
|
||||
|
||||
return {
|
||||
name,
|
||||
ok,
|
||||
expectedRootClass,
|
||||
geometryBudget,
|
||||
requireGeometryStable,
|
||||
requireModifiedScrollStable,
|
||||
scrollBudget,
|
||||
requireModifiedAnchorStable,
|
||||
anchorBudget,
|
||||
rootClassOk,
|
||||
scrollOk,
|
||||
anchorOk,
|
||||
widgetHeightOk,
|
||||
lineShiftOk,
|
||||
visibleRailCountOk,
|
||||
visibleRevealCountOk,
|
||||
metadataSummaryNonInteractiveOk,
|
||||
simpleSummaryNonInteractiveOk,
|
||||
luminanceOk,
|
||||
finalLuminance: round(finalLuminance ?? Number.NaN),
|
||||
nodeStable,
|
||||
geometry: {
|
||||
hostMaxDelta: round(hostMaxDelta),
|
||||
diffMaxDelta: round(diffMaxDelta),
|
||||
frameMaxDelta: round(frameMaxDelta),
|
||||
overlayMaxDelta: round(overlayMaxDelta),
|
||||
},
|
||||
scroll: {
|
||||
before: round(baseline.modifiedScrollTop ?? 0),
|
||||
after: round(finalFrame.modifiedScrollTop ?? 0),
|
||||
maxDelta: round(modifiedScrollMaxDelta),
|
||||
},
|
||||
anchor: {
|
||||
before: baseline.modifiedAnchor ?? null,
|
||||
after: finalFrame.modifiedAnchor ?? null,
|
||||
lineStable: anchorLineStable,
|
||||
maxDelta: round(modifiedAnchorMaxDelta),
|
||||
},
|
||||
unchangedWidgets: {
|
||||
expectedHeight: expectedVisibleWidgetHeight,
|
||||
finalHeights: finalWidgetHeights,
|
||||
},
|
||||
lineShift: {
|
||||
min: minSharedLineShift,
|
||||
max: maxSharedLineShift,
|
||||
expectedShiftUpAtLeast: expectAnyModifiedLineShiftUpAtLeast,
|
||||
},
|
||||
bridgeChrome: {
|
||||
expectedVisibleRailCount,
|
||||
actualVisibleRailCount: finalFrame.visibleBridgeRailCount ?? 0,
|
||||
expectedVisibleRevealCount,
|
||||
actualVisibleRevealCount: finalFrame.visibleBridgeRevealCount ?? 0,
|
||||
},
|
||||
metadataSummary: {
|
||||
requireNonInteractive: requireMetadataSummaryNonInteractive,
|
||||
states: metadataSummaryStates,
|
||||
},
|
||||
simpleSummary: {
|
||||
requireNonInteractive: requireSimpleSummaryNonInteractive,
|
||||
states: simpleSummaryStates,
|
||||
},
|
||||
bridgeCount: {
|
||||
before: baseline.bridgeCount,
|
||||
after: finalFrame.bridgeCount,
|
||||
},
|
||||
finalRootClasses: finalFrame.rootClasses,
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const port = process.env.PORT
|
||||
? Number(process.env.PORT)
|
||||
: await findFreePort()
|
||||
const vite = spawn(
|
||||
'pnpm',
|
||||
[
|
||||
'-C',
|
||||
demoDir,
|
||||
'dev',
|
||||
'--host',
|
||||
'127.0.0.1',
|
||||
'--port',
|
||||
String(port),
|
||||
'--strictPort',
|
||||
],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
|
||||
)
|
||||
|
||||
const logs = []
|
||||
vite.stdout.on('data', (d) => logs.push(String(d)))
|
||||
vite.stderr.on('data', (d) => logs.push(String(d)))
|
||||
|
||||
const onExit = () => killProcessTree(vite)
|
||||
process.on('SIGINT', onExit)
|
||||
process.on('SIGTERM', onExit)
|
||||
process.on('exit', onExit)
|
||||
|
||||
try {
|
||||
await waitForPort(port)
|
||||
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1960, height: 1600 },
|
||||
deviceScaleFactor: 2,
|
||||
})
|
||||
|
||||
const pageErrors = []
|
||||
const consoleErrors = []
|
||||
page.on('pageerror', (error) => {
|
||||
pageErrors.push(String(error))
|
||||
})
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') consoleErrors.push(message.text())
|
||||
})
|
||||
|
||||
const url = `http://127.0.0.1:${port}/diff-ux?style=background&scenario=line-info-reference&theme=vitesse-light&appearance=light&unchangedStyle=line-info`
|
||||
await page.goto(url, { waitUntil: 'networkidle' })
|
||||
await page.waitForSelector('.editor-card', { timeout: 20000 })
|
||||
await page.waitForSelector('.monaco-diff-editor', { timeout: 20000 })
|
||||
await page.waitForSelector('.stream-monaco-diff-unchanged-overlay', {
|
||||
timeout: 20000,
|
||||
})
|
||||
await page.waitForTimeout(1400)
|
||||
|
||||
const results = []
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'appearance-light-to-dark',
|
||||
await collectTransition(page, async () => {
|
||||
await page.getByRole('button', { name: /^Dark$/ }).click()
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-appearance-dark',
|
||||
requireGeometryStable: true,
|
||||
expectLuminance: 'dark',
|
||||
requireModifiedScrollStable: true,
|
||||
expectedVisibleWidgetHeight: 32,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'appearance-dark-to-light',
|
||||
await collectTransition(page, async () => {
|
||||
await page.getByRole('button', { name: /^Light$/ }).click()
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-appearance-light',
|
||||
requireGeometryStable: true,
|
||||
expectLuminance: 'light',
|
||||
requireModifiedScrollStable: true,
|
||||
expectedVisibleWidgetHeight: 32,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'style-line-info-to-basic',
|
||||
await collectTransition(page, async () => {
|
||||
await page.getByRole('button', { name: /^Line Info Basic$/ }).click()
|
||||
}),
|
||||
{
|
||||
expectedRootClass:
|
||||
'stream-monaco-diff-unchanged-style-line-info-basic',
|
||||
requireGeometryStable: true,
|
||||
requireModifiedScrollStable: true,
|
||||
expectedVisibleWidgetHeight: 32,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'style-basic-to-metadata',
|
||||
await collectTransition(page, async () => {
|
||||
await page
|
||||
.getByRole('button', { name: /^Metadata$/ })
|
||||
.first()
|
||||
.click()
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-unchanged-style-metadata',
|
||||
requireGeometryStable: true,
|
||||
requireModifiedScrollStable: true,
|
||||
expectedVisibleWidgetHeight: 32,
|
||||
expectedVisibleRailCount: 0,
|
||||
expectedVisibleRevealCount: 0,
|
||||
requireMetadataSummaryNonInteractive: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'set-diff-models-same-content',
|
||||
await collectTransition(page, async () => {
|
||||
await invokeDiffTestApi(page, 'swapModelsSameContent')
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-unchanged-style-metadata',
|
||||
requireGeometryStable: true,
|
||||
requireModifiedScrollStable: false,
|
||||
requireModifiedAnchorStable: true,
|
||||
anchorBudget: 1,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'set-diff-models-changed-content',
|
||||
await collectTransition(page, async () => {
|
||||
await invokeDiffTestApi(page, 'swapModelsChangedContent')
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-unchanged-style-metadata',
|
||||
requireGeometryStable: true,
|
||||
requireModifiedScrollStable: false,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await primeModifiedScroll(page)
|
||||
results.push(
|
||||
summarizeTransition(
|
||||
'style-metadata-to-simple',
|
||||
await collectTransition(page, async () => {
|
||||
await page
|
||||
.getByRole('button', { name: /^Simple$/ })
|
||||
.first()
|
||||
.click()
|
||||
}),
|
||||
{
|
||||
expectedRootClass: 'stream-monaco-diff-unchanged-style-simple',
|
||||
requireGeometryStable: false,
|
||||
requireModifiedScrollStable: true,
|
||||
expectedVisibleRailCount: 0,
|
||||
expectedVisibleRevealCount: 0,
|
||||
requireSimpleSummaryNonInteractive: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await browser.close()
|
||||
|
||||
const ok =
|
||||
pageErrors.length === 0 &&
|
||||
consoleErrors.length === 0 &&
|
||||
results.every((result) => result.ok)
|
||||
|
||||
const output = {
|
||||
ok,
|
||||
url,
|
||||
pageErrors,
|
||||
consoleErrors,
|
||||
results,
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(output, null, 2))
|
||||
|
||||
if (!ok) {
|
||||
console.error('Transition validation failed; recent Vite logs:\n')
|
||||
console.error(logs.slice(-60).join(''))
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transition validation startup/runtime error:')
|
||||
console.error(error)
|
||||
if (logs.length > 0) {
|
||||
console.error('Recent Vite logs:\n')
|
||||
console.error(logs.slice(-60).join(''))
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
killProcessTree(vite)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user