mirror of
https://github.com/screenci/screenci.git
synced 2026-09-19 08:57:46 +08:00
editor codegen for options and narration, dev source watching
- optionsEdit and narrationEdit records (timeline edits v4): render/record option snapshots merge into .renderOptions/.recordOptions builder calls (section added when missing), narration cue values merge into the video.narration declaration via the new setNarrationValue codemod (content-major and language-major forms, conversion to language-major on a non-default language edit, names-only declarations stay app-managed) - applyCodegenRequest wires the new records into planCodeSync - screenci dev watches the managed videos' test sources and screenci.config.ts (src/devWatch.ts): a real content change re-records the affected videos as previews through a new machine-local record job in the listen loop; codegen self-writes are baseline-suppressed; --no-watch disables it - docs: cli.mdx dev watching, editor.md option/narration codegen coverage
This commit is contained in:
+163
@@ -507,6 +507,169 @@ describe('runDevListenLoop', () => {
|
||||
runDevListenLoop(config, deps, 'lst_1', controller)
|
||||
).rejects.toBeInstanceOf(DevAuthError)
|
||||
})
|
||||
|
||||
it('runs a local record request without touching the trigger channel', async () => {
|
||||
const controller = { stopped: false }
|
||||
const runLocalRecord = vi.fn(async () => {})
|
||||
let request: { videoNames: string[] } | null = {
|
||||
videoNames: ['Intro video'],
|
||||
}
|
||||
const takeLocalRequest = vi.fn(() => {
|
||||
const taken = request
|
||||
request = null
|
||||
return taken
|
||||
})
|
||||
const deps = makeDeps({ runLocalRecord, takeLocalRequest })
|
||||
deps.fetchMock.mockImplementation(async (url: string) => {
|
||||
if (url.endsWith('/cli/dev/poll')) {
|
||||
if (runLocalRecord.mock.calls.length > 0) controller.stopped = true
|
||||
return jsonResponse({})
|
||||
}
|
||||
return jsonResponse({ ok: true })
|
||||
})
|
||||
|
||||
await runDevListenLoop(config, deps, 'lst_1', controller)
|
||||
|
||||
expect(runLocalRecord).toHaveBeenCalledWith(
|
||||
['Intro video'],
|
||||
expect.any(AbortSignal)
|
||||
)
|
||||
const triggerReports = deps.fetchMock.mock.calls.filter(([url]) =>
|
||||
(url as string).endsWith('/cli/dev/report')
|
||||
)
|
||||
expect(triggerReports).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('merges queued local requests into one union re-record', async () => {
|
||||
const controller = { stopped: false }
|
||||
let clock = 0
|
||||
const recorded: string[][] = []
|
||||
let releaseFirst = () => {}
|
||||
const runLocalRecord = vi.fn((videoNames: string[]) => {
|
||||
recorded.push(videoNames)
|
||||
if (recorded.length === 1) {
|
||||
return new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve
|
||||
})
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
const requests: Array<{ videoNames: string[] }> = [
|
||||
{ videoNames: ['A'] },
|
||||
{ videoNames: ['B'] },
|
||||
{ videoNames: ['C', 'B'] },
|
||||
]
|
||||
const takeLocalRequest = vi.fn(() => requests.shift() ?? null)
|
||||
const deps = makeDeps({
|
||||
runLocalRecord,
|
||||
takeLocalRequest,
|
||||
now: () => clock,
|
||||
})
|
||||
deps.fetchMock.mockImplementation(async (url: string) => {
|
||||
if (url.endsWith('/cli/dev/poll')) {
|
||||
// Keep the first record outside the kill window so newcomers queue.
|
||||
clock = 60_000
|
||||
if (recorded.length >= 1 && requests.length === 0) {
|
||||
releaseFirst()
|
||||
}
|
||||
if (recorded.length >= 2) controller.stopped = true
|
||||
return jsonResponse({})
|
||||
}
|
||||
return jsonResponse({ ok: true })
|
||||
})
|
||||
|
||||
await runDevListenLoop(config, deps, 'lst_1', controller)
|
||||
|
||||
expect(recorded[0]).toEqual(['A'])
|
||||
expect(recorded[1]).toEqual(['B', 'C'])
|
||||
})
|
||||
|
||||
it('kills a young trigger record when a local request arrives', async () => {
|
||||
const controller = { stopped: false }
|
||||
let clock = 0
|
||||
const runRecord = vi.fn(
|
||||
(_t: DevTrigger, signal?: AbortSignal) =>
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
// Like the real runner, honor a signal aborted before the spawn.
|
||||
if (signal?.aborted) {
|
||||
reject(new Error('Record aborted'))
|
||||
return
|
||||
}
|
||||
signal?.addEventListener('abort', () =>
|
||||
reject(new Error('Record aborted'))
|
||||
)
|
||||
})
|
||||
)
|
||||
const runLocalRecord = vi.fn(async () => {})
|
||||
const requests: Array<{ videoNames: string[] } | null> = [
|
||||
null,
|
||||
{ videoNames: ['Intro video'] },
|
||||
]
|
||||
const takeLocalRequest = vi.fn(() =>
|
||||
requests.length > 0 ? (requests.shift() ?? null) : null
|
||||
)
|
||||
const deps = makeDeps({
|
||||
runRecord,
|
||||
runLocalRecord,
|
||||
takeLocalRequest,
|
||||
now: () => clock,
|
||||
})
|
||||
let polls = 0
|
||||
deps.fetchMock.mockImplementation(async (url: string) => {
|
||||
if (url.endsWith('/cli/dev/poll')) {
|
||||
polls += 1
|
||||
if (polls === 1) return jsonResponse({ trigger })
|
||||
clock = 5_000 // Inside the 10s kill window.
|
||||
if (runLocalRecord.mock.calls.length > 0) controller.stopped = true
|
||||
return jsonResponse({})
|
||||
}
|
||||
return jsonResponse({ ok: true })
|
||||
})
|
||||
|
||||
await runDevListenLoop(config, deps, 'lst_1', controller)
|
||||
|
||||
expect(runLocalRecord).toHaveBeenCalledWith(
|
||||
['Intro video'],
|
||||
expect.any(AbortSignal)
|
||||
)
|
||||
// The superseded trigger reported failed.
|
||||
const reports = deps.fetchMock.mock.calls
|
||||
.filter(([url]) => (url as string).endsWith('/cli/dev/report'))
|
||||
.map(([, init]) => JSON.parse((init as RequestInit).body as string))
|
||||
expect(reports.some((r) => r.state === 'failed')).toBe(true)
|
||||
})
|
||||
|
||||
it('aborts an active local record on shutdown', async () => {
|
||||
const controller = { stopped: false }
|
||||
let aborted = false
|
||||
const runLocalRecord = vi.fn(
|
||||
(_names: string[], signal?: AbortSignal) =>
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
signal?.addEventListener('abort', () => {
|
||||
aborted = true
|
||||
reject(new Error('Record aborted'))
|
||||
})
|
||||
})
|
||||
)
|
||||
let request: { videoNames: string[] } | null = { videoNames: ['A'] }
|
||||
const takeLocalRequest = vi.fn(() => {
|
||||
const taken = request
|
||||
request = null
|
||||
return taken
|
||||
})
|
||||
const deps = makeDeps({ runLocalRecord, takeLocalRequest })
|
||||
deps.fetchMock.mockImplementation(async (url: string) => {
|
||||
if (url.endsWith('/cli/dev/poll')) {
|
||||
if (runLocalRecord.mock.calls.length > 0) controller.stopped = true
|
||||
return jsonResponse({})
|
||||
}
|
||||
return jsonResponse({ ok: true })
|
||||
})
|
||||
|
||||
await runDevListenLoop(config, deps, 'lst_1', controller)
|
||||
|
||||
expect(aborted).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reportDevTrigger and deregisterDevListener', () => {
|
||||
|
||||
Reference in New Issue
Block a user