fix(screenci): remove retry command and extend hide pause

This commit is contained in:
Olli Paloviita
2026-05-21 17:48:04 +03:00
parent 14ec6ce3a6
commit 599146b518
9 changed files with 85 additions and 157 deletions
+1 -5
View File
@@ -250,8 +250,4 @@ The `page` fixture inside `video()` is a `ScreenCIPage` — a Playwright `Page`
data.json ← interaction events + cue metadata
```
Upload to screenci.com for rendering, narration generation, and the permanent embed link:
```bash
npm run retry
```
When `SCREENCI_SECRET` is configured, `screenci record` uploads the output to screenci.com automatically for rendering, narration generation, and the permanent embed link.
+3 -60
View File
@@ -616,70 +616,13 @@ describe('CLI', () => {
})
})
describe('retry command', () => {
it('should recognize retry command (not unknown)', async () => {
describe('removed retry command', () => {
it('should report retry as an unknown command', async () => {
process.argv = ['node', 'cli.js', 'retry']
const { main } = await import('./cli')
// Will exit due to missing config, not an unknown command error
await expect(main()).rejects.toThrow('process.exit called')
expect(loggerErrorSpy).not.toHaveBeenCalledWith('Unknown command: retry')
})
it('should warn when no recordings found', async () => {
process.argv = ['node', 'cli.js', 'retry']
mockReaddir.mockResolvedValue([])
const { main } = await import('./cli')
// Will exit because config mock isn't set up — just ensure command is recognized
await expect(main()).rejects.toThrow('process.exit called')
// exit is called due to missing config, not unknown command
expect(loggerErrorSpy).not.toHaveBeenCalledWith('Unknown command: retry')
})
it('should error when no API URL is configured', async () => {
process.argv = ['node', 'cli.js', 'retry']
mockExistsSync.mockReturnValue(true)
const { main } = await import('./cli')
await expect(main()).rejects.toThrow('process.exit called')
expect(loggerErrorSpy).not.toHaveBeenCalledWith('Unknown command: retry')
})
it('should write project URL to GitHub Actions output after upload', async () => {
process.argv = [
'node',
'cli.js',
'retry',
'--config',
'test-fixtures/screenci.config.ts',
]
process.env.SCREENCI_SECRET = 'test-secret'
process.env.GITHUB_OUTPUT = '/tmp/github-output'
mockExistsSync.mockImplementation(
(path: string) => !String(path).endsWith('recording.mp4')
)
mockReaddir.mockResolvedValue(['demo'])
mockReadFile.mockResolvedValue(
JSON.stringify({ events: [], metadata: { videoName: 'Demo' } })
)
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
recordingId: 'recording_123',
projectId: 'project_123',
}),
text: vi.fn().mockResolvedValue(''),
})
const { main } = await import('./cli')
await main()
expect(mockAppendFile).toHaveBeenCalledWith(
'/tmp/github-output',
'screenci_project_url=https://app.screenci.com/project/project_123\n'
)
expect(loggerErrorSpy).toHaveBeenCalledWith('Unknown command: retry')
})
it('should launch Playwright through cmd on Windows', async () => {
+1 -68
View File
@@ -962,59 +962,6 @@ async function writeGitHubProjectOutput(projectUrl: string): Promise<void> {
await appendFile(githubOutput, `screenci_project_url=${projectUrl}\n`)
}
async function uploadLatest(
configPath: string | undefined,
verbose = false
): Promise<void> {
const { resolvedConfigPath, screenciConfig } =
await loadScreenCIConfigAndEnv(configPath)
const apiUrl = getDevBackendUrl()
const secret = process.env.SCREENCI_SECRET
if (!secret) {
logger.error(
'No secret configured. Set SCREENCI_SECRET in your .env file (get it from the API Key page in the dashboard).'
)
process.exit(1)
}
const configDir = dirname(resolvedConfigPath)
const screenciDir = resolve(configDir, '.screenci')
if (verbose) {
logger.info(`screenciDir=${screenciDir}`)
logger.info(`apiUrl=${apiUrl}`)
}
const appUrl = getDevFrontendUrl()
let projectId: string | null = null
try {
projectId = await uploadRecordings(
screenciDir,
screenciConfig.projectName,
apiUrl,
secret,
undefined,
verbose
)
} catch (err) {
if (isUploadCancelledError(err)) {
process.exit(130)
}
throw err
}
if (projectId !== null) {
const projectUrl = `${appUrl}/project/${projectId}`
await writeGitHubProjectOutput(projectUrl)
logger.info('')
logger.info('Upload complete, rendering continues in the background.')
logger.info('Recording finished, results available at:')
logger.info(pc.cyan(projectUrl))
}
}
async function loadScreenCIConfigAndEnv(configPath?: string): Promise<{
resolvedConfigPath: string
screenciConfig: ScreenCIConfig
@@ -1276,7 +1223,6 @@ function generatePackageJson(
type: 'module',
scripts: {
record: 'screenci record',
retry: 'screenci retry',
test: 'screenci test',
},
dependencies: {
@@ -1838,7 +1784,7 @@ export async function main() {
if (process.argv.length <= 2) {
logger.error('Error: No command provided')
logger.error(
'Available commands: record, test, info, make-public, make-private, retry, init'
'Available commands: record, test, info, make-public, make-private, init'
)
process.exit(1)
}
@@ -1985,19 +1931,6 @@ export async function main() {
)
})
// retry command
program
.command('retry')
.description('Retry uploading all pending recordings')
.option('-c, --config <path>', 'path to screenci.config.ts')
.option('-v, --verbose', 'verbose output')
.action(async (options: Record<string, unknown>) => {
await uploadLatest(
options['config'] as string | undefined,
(options['verbose'] as boolean | undefined) ?? false
)
})
// init command
program
.command('init [name]')
+17 -15
View File
@@ -16,7 +16,6 @@ Most commands look for `screenci.config.ts` in the current directory. Use `--con
| `screenci init [name]` | Scaffold a new ScreenCI project |
| `screenci test [args...]` | Forward directly to `playwright test` using your ScreenCI config |
| `screenci record [args...]` | Record videos with local Playwright |
| `screenci retry` | Upload the newest local recording in `.screenci/` |
| `screenci info` | Print remote project info as JSON |
| `screenci make-public <videoId>` | Enable public URLs for a video |
| `screenci make-private <videoId>` | Disable public URLs for a video |
@@ -45,16 +44,32 @@ Options:
## `screenci test [playwrightArgs...]`
Forwards directly to `playwright test` while still resolving `screenci.config.ts`.
Forwards Playwright test arguments in normal `playwright test` syntax while still resolving `screenci.config.ts`.
```bash
npx screenci test
npx screenci test --grep "checkout"
npx screenci test --project=chromium
npx screenci test tests/onboarding.video.ts --grep "step 2"
```
Use this when you want normal Playwright execution without recording.
To run only some tests, pass the same filters you would use with `playwright test`, such as a file path or `--grep`:
```bash
npx screenci test videos/onboarding.video.ts
npx screenci test --grep "checkout"
npx screenci test videos/onboarding.video.ts --grep "step 2"
```
Notes:
- Most arguments after `test` are passed through as-is to `playwright test`
- `screenci` always adds `--config <resolved-path-to-screenci.config.ts>` for you
- `--config` / `-c` are handled by `screenci` itself, so use them to point to a different `screenci.config.ts`
- `--verbose` / `-v` are also handled by `screenci` itself for extra CLI logging, not forwarded to Playwright
## `screenci record [playwrightArgs...]`
Records videos with ScreenCI by running local Playwright with `SCREENCI_RECORDING=true`, then uploads results if `SCREENCI_SECRET` is set. If the secret is missing, `record` prompts for login before recording begins.
@@ -73,18 +88,6 @@ Restrictions:
- `--workers`, `-j`, `--retries`, and `--fully-parallel` are rejected because ScreenCI records sequentially with one worker
## `screenci retry`
Uploads the newest recording from `.screenci/` to ScreenCI.
```bash
npx screenci retry
```
Requirements:
- `SCREENCI_SECRET` must be available, usually via the `envFile` configured in `screenci.config.ts`
## `screenci info`
Fetches the current remote project info for the local `projectName` and prints it as 2-space-formatted JSON.
@@ -157,7 +160,6 @@ These commands support `--config <path>`:
- `test`
- `record`
- `retry`
- `info`
- `make-public`
- `make-private`
+16
View File
@@ -55,6 +55,22 @@ npm run test
This is similar to running `playwright test`: it executes the `.video.ts` script without recording, so you can quickly check selectors, timing, and narration cues.
You can also pass normal Playwright test arguments through `screenci test`, for example:
```bash
npx screenci test --grep "example"
npx screenci test videos/example.video.ts
```
That also means you can run only some tests while iterating:
```bash
npx screenci test videos/example.video.ts
npx screenci test --grep "intro"
```
`npx screenci test` still injects your resolved `screenci.config.ts` automatically. `--config` and `--verbose` are reserved for the `screenci` CLI itself.
## Record
Inside the ScreenCI project directory, record the final video when the script is working:
+8 -5
View File
@@ -9,7 +9,13 @@ ScreenCI is the first **Deployment Automation** platform for product videos. We
In practice, ScreenCI extends the Playwright E2E test library with product video related features.
In code, it looks something like this:
<video controls crossorigin="anonymous" poster="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/thumbnail" style="max-width:100%; border: 1px solid #ccc;">
<source src="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/video" type="video/mp4" />
<track kind="subtitles" src="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/subtitle" srclang="en" label="English" default />
</video>
<details>
<summary>Show code</summary>
```ts
import { autoZoom, createNarration, hide, video, voices } from 'screenci'
@@ -44,10 +50,7 @@ video('How to get started', async ({ page }) => {
})
```
<video controls crossorigin="anonymous" poster="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/thumbnail" style="max-width:100%; border: 1px solid #ccc;">
<source src="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/video" type="video/mp4" />
<track kind="subtitles" src="https://api.screenci.com/public/kh7dq5rk3vabtxya45w6zm1fmd871jdx/en/subtitle" srclang="en" label="English" default />
</video>
</details>
## Why ScreenCI?
+6 -1
View File
@@ -36,10 +36,15 @@ npx screenci test --ui
# verify repeatedly until green
npx screenci test
# run only some tests with normal Playwright filters
npx screenci test videos/signup.video.ts --grep "fills billing details"
# only record after tests pass
npx screenci record
```
`npx screenci test` accepts normal `playwright test` argument syntax after `test`. ScreenCI still injects its resolved `screenci.config.ts` automatically. `--config` / `-c` and `--verbose` / `-v` are reserved for the ScreenCI CLI itself rather than being forwarded to Playwright.
## What ScreenCI Adds
ScreenCI uses Playwright-style `.video.ts` files and adds recording-specific helpers:
@@ -97,7 +102,7 @@ await autoZoom(async () => {
- `screenci record` runs the recording flow with local Playwright.
- `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.
- `screenci test <playwright args...>` forwards most Playwright test arguments unchanged, while still using `screenci.config.ts`.
## Recording Workflow
+32 -2
View File
@@ -74,6 +74,36 @@ describe('hide', () => {
expect(order[1]).toBe('hideEnd')
})
it('waits for the post-hide pause before emitting hideEnd', async () => {
vi.useFakeTimers()
try {
const order: string[] = []
vi.mocked(recorder.addHideEnd).mockImplementation(() => {
order.push('hideEnd')
})
const hidePromise = hide(() => {
order.push('callback')
}).then(() => {
order.push('resolved')
})
await Promise.resolve()
expect(order).toEqual(['callback'])
await vi.advanceTimersByTimeAsync(POST_HIDE_PAUSE - 1)
await Promise.resolve()
expect(order).toEqual(['callback'])
await vi.advanceTimersByTimeAsync(1)
await hidePromise
expect(order).toEqual(['callback', 'hideEnd', 'resolved'])
} finally {
vi.useRealTimers()
}
})
it('awaits async callbacks', async () => {
const order: string[] = []
@@ -126,8 +156,8 @@ describe('hide', () => {
})
describe('POST_HIDE_PAUSE', () => {
it('adds a 250ms tail before revealing hidden recording', () => {
expect(POST_HIDE_PAUSE).toBe(250)
it('adds a 350ms tail before revealing hidden recording', () => {
expect(POST_HIDE_PAUSE).toBe(350)
})
})
})
+1 -1
View File
@@ -3,7 +3,7 @@ import type { IEventRecorder } from './events.js'
let activeRecorder: IEventRecorder | null = null
let insideHide = false
export const POST_HIDE_PAUSE = 250
export const POST_HIDE_PAUSE = 350
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))