mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-30 18:44:13 +08:00
fix(galaxy): expand timeline for readable history
This commit is contained in:
@@ -24,7 +24,9 @@ const assert = require('node:assert/strict')
|
||||
history.append({ schemaVersion: 1, ownerId, sessionId, rowOffset: 0, totalRows: 7, nextCursor: null, source: 'qlog', legend,
|
||||
rows: [[0, 20, 'chill'], [20, 40, 'aol'], [40, 70, 'experimental'], [55, 56, 'error'],
|
||||
[55.5, 55.5, 'bookmark'], [80, 120, 'chill'], [100, 100, 'bookmark']] }, status)
|
||||
document.body.innerHTML = '<main class="gx-content"><div id="fixture"></div></main>'
|
||||
document.body.innerHTML = `<main class="gx-content"><div id="workspace" class="gx-route-workspace">
|
||||
<div class="gx-route-workspace__body"><section class="gx-card gx-route-workspace__viewer"><div id="fixture"></div></section>
|
||||
<aside class="gx-card gx-route-workspace__files"><h3>Logs & files</h3><p>Segment 0</p></aside></div></div></main>`
|
||||
window.replayControls = []
|
||||
window.fixtureVM = createApp({ ...ReplayViewer,
|
||||
data: () => ({ ...ReplayViewer.data(), replay: status, replayTimeline: markRaw(history), replayVideoReady: true,
|
||||
@@ -38,22 +40,58 @@ const assert = require('node:assert/strict')
|
||||
},
|
||||
beforeUnmount() {},
|
||||
}, { clientId: 'c'.repeat(32), routeId: 'fixture', unauthorized() {} }).mount('#fixture')
|
||||
window.loadLongReplay = () => {
|
||||
const vm = window.fixtureVM, status = { ...vm.replay, end: 1968, position: 5, seekRanges: [[0, 1968]] }
|
||||
const rows = Array.from({ length: 328 }, (_, i) => [i * 6, (i + 1) * 6, i % 3 ? 'chill' : 'aol'])
|
||||
const history = new ReplayHistory(status.ownerId, status.sessionId)
|
||||
for (let offset = 0; offset < rows.length; offset += 256) history.append({ schemaVersion: 1, ownerId: status.ownerId,
|
||||
sessionId: status.sessionId, rowOffset: offset, totalRows: rows.length, source: 'qlog', legend,
|
||||
rows: rows.slice(offset, offset + 256), nextCursor: offset + 256 < rows.length ? offset + 256 : null }, status)
|
||||
vm.replay = status; vm.replayTimeline = markRaw(history)
|
||||
}
|
||||
})
|
||||
for (const width of [1280, 390]) {
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
const geometry = async () => page.locator('.gx-replay__media-controls').evaluate(media => {
|
||||
const bounds = element => {
|
||||
const { x, y, width, height } = element.getBoundingClientRect()
|
||||
return { x, y, width, height }
|
||||
}
|
||||
const track = media.querySelector('.gx-replay__track'), canvas = track.querySelector('canvas'), style = getComputedStyle(media)
|
||||
return { media: bounds(media), caption: bounds(media.querySelector('.gx-replay__timeline > span')),
|
||||
track: bounds(track), canvas: bounds(canvas), input: bounds(track.querySelector('input')),
|
||||
controls: bounds(media.querySelector('.gx-replay__controls')), pixels: canvas.width,
|
||||
available: media.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
|
||||
overflow: document.documentElement.scrollWidth > innerWidth }
|
||||
})
|
||||
const assertLayout = async width => {
|
||||
await page.waitForFunction(width => {
|
||||
const canvas = document.querySelector('.gx-replay__track canvas')
|
||||
return canvas?.height === (width <= 600 ? 44 : 54)
|
||||
const canvas = document.querySelector('.gx-replay__rail canvas')
|
||||
return canvas?.height === (width <= 600 ? 52 : 62) && Math.abs(canvas.width - canvas.getBoundingClientRect().width * 2) <= 1
|
||||
}, width)
|
||||
const geometry = await page.locator('.gx-replay__track').evaluate(track => {
|
||||
const canvas = track.querySelector('canvas'), input = track.querySelector('input')
|
||||
const bounds = element => ({ width: element.getBoundingClientRect().width, height: element.getBoundingClientRect().height })
|
||||
return { canvas: bounds(canvas), input: bounds(input), pixels: canvas.width, overflow: document.documentElement.scrollWidth > innerWidth }
|
||||
})
|
||||
assert.equal(geometry.canvas.height, width <= 600 ? 22 : 27)
|
||||
assert.ok(geometry.input.height >= 44, 'the rail keeps its generous seek hit area')
|
||||
assert.ok(Math.abs(geometry.pixels - geometry.canvas.width * 2) <= 1)
|
||||
assert.equal(geometry.overflow, false)
|
||||
const g = await geometry()
|
||||
assert.equal(g.canvas.height, width <= 600 ? 26 : 31)
|
||||
assert.equal(g.input.height, 48)
|
||||
assert.ok(Math.abs(g.track.width - g.available) <= 1, 'timeline uses the full control area')
|
||||
assert.ok(Math.abs(g.canvas.width - (g.track.width - 16)) <= 1)
|
||||
assert.ok(g.track.y >= g.caption.y + g.caption.height - 1, 'caption is above the track')
|
||||
assert.ok(g.controls.y >= g.track.y + g.track.height - 1, 'buttons are below the track')
|
||||
assert.ok(Math.abs(g.pixels - g.canvas.width * 2) <= 1)
|
||||
assert.equal(g.overflow, false)
|
||||
}
|
||||
for (const [embedded, width] of [[false, 1280], [true, 1280], [true, 1000], [true, 390]]) {
|
||||
await page.evaluate(embedded => {
|
||||
const workspace = document.getElementById('workspace'), body = workspace.firstElementChild, viewer = body.firstElementChild
|
||||
workspace.className = embedded ? 'gx-route-workspace' : ''
|
||||
body.className = embedded ? 'gx-route-workspace__body' : ''
|
||||
viewer.className = embedded ? 'gx-card gx-route-workspace__viewer' : ''
|
||||
body.lastElementChild.hidden = !embedded
|
||||
}, embedded)
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
await assertLayout(width)
|
||||
if (embedded && width === 1280) {
|
||||
const viewer = await page.locator('.gx-route-workspace__viewer').boundingBox()
|
||||
const files = await page.locator('.gx-route-workspace__files').boundingBox()
|
||||
assert.ok(files.x >= viewer.x + viewer.width, 'desktop fixture includes the real files sidebar')
|
||||
}
|
||||
const slider = page.getByRole('slider', { name: 'Replay route position' })
|
||||
const box = await slider.boundingBox()
|
||||
const x = box.x + box.width * .5, y = box.y + box.height / 2
|
||||
@@ -77,7 +115,7 @@ const assert = require('node:assert/strict')
|
||||
assert.ok(bounds.y + bounds.height < box.y - 20, 'preview stays above the touch target')
|
||||
assert.equal(await page.locator('.gx-replay__rail canvas').evaluate(canvas => canvas.toDataURL()), bitmap, 'scrubbing reuses the canvas')
|
||||
assert.equal(await page.evaluate(() => window.replayPaints), 0, 'scrubbing never repaints the history')
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-scrub-${width}.png` })
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-scrub-${embedded ? 'workspace' : 'standalone'}-${width}.png` })
|
||||
await page.mouse.up()
|
||||
assert.equal(await preview.count(), 0)
|
||||
assert.equal(await page.evaluate(() => window.replayControls.length), 1, 'release commits exactly once')
|
||||
@@ -117,16 +155,16 @@ const assert = require('node:assert/strict')
|
||||
assert.match(await slider.getAttribute('aria-valuetext'), /Experimental/)
|
||||
await page.locator('summary', { hasText: 'Timeline colors' }).click()
|
||||
assert.equal(await page.locator('.gx-replay__legend span').count(), 5)
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-${width}.png` })
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-${embedded ? 'workspace' : 'standalone'}-${width}.png` })
|
||||
await page.locator('summary', { hasText: 'Timeline colors' }).click()
|
||||
await page.getByRole('button', { name: 'Fullscreen video' }).click()
|
||||
await page.waitForFunction(() => document.fullscreenElement &&
|
||||
document.querySelector('.gx-replay__track canvas').height === (innerWidth <= 600 ? 44 : 54))
|
||||
await page.waitForFunction(() => document.fullscreenElement)
|
||||
await assertLayout(width)
|
||||
const fullscreenHeight = await page.locator('.gx-replay__track canvas').evaluate(canvas => ({
|
||||
actual: canvas.getBoundingClientRect().height, expected: innerWidth <= 600 ? 22 : 27,
|
||||
actual: canvas.getBoundingClientRect().height, expected: innerWidth <= 600 ? 26 : 31,
|
||||
}))
|
||||
assert.equal(fullscreenHeight.actual, fullscreenHeight.expected, 'fullscreen preserves restrained rail sizing')
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-fullscreen-${width}.png` })
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-web-fullscreen-${embedded ? 'workspace' : 'standalone'}-${width}.png` })
|
||||
await page.evaluate(() => document.exitFullscreen())
|
||||
}
|
||||
await page.setViewportSize({ width: 390, height: 900 })
|
||||
@@ -142,7 +180,19 @@ const assert = require('node:assert/strict')
|
||||
assert.equal(await page.locator('.gx-replay__preview').count(), 0)
|
||||
assert.equal(await page.evaluate(() => window.replayControls.length), cancel ? 0 : 1, 'touch end commits once; cancellation never commits')
|
||||
}
|
||||
await page.evaluate(() => window.loadLongReplay())
|
||||
for (const width of [1280, 390, 320]) {
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
await assertLayout(width)
|
||||
assert.match(await page.locator('.gx-replay__timeline').textContent(), /32:48/)
|
||||
if (process.env.REPLAY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.REPLAY_SCREENSHOT_DIR}/replay-long-route-${width}.png` })
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
window.fixtureVM.replayTimeline = null
|
||||
window.fixtureVM.replayHistoryNotice = 'History unavailable; the recorded route can still be replayed.'
|
||||
})
|
||||
await assertLayout(320)
|
||||
assert.deepEqual(errors, [])
|
||||
await browser.close()
|
||||
console.log('Replay timeline browser: sizing, cached drag preview, edges, touch, cancellation, authority changes, fullscreen and keyboard passed')
|
||||
console.log('Replay timeline browser: embedded/standalone layout, dense long routes, cached dragging, touch, cancellation, fullscreen and keyboard passed')
|
||||
})().catch(error => { console.error(error); process.exit(1) })
|
||||
|
||||
@@ -29,25 +29,25 @@ assert.throws(() => {
|
||||
}, /History unavailable/, 'a late page cannot change the declared total')
|
||||
|
||||
const painted = []
|
||||
let height = 27, tick = 5
|
||||
let height = 31, tick = 5
|
||||
globalThis.getComputedStyle = () => ({getPropertyValue: key => key === '--replay-tick' ? String(tick) : '2'})
|
||||
const canvas = {getBoundingClientRect: () => ({width: 100, height}), getContext: () => ({
|
||||
setTransform() {}, set fillStyle(value) { this.color = value }, fillRect(x, y, w, h) { painted.push({x, y, w, h, color: this.color}) },
|
||||
})}
|
||||
drawReplayTimeline(canvas, status, history, 2)
|
||||
assert.equal(canvas.width, 200)
|
||||
assert.equal(canvas.height, 54)
|
||||
assert.equal(canvas.height, 62)
|
||||
const red = painted.findIndex(row => row.color === 'rgb(13 20 30)')
|
||||
const mark = painted.findIndex(row => row.color === 'rgb(14 20 30)')
|
||||
assert.ok(red > painted.findIndex(row => row.color === 'rgb(12 20 30)'), 'red is painted after all mode blocks')
|
||||
assert.ok(mark > red && painted[mark].y < painted[red].y, 'bookmark occupies its own lane')
|
||||
assert.ok(painted[red].w >= .5, 'a brief error occupies at least one physical pixel at DPR2')
|
||||
assert.equal(painted[mark].w, 3, 'bookmark remains three CSS pixels wide at DPR2')
|
||||
assert.equal(painted[red].h, 20)
|
||||
height = 22; tick = 4
|
||||
assert.equal(painted[red].h, 24)
|
||||
height = 26; tick = 4
|
||||
drawReplayTimeline(canvas, status, history, 1)
|
||||
assert.equal(canvas.height, 22)
|
||||
assert.equal(painted.at(-2).h, 16)
|
||||
assert.equal(canvas.height, 26)
|
||||
assert.equal(painted.at(-2).h, 20)
|
||||
console.log('Replay timeline: atomic pages, validation, independent alerts, gaps, pixel precedence and compact sizing passed')
|
||||
|
||||
// A late page must not restore history after ownership or offroad authority disappears.
|
||||
|
||||
@@ -1318,9 +1318,8 @@ button.gx-chip:hover {
|
||||
.gx-route-workspace__job progress { flex: 1 1 8rem; max-width: 16rem; accent-color: var(--primary); }
|
||||
.gx-route-workspace .gx-replay { display: grid; gap: var(--sp-2); }
|
||||
.gx-route-workspace .gx-replay > .gx-notice, .gx-route-workspace .gx-replay > .gx-note, .gx-route-workspace .gx-replay > .gx-btn, .gx-route-workspace .gx-replay__gaps { margin-inline: var(--sp-3); }
|
||||
.gx-route-workspace .gx-replay__media-controls { position: static; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--sp-3) var(--sp-2); }
|
||||
.gx-route-workspace .gx-replay__timeline { flex: 1 1 16rem; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--sp-2); }
|
||||
.gx-route-workspace .gx-replay__controls { flex: 1 1 18rem; gap: var(--sp-1); }
|
||||
.gx-route-workspace .gx-replay__media-controls { position: static; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
|
||||
.gx-route-workspace .gx-replay__controls { gap: var(--sp-1); }
|
||||
.gx-route-workspace .gx-replay__controls .gx-btn { font-size: var(--fs-sm); }
|
||||
.gx-route-workspace .gx-replay__video { aspect-ratio: 2 / 1; max-height: 50dvh; object-fit: contain; }
|
||||
.gx-route-workspace .gx-replay__player:fullscreen .gx-replay__screen { flex: 1; min-height: 0; display: flex; align-items: center; }
|
||||
@@ -1743,7 +1742,7 @@ progress { max-width: 100%; accent-color: var(--primary); }
|
||||
.gx-replay__controls .gx-btn:focus-visible, .gx-replay__fullscreen:focus-visible { outline: 2px solid var(--replay-accent); outline-offset: 2px; }
|
||||
.gx-replay__fullscreen { position: absolute; top: 8px; right: 8px; width: var(--touch-target); background: rgb(8 8 10 / 82%); }
|
||||
.gx-replay__timeline { display: grid; gap: 0; font-size: clamp(12px, 1.3vw, 16px); }
|
||||
.gx-replay__track { position: relative; display: block; height: var(--touch-target); --replay-thumb: 16px; --replay-rail: 20px; --replay-tick: 5px; --replay-gap: 2px; }
|
||||
.gx-replay__track { position: relative; display: block; height: 48px; --replay-thumb: 16px; --replay-rail: 24px; --replay-tick: 5px; --replay-gap: 2px; }
|
||||
.gx-replay__rail { position: absolute; left: calc(var(--replay-thumb) / 2); right: calc(var(--replay-thumb) / 2); height: calc(var(--replay-rail) + var(--replay-tick) + var(--replay-gap)); top: 50%; transform: translateY(-50%); pointer-events: none; }
|
||||
.gx-replay__rail canvas { display: block; width: 100%; height: 100%; }
|
||||
.gx-replay__playhead { position: absolute; left: var(--replay-position); top: calc(var(--replay-tick) + var(--replay-gap) - 1px); width: 2px; height: calc(var(--replay-rail) + 2px); transform: translateX(-50%); background: #fff; }
|
||||
@@ -1765,7 +1764,7 @@ progress { max-width: 100%; accent-color: var(--primary); }
|
||||
.gx-replay__player:fullscreen { display: flex; flex-direction: column; justify-content: center; border-radius: 0; }
|
||||
.gx-replay__player:fullscreen .gx-replay__video { max-height: 100dvh; }
|
||||
@media (max-width: 600px) {
|
||||
.gx-replay__track { --replay-rail: 16px; --replay-tick: 4px; }
|
||||
.gx-replay__track { --replay-rail: 20px; --replay-tick: 4px; }
|
||||
.gx-replay__media-controls { position: static; padding-inline: 4px; }
|
||||
.gx-replay__controls { gap: 4px; }
|
||||
.gx-replay__player:fullscreen .gx-replay__video { min-height: 0; flex: 1; }
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "2a9adededf0b923acb27758cbd871bab480655d96dd4de529570442019412162", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "0461dbb098afa599b01e97b034e3abb4ce64f2cff182d133c7c29fddad5ec0dd", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user