Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ I.click({ role: 'button', name: 'Submit' }, '#login-form')

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.

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"]' }`.
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"]' }`.

## Locator types at a glance

Expand Down
40 changes: 38 additions & 2 deletions docs/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,47 @@ When a test fails and video was enabled a video file is shown under the `artifac

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.

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:
## Screencast

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.

```js
plugins: {
screencast: {
enabled: true,
on: 'fail',
}
}
```

`on: 'fail'` (default) deletes the recording when the test passes; `on: 'test'` keeps every test's video.

`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.

```js
plugins: {
screencast: {
enabled: true,
on: 'test',
captions: true,
subtitles: true,
}
}
```

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

## Trace <Badge text="Since 3.1" type="warning"/>
CLI usage:

npx codeceptjs run -p screencast
npx codeceptjs run -p screencast:on=test
npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true

The recording is attached to the test as `test.artifacts.screencast`; the `.srt` (when enabled) is attached as `test.artifacts.subtitle`.

> Enabling helper-level `video: true` **and** the `screencast` plugin produces two independent recordings (one in `output/videos/`, one in `output/screencast/`). Pick one.

## Trace

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

Expand Down
55 changes: 41 additions & 14 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,47 @@ Scenario('scenario tite', { disableRetryFailedStep: true }, () => {

* `config` &#x20;

## screencast

Records WebM video of tests using Playwright's screencast API (Playwright >= 1.59).
When `captions` is enabled, action annotations are burned into the video; when
`subtitles` is enabled, a standalone `.srt` file is also produced.

```js
plugins: {
screencast: {
enabled: true,
on: 'fail',
}
}
```

#### `on=` modes

* **fail** — record while running; delete on pass, keep on fail (default)
* **test** — record and keep every test's video

CLI examples:

npx codeceptjs run -p screencast
npx codeceptjs run -p screencast:on=test
npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true

Possible config options:

* `captions`: burn-in action overlays via `page.screencast.showActions()`. Default: true.
* `subtitles`: also write a standalone `.srt` file alongside the video. Default: false.
* `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.
* `size`: pass-through `{ width, height }` for `screencast.start`.
* `quality`: pass-through 0–100 for `screencast.start`.

> Enabling Playwright's helper-level `video: true` and this plugin together
> produces two independent recordings. Pick one.

### Parameters

* `config` &#x20;

## screenshot

Saves screenshots from the browser at points triggered by `on=`. Replaces the
Expand Down Expand Up @@ -812,20 +853,6 @@ plugins: {

* `config` &#x20;

## subtitles

Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test

#### Configuration

```js
plugins: {
subtitles: {
enabled: true
}
}
```

[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

[2]: https://github.com/cenfun/monocart-coverage-reports?tab=readme-ov-file#default-options
Expand Down
4 changes: 3 additions & 1 deletion examples/codecept.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ export const config = {
retryFailedStep: {
enabled: false,
},
subtitles: {
screencast: {
enabled: true,
on: 'test',
subtitles: true,
},
aiTrace: {
enabled: true,
Expand Down
6 changes: 1 addition & 5 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
import Popup from './extras/Popup.js'
import Console from './extras/Console.js'
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
import { findReact, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
import { dropFile } from './scripts/dropFile.js'
import WebElement from '../element/WebElement.js'
import { selectElement } from './extras/elementSelection.js'
Expand Down Expand Up @@ -4223,13 +4223,10 @@ async function findByRole(context, locator) {
}

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

if (isReactLocator) return findReact(matcher, locator)
if (isVueLocator) return findVue(matcher, locator)
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)

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

async function findElement(matcher, locator) {
if (locator.react) return findReact(matcher, locator)
if (locator.vue) return findVue(matcher, locator)
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)

locator = new Locator(locator, 'css')
Expand Down
81 changes: 45 additions & 36 deletions lib/helper/extras/PlaywrightReactVueLocator.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,61 @@
import fs from 'fs'
import { fileURLToPath } from 'url'

let resqScript

async function findReact(matcher, locator) {
// Handle both Locator objects and raw locator objects
const reactLocator = locator.locator || locator
let _locator = `_react=${reactLocator.react}`;
let props = '';
const page = typeof matcher.page === 'function' ? matcher.page() : matcher

if (reactLocator.props) {
props += propBuilder(reactLocator.props);
_locator += props;
if (!resqScript) {
resqScript = fs.readFileSync(fileURLToPath(import.meta.resolve('resq'))).toString()
}
return matcher.locator(_locator).all();
}
await page.evaluate(resqScript)
await page.evaluate(() => window.resq.waitToLoadReact())

const arrayHandle = await page.evaluateHandle(
({ selector, props, state }) => {
let elements = window.resq.resq$$(selector)
if (Object.keys(props).length) elements = elements.byProps(props)
if (Object.keys(state).length) elements = elements.byState(state)
if (!elements.length) return []

async function findVue(matcher, locator) {
// Handle both Locator objects and raw locator objects
const vueLocator = locator.locator || locator
let _locator = `_vue=${vueLocator.vue}`;
let props = '';
let nodes = []
elements.forEach(element => {
let { node, isFragment } = element
if (!node) {
isFragment = true
node = element.children
}
if (isFragment) nodes = nodes.concat(node)
else nodes.push(node)
})
return [...nodes]
},
{
selector: reactLocator.react,
props: reactLocator.props || {},
state: reactLocator.state || {},
},
)

if (vueLocator.props) {
props += propBuilder(vueLocator.props);
_locator += props;
const properties = await arrayHandle.getProperties()
await arrayHandle.dispose()
const result = []
for (const property of properties.values()) {
const elementHandle = property.asElement()
if (elementHandle) result.push(elementHandle)
}
return matcher.locator(_locator).all();
return result
}

async function findByPlaywrightLocator(matcher, locator) {
// Handle both Locator objects and raw locator objects
const pwLocator = locator.locator || locator
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
return matcher.getByTestId(pwLocator.pw.value.split('=')[1]);
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
}
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
return matcher.locator(pwValue).all();
}

function propBuilder(props) {
let _props = '';

for (const [key, value] of Object.entries(props)) {
if (typeof value === 'object') {
for (const [k, v] of Object.entries(value)) {
_props += `[${key}.${k} = "${v}"]`;
}
} else {
_props += `[${key} = "${value}"]`;
}
}
return _props;
return matcher.locator(pwValue).all()
}

export { findReact, findVue, findByPlaywrightLocator };
export { findReact, findByPlaywrightLocator }
Loading