refactor(screenci): remove dev command and no-container mode

This commit is contained in:
Olli Paloviita
2026-04-15 12:37:52 +03:00
parent f3e0fa748c
commit c5d50c76b1
8 changed files with 30 additions and 385 deletions
+2 -2
View File
@@ -69,7 +69,7 @@ All standard `page` methods (`goto`, `waitForURL`, `waitForLoadState`, `waitForT
```bash
# Dry-run: opens Playwright UI so you can verify selectors and pacing
npx screenci dev
npx screenci test --ui
# Record: captures the screen and writes .screenci/<name>/recording.mp4
npx screenci record
@@ -78,7 +78,7 @@ npx screenci record
Or via the package scripts scaffolded by `init`:
```bash
npm run dev # → npx screenci dev
npm run test # → npx screenci test
npm run record # → npx screenci record
```
+11 -316
View File
@@ -349,80 +349,6 @@ describe('CLI', () => {
})
})
describe('record command with --no-container', () => {
beforeEach(() => {
process.env.SCREENCI_SECRET = 'test-secret'
})
it('should spawn playwright directly when --no-container is provided', async () => {
process.argv = ['node', 'cli.js', 'record', '--no-container']
const { main } = await import('./cli')
const mainPromise = main()
await vi.waitFor(() => {
expect(mockSpawn).toHaveBeenCalled()
})
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
'npx',
expect.arrayContaining([
'playwright',
'test',
'--config',
expect.stringContaining('screenci.config.ts'),
]),
expect.objectContaining({
stdio: 'inherit',
env: expect.objectContaining({
SCREENCI_RECORD: 'true',
}),
})
)
})
it('should not include --no-container in playwright args', async () => {
process.argv = ['node', 'cli.js', 'record', '--no-container']
const { main } = await import('./cli')
const mainPromise = main()
await vi.waitFor(() => {
expect(mockSpawn).toHaveBeenCalled()
})
mockChildProcess.emit('close', 0)
await mainPromise
const spawnCall = mockSpawn.mock.calls[0]
const args = spawnCall[1] as string[]
expect(args).not.toContain('--no-container')
})
it('should pass other args alongside --no-container', async () => {
process.argv = ['node', 'cli.js', 'record', '--no-container', '--headed']
const { main } = await import('./cli')
const mainPromise = main()
await vi.waitFor(() => {
expect(mockSpawn).toHaveBeenCalled()
})
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
'npx',
expect.arrayContaining(['--headed']),
expect.objectContaining({ stdio: 'inherit' })
)
})
})
describe('container workflow', () => {
let mockBuildProcess: EventEmitter
let mockRecordingBuildProcess: EventEmitter
@@ -889,6 +815,17 @@ describe('CLI', () => {
expect(processExitSpy).toHaveBeenCalledWith(1)
})
it('should reject the removed dev command', async () => {
process.argv = ['node', 'cli.js', 'dev']
const { main } = await import('./cli')
await expect(main()).rejects.toThrow('process.exit called')
expect(loggerErrorSpy).toHaveBeenCalledWith('Unknown command: dev')
expect(processExitSpy).toHaveBeenCalledWith(1)
})
it('should show global help with --help', async () => {
process.argv = ['node', 'cli.js', '--help']
const stdoutSpy = vi
@@ -1067,248 +1004,6 @@ describe('CLI', () => {
})
})
describe('dev command', () => {
it('should spawn playwright with --ui flag and default config path', async () => {
process.argv = ['node', 'cli.js', 'dev']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
'npx',
expect.arrayContaining([
'playwright',
'test',
'--config',
expect.stringContaining('screenci.config.ts'),
'--ui',
]),
expect.objectContaining({ stdio: 'inherit' })
)
})
it('should support --config flag with custom path', async () => {
process.argv = ['node', 'cli.js', 'dev', '--config', 'custom.config.ts']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining([
'playwright',
'test',
'--config',
expect.stringContaining('custom.config.ts'),
'--ui',
]),
expect.objectContaining({ stdio: 'inherit' })
)
})
it('should support -c flag with custom path', async () => {
process.argv = ['node', 'cli.js', 'dev', '-c', 'custom.config.ts']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining([
'playwright',
'test',
'--config',
expect.stringContaining('custom.config.ts'),
'--ui',
]),
expect.objectContaining({ stdio: 'inherit' })
)
})
it('should pass additional arguments to playwright', async () => {
process.argv = [
'node',
'cli.js',
'dev',
'--project=chromium',
'--timeout=5000',
]
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining([
'playwright',
'test',
'--config',
expect.stringContaining('screenci.config.ts'),
'--ui',
'--project=chromium',
'--timeout=5000',
]),
expect.objectContaining({ stdio: 'inherit' })
)
})
it('should allow parallel execution flags', async () => {
process.argv = [
'node',
'cli.js',
'dev',
'--workers=4',
'--fully-parallel',
]
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(mockSpawn).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining([
'playwright',
'test',
'--ui',
'--workers=4',
'--fully-parallel',
]),
expect.objectContaining({ stdio: 'inherit' })
)
})
it('should log UI mode when running dev command', async () => {
process.argv = ['node', 'cli.js', 'dev']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Running ScreenCI UI mode with npx...'
)
expect(loggerInfoSpy).toHaveBeenCalledWith(
expect.stringContaining('Using config:')
)
})
it('should run in headed mode when --headed flag is provided', async () => {
process.argv = ['node', 'cli.js', 'dev', '--headed']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
const spawnCall = mockSpawn.mock.calls[0]
const args = spawnCall[1]
// Should NOT include --ui when --headed is present
expect(args).not.toContain('--ui')
expect(args).toContain('--headed')
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Running ScreenCI headed mode with npx...'
)
})
it('should run in headed mode with additional flags', async () => {
process.argv = [
'node',
'cli.js',
'dev',
'--headed',
'--project=chromium',
'--workers=2',
]
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 0)
await mainPromise
const spawnCall = mockSpawn.mock.calls[0]
const args = spawnCall[1]
expect(args).not.toContain('--ui')
expect(args).toContain('--headed')
expect(args).toContain('--project=chromium')
expect(args).toContain('--workers=2')
})
it('should handle playwright exit with error code', async () => {
process.argv = ['node', 'cli.js', 'dev']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('close', 1)
await expect(mainPromise).rejects.toThrow('Playwright exited with code 1')
})
it('should handle playwright error event', async () => {
process.argv = ['node', 'cli.js', 'dev']
const { main } = await import('./cli')
const mainPromise = main()
await new Promise((resolve) => setTimeout(resolve, 10))
mockChildProcess.emit('error', new Error('spawn failed'))
await expect(mainPromise).rejects.toThrow('spawn failed')
})
it('should exit if config not found', async () => {
process.argv = ['node', 'cli.js', 'dev']
mockExistsSync.mockReturnValue(false)
const { main } = await import('./cli')
await expect(main()).rejects.toThrow('process.exit called')
expect(loggerErrorSpy).toHaveBeenCalledWith(
'Error: screenci.config.ts not found in current directory'
)
expect(processExitSpy).toHaveBeenCalledWith(1)
})
})
describe('test command', () => {
it('should spawn playwright test with default config path', async () => {
process.argv = ['node', 'cli.js', 'test', '--project=chromium']
+9 -37
View File
@@ -1306,7 +1306,7 @@ function generatePackageJson(
scripts: {
record: 'screenci record',
retry: 'screenci retry',
dev: 'screenci dev',
test: 'screenci test',
},
dependencies: {
screenci: 'latest',
@@ -1330,7 +1330,7 @@ Write video scripts in \`videos/*.video.ts\`. Each \`video(...)\` scenario opens
## Quick start
1. Run tests in UI/dev mode:
1. Run tests in UI mode:
\`npx screenci test\`
@@ -1663,7 +1663,7 @@ export async function main() {
if (process.argv.length <= 2) {
logger.error('Error: No command provided')
logger.error(
'Available commands: record, dev, test, info, make-public, make-private, retry, init'
'Available commands: record, test, info, make-public, make-private, retry, init'
)
process.exit(1)
}
@@ -1685,8 +1685,7 @@ export async function main() {
process.exit(1)
}
const useContainer =
!parsed.noContainer && process.env.SCREENCI_IN_CONTAINER !== 'true'
const useContainer = process.env.SCREENCI_IN_CONTAINER !== 'true'
// Validate early so we don't build the container unnecessarily
if (useContainer) {
@@ -1789,22 +1788,12 @@ export async function main() {
}
})
// dev command — playwright args pass through as-is
program
.command('dev [playwrightArgs...]')
.description('Run Playwright in dev/UI mode')
.allowUnknownOption(true)
.action(async () => {
const parsed = parseDevCliArgs(getSubcommandArgv('dev'))
await run('dev', parsed.otherArgs, parsed.configPath)
})
program
.command('test [playwrightArgs...]')
.description('Run Playwright test with screenci.config.ts')
.allowUnknownOption(true)
.action(async () => {
const parsed = parseDevCliArgs(getSubcommandArgv('test'))
const parsed = parseConfigCliArgs(getSubcommandArgv('test'))
await run('test', parsed.otherArgs, parsed.configPath)
})
@@ -1918,14 +1907,12 @@ function getSubcommandArgv(command: string): string[] {
function parseRecordCliArgs(args: string[]): {
configPath: string | undefined
noContainer: boolean
imageTag: string | undefined
verbose: boolean
forcedRuntime: ContainerRuntimeName | 'both' | undefined
otherArgs: string[]
} {
let configPath: string | undefined
let noContainer = false
let imageTag: string | undefined
let verbose = false
let forcedRuntime: ContainerRuntimeName | 'both' | undefined
@@ -1942,8 +1929,6 @@ function parseRecordCliArgs(args: string[]): {
}
configPath = nextArg
i++
} else if (arg === '--no-container') {
noContainer = true
} else if (arg === '--verbose' || arg === '-v') {
verbose = true
} else if (arg === '--podman') {
@@ -1965,7 +1950,6 @@ function parseRecordCliArgs(args: string[]): {
return {
configPath,
noContainer,
imageTag,
verbose,
forcedRuntime,
@@ -1973,7 +1957,7 @@ function parseRecordCliArgs(args: string[]): {
}
}
function parseDevCliArgs(args: string[]): {
function parseConfigCliArgs(args: string[]): {
configPath: string | undefined
otherArgs: string[]
} {
@@ -2282,7 +2266,7 @@ async function runWithContainer(
}
async function run(
command: string,
command: 'record' | 'test',
additionalArgs: string[],
customConfigPath?: string
) {
@@ -2296,25 +2280,14 @@ async function run(
process.exit(1)
}
// Only validate args for record command (dev allows parallel execution)
// Only validate args for record command
if (command === 'record') {
validateArgs(additionalArgs)
const screenciDir = resolve(dirname(configPath), '.screenci')
clearDirectory(screenciDir)
}
// For dev command: use --ui unless --headed is specified
const isHeaded = additionalArgs.includes('--headed')
const shouldUseUI = command === 'dev' && !isHeaded
const mode =
command === 'dev'
? isHeaded
? 'headed mode'
: 'UI mode'
: command === 'test'
? 'tests'
: 'recorder'
const mode = command === 'test' ? 'tests' : 'recorder'
if (process.env.SCREENCI_IN_CONTAINER !== 'true') {
logger.info(`Running ScreenCI ${mode} with npx...`)
logger.info(`Using config: ${configPath}`)
@@ -2325,7 +2298,6 @@ async function run(
'test',
'--config',
configPath,
...(shouldUseUI ? ['--ui'] : []),
...additionalArgs,
]
+1 -20
View File
@@ -14,7 +14,6 @@ Most commands look for `screenci.config.ts` in the current directory. Use `--con
| Command | What it does |
| --------------------------------- | ---------------------------------------------------------------- |
| `screenci init [name]` | Scaffold a new ScreenCI project |
| `screenci dev [args...]` | Run Playwright in UI mode for fast iteration |
| `screenci test [args...]` | Forward directly to `playwright test` using your ScreenCI config |
| `screenci record [args...]` | Record videos, usually in a container |
| `screenci retry` | Upload the newest local recording in `.screenci/` |
@@ -35,21 +34,6 @@ Options:
- `-v, --verbose` prints underlying command output instead of spinners
## `screenci dev [playwrightArgs...]`
Runs Playwright against your ScreenCI config in local development mode. By default this starts Playwright UI mode.
```bash
npx screenci dev
npx screenci dev --project=chromium
npx screenci dev --headed
```
Notes:
- `--headed` disables Playwright UI mode and runs headed instead
- any additional args are forwarded to `playwright test`
## `screenci test [playwrightArgs...]`
Forwards directly to `playwright test` while still resolving `screenci.config.ts`.
@@ -60,7 +44,7 @@ npx screenci test --grep "checkout"
npx screenci test --project=chromium
```
Use this when you want normal Playwright execution without recording and without the Playwright UI shortcut from `screenci dev`.
Use this when you want normal Playwright execution without recording.
## `screenci record [playwrightArgs...]`
@@ -69,13 +53,11 @@ Records videos with ScreenCI. On the host this normally builds and runs the proj
```bash
npx screenci record
npx screenci record --project=chromium
npx screenci record --no-container --headed
```
Options:
- `-c, --config <path>` use a custom config path
- `--no-container` run locally instead of in Podman or Docker
- `--podman` force Podman
- `--docker` force Docker
- `--tag <tag>` pull and use a specific `ghcr.io/screenci/record:<tag>` image
@@ -167,7 +149,6 @@ Requirements:
These commands support `--config <path>`:
- `dev`
- `test`
- `record`
- `retry`
+1 -1
View File
@@ -65,7 +65,7 @@ All of these are composable with normal [Playwright](https://playwright.dev/docs
## Develop without recording
```bash
npm run dev
npm run test -- --ui
```
Opens the Playwright UI. Run your scripts, verify selectors work, iterate fast. No screen capture, no Docker, no FFmpeg. Just Playwright.
+1 -2
View File
@@ -57,8 +57,7 @@ That's Playwright. screenci extends it — it does not replace it.
### 4. Develop without recording
```bash
npm run dev
# or: npx screenci dev
npx screenci test --ui
```
Opens the Playwright UI. Run your scripts, verify they work, fix selectors — no screen capture, no container, no FFmpeg. Just normal Playwright test execution.
+4 -4
View File
@@ -31,7 +31,7 @@ If you are creating new videos, remove the starter `videos/example.video.ts` fil
```bash
# iterate locally without recording
npx screenci dev
npx screenci test --ui
# verify repeatedly until green
npx screenci test
@@ -60,8 +60,8 @@ ScreenCI uses Playwright-style `.video.ts` files and adds recording-specific hel
## Command Notes
- `screenci record` runs the recording flow. By default it uses Podman or Docker unless `--no-container` is used.
- `screenci dev` runs Playwright in UI mode for fast iteration without screen capture.
- `screenci record` runs the recording flow in Podman or Docker.
- `screenci test --ui` runs Playwright in UI mode for fast iteration without screen capture.
- `screenci retry` uploads the latest `.screenci` output when API configuration is available.
## Recording Workflow
@@ -70,7 +70,7 @@ ScreenCI uses Playwright-style `.video.ts` files and adds recording-specific hel
2. Add or edit `.video.ts` files in `videos/`.
Remove `videos/example.video.ts` if you are creating new videos and do not need the starter video.
For narration, define `const narration = createNarration({ ... })` near the top of the file and trigger lines with `await narration.someKey` inside the test body.
3. Run `npx screenci dev` to validate selectors and flow.
3. Run `npx screenci test --ui` to validate selectors and flow.
4. Run `npx screenci test` until it passes.
5. Run `npx screenci record` to produce `.screenci/<video-name>/recording.mp4` and `data.json`.
+1 -3
View File
@@ -9,7 +9,6 @@ If you are creating new videos, remove the starter `videos/example.video.ts` fil
```bash
npx screenci record
npx screenci record --no-container
npx screenci record -c screenci.config.ts
```
@@ -23,7 +22,6 @@ npx screenci record -c screenci.config.ts
## Runtime Behavior
- By default, recording runs in Podman or Docker.
- `--no-container` runs directly on the host.
- Playwright arguments can be passed through after the command.
- When API configuration and `SCREENCI_SECRET` are available, uploads may run after recording.
@@ -31,7 +29,7 @@ npx screenci record -c screenci.config.ts
```bash
# first verify the flow
npx screenci dev
npx screenci test --ui
# then record
npx screenci record