Skip to content

Commit 6f791dc

Browse files
DavertMikDavertMik
andauthored
feat(plugins): add screencast plugin (replaces subtitles) (#5543)
* feat(plugins): add screencast plugin (replaces subtitles) New `screencast` plugin built on Playwright 1.59's page.screencast API. Records WebM video with optional burn-in action captions (showActions) and optional standalone .srt subtitles. Wires into the unified `on=` parameter introduced in #5542 with two valid modes: `on=fail` (default; deletes the recording on pass) and `on=test` (always keeps). - Replaces and removes the legacy `subtitles` plugin and its test - Bumps `playwright` devDependency to 1.59.0 (required for page.screencast) - Adds 8 unit tests covering modes, captions, subtitles, video=false fallback, missing-API guard, invalid-mode rejection - Updates examples/codecept.config.js to swap subtitles for screencast - Documents the plugin in docs/plugins.md and docs/playwright.md, including the empirically-verified "two videos" note when helper video:true and the plugin are enabled together Verified against a real Playwright 1.59 run: produces output/screencast/*.webm plus *.srt; coexists with helper recordVideo without conflict. * Apply suggestion from @DavertMik * Apply suggestion from @DavertMik * fix(playwright): port React locator off removed _react= engine; drop Vue Playwright 1.59 removed the experimental `_react=` and `_vue=` selector engines, which broke `{ react: ... }` and `{ vue: ... }` locators with: locator.all: TypeError: Cannot read properties of undefined (reading 'queryAll') at InjectedScript._queryEngineAll Reimplemented React locators on top of the `resq` library + `page.evaluateHandle` (the same approach the Puppeteer helper already uses). Dropped Vue locator support entirely — there is no in-deps equivalent to resq for Vue, and the public surface was minimal. - lib/helper/extras/PlaywrightReactVueLocator.js: rewrite findReact via resq; remove findVue - lib/helper/Playwright.js: remove findVue import and Vue branches in findElements / findElement - test/acceptance/react_test.js: remove the third scenario that used the raw `{ pw: '_react=...' }` form (no longer valid syntax) - test/unit/locator_test.js: drop `pw: '_react=button'` and `pw: '_vue=button'` cases - typings/index.d.ts: drop `{ vue: string }` from ILocator - docs/locators.md: update note to remove `_react`/`_vue` mentions Verified end-to-end against Playwright 1.59: React Selectors ✔ props @puppeteer @playwright (3.7s) ✔ component name @puppeteer @playwright (3.4s) --------- Co-authored-by: DavertMik <davert@testomat.io>
1 parent e0fe61f commit 6f791dc

14 files changed

Lines changed: 634 additions & 328 deletions

File tree

docs/locators.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ I.click({ role: 'button', name: 'Submit' }, '#login-form')
2222

2323
The context narrows the search to one region of the page, and the semantic string says what the user actually clicks. This is **more precise than ARIA or CSS alone** because it combines structural scope with human-readable intent.
2424

25-
Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `pw`. Shadow DOM and React selectors have their own pages — see [Shadow DOM](/shadow) and [React](/react). Playwright-specific locators (`_react`, `_vue`, `data-testid`) use the `pw` strategy: `{ pw: '_react=Button[name="Save"]' }`.
25+
Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `pw`. Shadow DOM and React selectors have their own pages — see [Shadow DOM](/shadow) and [React](/react). Playwright-specific locators use the `pw` strategy: `{ pw: '[data-testid="save"]' }`.
2626

2727
## Locator types at a glance
2828

docs/playwright.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,11 +478,47 @@ When a test fails and video was enabled a video file is shown under the `artifac
478478

479479
Open video and use it to debug a failed test case. Video helps when running tests on CI. Configure your CI system to enable artifacts storage for `output/video` and review videos of failed test case to understand failures.
480480

481-
It is recommended to enable [subtitles](https://codecept.io/plugins/#subtitles) plugin which will generate subtitles from steps in `.srt` format. Subtitles file will be saved into after a video file so video player (like VLC) would load them automatically:
481+
## Screencast
482+
483+
For richer evidence than helper-level `video`, enable the [`screencast`](https://codecept.io/plugins/#screencast) plugin. It uses Playwright's `page.screencast` API (Playwright >= 1.59) to record WebM video with optional burned-in action captions and a standalone `.srt` subtitle track.
484+
485+
```js
486+
plugins: {
487+
screencast: {
488+
enabled: true,
489+
on: 'fail',
490+
}
491+
}
492+
```
493+
494+
`on: 'fail'` (default) deletes the recording when the test passes; `on: 'test'` keeps every test's video.
495+
496+
`captions: true` (default) burns `I.click()` / `I.fillField()` annotations into the video via `page.screencast.showActions()`. `subtitles: true` writes a standalone `.srt` file alongside the video — VLC and most players auto-load it.
497+
498+
```js
499+
plugins: {
500+
screencast: {
501+
enabled: true,
502+
on: 'test',
503+
captions: true,
504+
subtitles: true,
505+
}
506+
}
507+
```
482508

483509
![](https://user-images.githubusercontent.com/220264/131644090-38d1ca55-1ba1-41fa-8fd1-7dea2b7ae995.png)
484510

485-
## Trace <Badge text="Since 3.1" type="warning"/>
511+
CLI usage:
512+
513+
npx codeceptjs run -p screencast
514+
npx codeceptjs run -p screencast:on=test
515+
npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true
516+
517+
The recording is attached to the test as `test.artifacts.screencast`; the `.srt` (when enabled) is attached as `test.artifacts.subtitle`.
518+
519+
> Enabling helper-level `video: true` **and** the `screencast` plugin produces two independent recordings (one in `output/videos/`, one in `output/screencast/`). Pick one.
520+
521+
## Trace
486522

487523
If video is not enough to descover why a test failed a [trace](https://playwright.dev/docs/trace-viewer/) can be recorded.
488524

docs/plugins.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,47 @@ Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
700700
701701
* `config` &#x20;
702702
703+
## screencast
704+
705+
Records WebM video of tests using Playwright's screencast API (Playwright >= 1.59).
706+
When `captions` is enabled, action annotations are burned into the video; when
707+
`subtitles` is enabled, a standalone `.srt` file is also produced.
708+
709+
```js
710+
plugins: {
711+
screencast: {
712+
enabled: true,
713+
on: 'fail',
714+
}
715+
}
716+
```
717+
718+
#### `on=` modes
719+
720+
* **fail** — record while running; delete on pass, keep on fail (default)
721+
* **test** — record and keep every test's video
722+
723+
CLI examples:
724+
725+
npx codeceptjs run -p screencast
726+
npx codeceptjs run -p screencast:on=test
727+
npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true
728+
729+
Possible config options:
730+
731+
* `captions`: burn-in action overlays via `page.screencast.showActions()`. Default: true.
732+
* `subtitles`: also write a standalone `.srt` file alongside the video. Default: false.
733+
* `video`: record a video. With `video=false, subtitles=true`, only the `.srt` is produced (next to `test.artifacts.video` if a helper recorded one). Default: true.
734+
* `size`: pass-through `{ width, height }` for `screencast.start`.
735+
* `quality`: pass-through 0–100 for `screencast.start`.
736+
737+
> Enabling Playwright's helper-level `video: true` and this plugin together
738+
> produces two independent recordings. Pick one.
739+
740+
### Parameters
741+
742+
* `config` &#x20;
743+
703744
## screenshot
704745
705746
Saves screenshots from the browser at points triggered by `on=`. Replaces the
@@ -812,20 +853,6 @@ plugins: {
812853
813854
* `config` &#x20;
814855
815-
## subtitles
816-
817-
Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test
818-
819-
#### Configuration
820-
821-
```js
822-
plugins: {
823-
subtitles: {
824-
enabled: true
825-
}
826-
}
827-
```
828-
829856
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
830857
831858
[2]: https://github.com/cenfun/monocart-coverage-reports?tab=readme-ov-file#default-options

examples/codecept.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ export const config = {
5757
retryFailedStep: {
5858
enabled: false,
5959
},
60-
subtitles: {
60+
screencast: {
6161
enabled: true,
62+
on: 'test',
63+
subtitles: true,
6264
},
6365
aiTrace: {
6466
enabled: true,

lib/helper/Playwright.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
3636
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
3737
import Popup from './extras/Popup.js'
3838
import Console from './extras/Console.js'
39-
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39+
import { findReact, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
4040
import { dropFile } from './scripts/dropFile.js'
4141
import WebElement from '../element/WebElement.js'
4242
import { selectElement } from './extras/elementSelection.js'
@@ -4223,13 +4223,10 @@ async function findByRole(context, locator) {
42234223
}
42244224

42254225
async function findElements(matcher, locator) {
4226-
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
42274226
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4228-
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
42294227
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
42304228

42314229
if (isReactLocator) return findReact(matcher, locator)
4232-
if (isVueLocator) return findVue(matcher, locator)
42334230
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
42344231

42354232
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
@@ -4245,7 +4242,6 @@ async function findElements(matcher, locator) {
42454242

42464243
async function findElement(matcher, locator) {
42474244
if (locator.react) return findReact(matcher, locator)
4248-
if (locator.vue) return findVue(matcher, locator)
42494245
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
42504246

42514247
locator = new Locator(locator, 'css')
Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,61 @@
1+
import fs from 'fs'
2+
import { fileURLToPath } from 'url'
3+
4+
let resqScript
5+
16
async function findReact(matcher, locator) {
2-
// Handle both Locator objects and raw locator objects
37
const reactLocator = locator.locator || locator
4-
let _locator = `_react=${reactLocator.react}`;
5-
let props = '';
8+
const page = typeof matcher.page === 'function' ? matcher.page() : matcher
69

7-
if (reactLocator.props) {
8-
props += propBuilder(reactLocator.props);
9-
_locator += props;
10+
if (!resqScript) {
11+
resqScript = fs.readFileSync(fileURLToPath(import.meta.resolve('resq'))).toString()
1012
}
11-
return matcher.locator(_locator).all();
12-
}
13+
await page.evaluate(resqScript)
14+
await page.evaluate(() => window.resq.waitToLoadReact())
15+
16+
const arrayHandle = await page.evaluateHandle(
17+
({ selector, props, state }) => {
18+
let elements = window.resq.resq$$(selector)
19+
if (Object.keys(props).length) elements = elements.byProps(props)
20+
if (Object.keys(state).length) elements = elements.byState(state)
21+
if (!elements.length) return []
1322

14-
async function findVue(matcher, locator) {
15-
// Handle both Locator objects and raw locator objects
16-
const vueLocator = locator.locator || locator
17-
let _locator = `_vue=${vueLocator.vue}`;
18-
let props = '';
23+
let nodes = []
24+
elements.forEach(element => {
25+
let { node, isFragment } = element
26+
if (!node) {
27+
isFragment = true
28+
node = element.children
29+
}
30+
if (isFragment) nodes = nodes.concat(node)
31+
else nodes.push(node)
32+
})
33+
return [...nodes]
34+
},
35+
{
36+
selector: reactLocator.react,
37+
props: reactLocator.props || {},
38+
state: reactLocator.state || {},
39+
},
40+
)
1941

20-
if (vueLocator.props) {
21-
props += propBuilder(vueLocator.props);
22-
_locator += props;
42+
const properties = await arrayHandle.getProperties()
43+
await arrayHandle.dispose()
44+
const result = []
45+
for (const property of properties.values()) {
46+
const elementHandle = property.asElement()
47+
if (elementHandle) result.push(elementHandle)
2348
}
24-
return matcher.locator(_locator).all();
49+
return result
2550
}
2651

2752
async function findByPlaywrightLocator(matcher, locator) {
28-
// Handle both Locator objects and raw locator objects
2953
const pwLocator = locator.locator || locator
3054
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
31-
return matcher.getByTestId(pwLocator.pw.value.split('=')[1]);
55+
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
3256
}
3357
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
34-
return matcher.locator(pwValue).all();
35-
}
36-
37-
function propBuilder(props) {
38-
let _props = '';
39-
40-
for (const [key, value] of Object.entries(props)) {
41-
if (typeof value === 'object') {
42-
for (const [k, v] of Object.entries(value)) {
43-
_props += `[${key}.${k} = "${v}"]`;
44-
}
45-
} else {
46-
_props += `[${key} = "${value}"]`;
47-
}
48-
}
49-
return _props;
58+
return matcher.locator(pwValue).all()
5059
}
5160

52-
export { findReact, findVue, findByPlaywrightLocator };
61+
export { findReact, findByPlaywrightLocator }

0 commit comments

Comments
 (0)