feat(screenci): add verbose config logging and cue warning

This commit is contained in:
Olli Paloviita
2026-05-21 16:03:32 +03:00
parent 8f8c972396
commit 9dd7c6d1f7
4 changed files with 117 additions and 11 deletions
+62
View File
@@ -231,6 +231,38 @@ describe('CLI', () => {
})
)
})
it('should only log the config path in verbose mode', async () => {
process.argv = ['node', 'cli.js', 'record', '--verbose']
process.env.VITE_APP_BASE_URL = 'https://example.com'
mockReadFile.mockImplementation(async (path: string | URL) => {
if (String(path).endsWith('screenci.config.ts')) {
return `export default defineConfig({ projectName: 'Test Project' })`
}
if (String(path).endsWith('package.json')) {
return JSON.stringify({ version: '0.0.32' })
}
return ''
})
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ projectId: 'project_123' }),
text: vi.fn().mockResolvedValue(''),
})
mockSpawn.mockImplementation(() => {
process.nextTick(() => mockChildProcess.emit('close', 0))
return mockChildProcess as unknown as ChildProcess
})
const { main } = await import('./cli')
await main()
expect(loggerInfoSpy).toHaveBeenCalledWith(
expect.stringContaining('Using config:')
)
})
})
describe('test command', () => {
@@ -272,6 +304,36 @@ describe('CLI', () => {
)
})
it('should not log the config path by default', async () => {
process.argv = ['node', 'cli.js', 'test']
mockSpawn.mockImplementation(() => {
process.nextTick(() => mockChildProcess.emit('close', 0))
return mockChildProcess as unknown as ChildProcess
})
const { main } = await import('./cli')
await main()
expect(loggerInfoSpy).not.toHaveBeenCalledWith(
expect.stringContaining('Using config:')
)
})
it('should log the config path in verbose mode', async () => {
process.argv = ['node', 'cli.js', 'test', '--verbose']
mockSpawn.mockImplementation(() => {
process.nextTick(() => mockChildProcess.emit('close', 0))
return mockChildProcess as unknown as ChildProcess
})
const { main } = await import('./cli')
await main()
expect(loggerInfoSpy).toHaveBeenCalledWith(
expect.stringContaining('Using config:')
)
})
it('should not warn when configured envFile is missing', async () => {
process.argv = [
'node',
+12 -5
View File
@@ -1851,11 +1851,12 @@ export async function main() {
program
.command('record [playwrightArgs...]')
.description('Record videos using Playwright')
.option('-v, --verbose', 'verbose output')
.allowUnknownOption(true)
.action(async () => {
const parsed = parseRecordCliArgs(getSubcommandArgv('record'))
await run('record', parsed.otherArgs, parsed.configPath)
await run('record', parsed.otherArgs, parsed.configPath, parsed.verbose)
if (process.env.SCREENCI_RECORDING === 'true') return
@@ -1916,6 +1917,7 @@ export async function main() {
program
.command('test [playwrightArgs...]')
.description('Run Playwright test with screenci.config.ts')
.option('-v, --verbose', 'verbose output')
.allowUnknownOption(true)
.action(async () => {
const parsed = parseConfigCliArgs(getSubcommandArgv('test'))
@@ -1937,7 +1939,7 @@ export async function main() {
}
}
await run('test', parsed.otherArgs, parsed.configPath)
await run('test', parsed.otherArgs, parsed.configPath, parsed.verbose)
if (process.env.SCREENCI_RECORDING === 'true') return
@@ -2103,9 +2105,11 @@ function parseRecordCliArgs(args: string[]): {
function parseConfigCliArgs(args: string[]): {
configPath: string | undefined
verbose: boolean
otherArgs: string[]
} {
let configPath: string | undefined
let verbose = false
const otherArgs: string[] = []
for (let i = 0; i < args.length; i++) {
@@ -2119,12 +2123,14 @@ function parseConfigCliArgs(args: string[]): {
}
configPath = nextArg
i++
} else if (arg === '--verbose' || arg === '-v') {
verbose = true
} else {
otherArgs.push(arg)
}
}
return { configPath, otherArgs }
return { configPath, verbose, otherArgs }
}
function validateArgs(args: string[]): void {
@@ -2197,7 +2203,8 @@ function spawnInherited(
async function run(
command: 'record' | 'test',
additionalArgs: string[],
customConfigPath?: string
customConfigPath?: string,
verbose = false
) {
const configPath = findScreenCIConfig(customConfigPath)
@@ -2223,7 +2230,7 @@ async function run(
clearDirectory(screenciDir)
}
if (process.env.SCREENCI_RECORDING !== 'true') {
if (verbose && process.env.SCREENCI_RECORDING !== 'true') {
logger.info(`Using config: ${configPath}`)
}
+28
View File
@@ -13,6 +13,7 @@ import type { IEventRecorder } from './events.js'
import type { RecordingEvent } from './events.js'
import type { CustomVoiceRef } from './voices.js'
import { modelTypes, voices } from './voices.js'
import { logger } from './logger.js'
function createMockRecorder(): IEventRecorder {
return {
@@ -46,10 +47,12 @@ const singleLangInput = {
describe('createNarration', () => {
let recorder: IEventRecorder
let order: string[]
let warnSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
order = []
recorder = createMockRecorder()
warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {})
resetCueChain()
;(recorder.addCueStart as ReturnType<typeof vi.fn>).mockImplementation(
(text: string, _name: string, _config: unknown, translations: unknown) =>
@@ -63,6 +66,7 @@ describe('createNarration', () => {
})
afterEach(() => {
warnSpy.mockRestore()
setActiveCueRecorder(null)
setActiveHideRecorder(null)
setSleepFn((ms) => {
@@ -123,6 +127,9 @@ describe('createNarration', () => {
await cues.outro.start()
expect(order).toEqual(['cueEnd', 'sleep', 'sleep', 'cueStart(multilang)'])
expect(warnSpy).toHaveBeenCalledWith(
'[screenci] Cue "intro" was started with .start() and auto-ended when cue "outro" started. Call .end() explicitly before starting the next narration cue.'
)
})
it('start() on one cue then end() on another throws', async () => {
@@ -145,6 +152,27 @@ describe('createNarration', () => {
expect(order).toEqual(['cueEnd', 'sleep', 'sleep', 'cueStart(multilang)'])
})
it('warns when a cue started with .start() is followed by a callable cue', async () => {
const cues = createNarration(singleLangInput)
await cues.intro.start()
await cues.outro()
expect(warnSpy).toHaveBeenCalledWith(
'[screenci] Cue "intro" was started with .start() and auto-ended when cue "outro" started. Call .end() explicitly before starting the next narration cue.'
)
})
it('does not warn when a callable cue is followed by another cue', async () => {
const cues = createNarration(singleLangInput)
await cues.intro()
await cues.outro.start()
expect(warnSpy).not.toHaveBeenCalled()
})
it('end() throws after a callable cue run has completed', async () => {
const cues = createNarration(singleLangInput)
+15 -6
View File
@@ -15,6 +15,7 @@ import type {
} from './voices.js'
import { isCustomVoiceRef } from './voices.js'
import { isInsideHide } from './hide.js'
import { logger } from './logger.js'
import { access, readFile } from 'fs/promises'
import { createHash } from 'crypto'
import { dirname, resolve } from 'path'
@@ -39,6 +40,7 @@ let activeCueName: string | null = null
let activeCueRun: {
finished: Promise<void>
resolveFinished: () => void
startedWithExplicitStart: boolean
} | null = null
const usedCueNames = new Set<string>()
const registeredCustomVoiceRefs = new Set<CustomVoiceRef>()
@@ -138,8 +140,13 @@ function toRecordedVoice(
* Auto-ends any currently active cue before starting a new one.
* Called internally at the start of every narration controller.
*/
function cueAutoEnd(): void {
function cueAutoEnd(nextCueName: string): void {
if (activeCueRun === null || activeRecorder === null) return
if (activeCueRun.startedWithExplicitStart && activeCueName !== null) {
logger.warn(
`[screenci] Cue "${activeCueName}" was started with .start() and auto-ended when cue "${nextCueName}" started. Call .end() explicitly before starting the next narration cue.`
)
}
activeRecorder.addCueEnd('auto')
sleepFn(2 * ONE_FRAME_MS)
activeCueRun.resolveFinished()
@@ -167,14 +174,16 @@ function createDeferred(): {
return { promise, resolve }
}
function createActiveCueRun(): {
function createActiveCueRun(startedWithExplicitStart: boolean): {
finished: Promise<void>
resolveFinished: () => void
startedWithExplicitStart: boolean
} {
const deferred = createDeferred()
return {
finished: deferred.promise,
resolveFinished: deferred.resolve,
startedWithExplicitStart,
}
}
@@ -536,7 +545,7 @@ function buildCuesFromInput(
): NarrationCue {
let didRegisterName = false
const start = async (): Promise<void> => {
const start = async (startedWithExplicitStart = true): Promise<void> => {
if (isInsideHide())
throw new Error('Cannot start narration inside hide()')
const recorder = activeRecorder
@@ -545,8 +554,8 @@ function buildCuesFromInput(
assertUniqueCueName(name)
didRegisterName = true
}
cueAutoEnd()
const run = createActiveCueRun()
cueAutoEnd(name)
const run = createActiveCueRun(startedWithExplicitStart)
activeCueName = name
activeCueRun = run
await emitStart(recorder)
@@ -567,7 +576,7 @@ function buildCuesFromInput(
}
const cue = (async (): Promise<void> => {
await start()
await start(false)
await end()
}) as NarrationCue