mirror of
https://github.com/screenci/screenci.git
synced 2026-09-19 08:57:46 +08:00
fix(screenci): remove retry command and extend hide pause
This commit is contained in:
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user