mirror of
https://github.com/screenci/screenci.git
synced 2026-09-19 08:57:46 +08:00
feat(screenci): add verbose config logging and cue warning
This commit is contained in:
+62
@@ -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',
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user