Repository navigation
Migrating an Existing Repo
You already have a Playwright or Cucumber suite. You do not need to rewrite it, restructure it, or move your specs into a Plum scaffold.
Plum handles the rest itself: the report format, the step reporter, browser project names, dependencies, and the browsers your own runner version needs. What is left is one line for Playwright and one for Cucumber, both below, and both only for session replay — reports and per-step timings need nothing from your code. Writing Tests lists exactly what Plum asks of a test and what it does not.
Plum bind-mounts one tree into its container: projects/. Your tests have to live under it, and the order matters.
1. Create the project in Plum first. Settings → Projects, or the first-run setup. That gives you projects/<slug>/ with a scaffolded tests/ folder.
2. Delete that scaffolded tests/ folder. You are bringing your own.
3. Clone your repo one level inside projects/<slug>/:
plum/
└─ projects/
└─ <slug>/
└─ app/ ← your repo, cloned here
├─ package.json
├─ playwright.config.ts
└─ tests/
The extra level is required, not stylistic. The folder you point Plum at has to be the one holding your runner config, and that setting cannot be .: an empty value or . falls back to tests. So a repo whose config sits at its own root has to live one level down.
4. Set Settings → Project → Tests folder to that subpath (app above). It is always relative to projects/<slug>/; absolute paths and .. are rejected. A project with a custom tests folder is never scaffolded.
5. Restart once so Plum fills in the recording files it needs.
Nesting deeper is fine. A monorepo works, and dependency resolution walks up, so a root node_modules is found from a nested tests folder. If your tests folder has no package.json of its own, Plum uses what the repo above installed.
-
Results. The report format is passed on the command line, so your config needs no reporter entry. A Plum run uses Plum's reporters, which means another reporter declared in your config is not written during one; your own
npx playwright testis unaffected. -
Browsers.
--projectis only passed when your config declares a project by that name, so a config namingDesktop Chrome, or declaring none at all, still runs. - Your runner version. Browser builds are tied to it and the image ships only Plum's, so if your repo pins a different version Plum downloads the browsers that version needs on the first run. Expect that run to take a couple of minutes; later runs reuse them. You do not have to match Plum's version.
-
Dependencies. Installed from your tests folder's
package.jsonbefore a run, or inherited from the repo root. -
Recording files. The restart in step 5 fills in the session-recording files your framework needs and nothing else. Existing files are never overwritten;
package.json,tsconfig.jsonand your runner config are skipped when the tree above already provides them. Plum will not scaffold over a folder that already contains tests.
Session replay needs test to come from Plum's fixture, because overriding the browser context is the only way to inject the recorder. Everything else stays vanilla: expect, Page, Locator, test.step, test.describe, your own fixtures, your config.
If your specs import @playwright/test directly, change the import in each one:
- import { test, expect } from '@playwright/test';
+ import { expect } from '@playwright/test';
+ import { test } from '../fixtures/plum';expect, Page, Locator and everything else keep coming from @playwright/test. Only test moves.
If you already have your own fixtures, which most mature repos do, do not rewrite your specs. Change the base of your own fixture file, one line, and every spec keeps its current import:
// tests/fixtures/test.ts
- import { test as base } from '@playwright/test';
+ import { test as base } from './plum';
export const test = base.extend<{ login: LoginPage }>({ … });Check which case you are in before editing anything: if your specs import test from a local module, that module is the only file to change.
Use Playwright's own test.step and the report shows a row per step:
await test.step('open the store', async () => { await page.goto('/'); });Plum reads them through a reporter of its own, which it adds to the run itself, so your specs need nothing but test.step — including the ones you put in a beforeEach, which Playwright's JSON report leaves out.
Cucumber runs with no config file at all, on its defaults. If your repo has none and you want session replay, create one, because only a config can load the recording hooks:
// cucumber.js
module.exports = {
default: {
requireModule: ['ts-node/register'],
require: ['features/**/*.js', 'utils/hooks.ts']
}
};That needs ts-node and typescript in your devDependencies, since the hooks are TypeScript.
Cucumber has no fixture equivalent, so recording is wired through hooks, which your cucumber.js has to load.
-
Require the hooks. Plum puts
utils/hooks.ts,utils/recorder.tsandutils/world.tsin your tests folder on restart, but only your config can load them:- require: ['steps/**/*.ts'], + require: ['steps/**/*.ts', 'utils/hooks.ts'],
Plum's
Beforehook opens a recorded browser and assigns it onto whatever World your scenarios already use, sothis.page,this.contextandthis.browserare available in your steps without giving up a World of your own. If you already open a browser yourself, drop that hook and use Plum's, or nothing gets recorded.utils/world.tsis only needed if you want the types; it is yours to edit. -
Honour the browser choice. Plum passes the selected browser as
BROWSER. If your launch code hardcodeschromium.launch(), the picker in the UI does nothing, whatever the report says. Read it:const name = (process.env.BROWSER || 'chromium').toLowerCase();
-
Set
retry: 0. Plum re-runs failed scenarios itself using the project's max-retries setting. A retry in your config compounds with it.
Tags are only needed to link automation to Test Repository cases and to run a single case from the UI. Plain runs and reports work untagged.
Give every test or scenario its own tag. Tags are identities: two tests sharing one cannot be told apart in a report or matched to separate cases. A Cucumber Scenario Outline shares its tag across every row.
test('User can log in', { tag: '@TC-001' }, async ({ page }) => { … });@TC-001
Scenario: User can log in| Playwright | Cucumber | |
|---|---|---|
create the project in Plum, delete its scaffolded tests/
|
yes | yes |
clone your repo one level inside projects/<slug>/
|
yes | yes |
| set the tests folder to the folder holding your config | yes | yes |
| restart once so Plum adds its files | yes | yes |
| reporter / format in your config | not needed | not needed |
| browser project names | not needed | not needed |
| match Plum’s runner version | not needed | not needed |
import test from the fixture |
one line per file, or one line total | n/a |
| load the hooks in your config | n/a | one line |
read BROWSER in your launch code |
n/a | if you want the picker to work |
retry: 0 |
n/a | recommended |
| tags | only for Test Repository linking | only for Test Repository linking |
If a run comes back with no scenarios, check the run log first: it names the reason.
Getting started
Tests
Platform
- Roles and Access
- Google Sign-In
- Test Repository
- Reports and Session Replay
- Retrying Flaky Tests
- Integrations
- AI Agent
- API & MCP
- Activity Logs
- Backup