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
1 change: 1 addition & 0 deletions .docker/app/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"phpstan/phpstan-deprecation-rules": "2.0.4",
"phpunit/phpunit": "11.5.55",
"rregeer/phpunit-coverage-check": "0.3.1",
"silverstripe/htmleditor-tinymce": "1.1.0",
"tomasvotruba/type-coverage": "2.2.1",
"wernerkrauss/silverstripe-rector": "1.3.0"
},
Expand Down
25 changes: 23 additions & 2 deletions client/playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Reusable Playwright helpers for the `wedevelopnl/silverstripe-e2e` fixture endpoint.

## Requirements

The client ships only as a source file inside the Composer package
(`vendor/wedevelopnl/silverstripe-e2e/client/playwright/index.ts`) — it is not
published to npm. The machine that runs Playwright must therefore have the
module's Composer dependencies installed (i.e. `vendor/` present), even when the
application's PHP otherwise runs entirely inside Docker. In CI, run `composer
install` on the Playwright runner (or mount the container's `vendor/`) before
`playwright test`.

## Usage in a consuming project

Add a path alias in the project's `tsconfig.json` (or `tests/E2E/tsconfig.json`):
Expand Down Expand Up @@ -34,12 +44,23 @@ import { createFixtureClient } from '@wedevelop/e2e';

const fixtures = createFixtureClient(); // defaults to /dev/e2e-fixtures

test('...', async ({ page, request }) => {
const { pageId } = await fixtures.loadAndNavigate(page, request, 'my-fixture');
test('...', async ({ page }) => {
// `request` defaults to `page.request` (shares the page's auth cookies); pass
// an explicit APIRequestContext as the third argument to override.
const { pageId } = await fixtures.loadAndNavigate(page, 'my-fixture');
// ...
});
```

`loadAndNavigate` is a convenience wrapper for the common case of editing the
loaded record on the CMS pages screen (`/admin/pages/edit/show/{pageId}`). To
navigate elsewhere, compose the pieces directly:

```ts
const { pageId } = await fixtures.load(request, 'my-fixture');
await page.goto(`/admin/some-other-section/${pageId}`);
```

The `/dev/e2e-fixtures` endpoint and the `strict_user_agent_check` relaxation are
provided automatically by the module's dev-only config when installed. Register your
fixtures and `fixture_page_classes` in your project's own dev config:
Expand Down
18 changes: 16 additions & 2 deletions client/playwright/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ export interface FixtureClientOptions {
* `loadAndNavigate` waits for it to become hidden after navigation.
*/
editorReadySelector?: string;
/**
* Timeout (ms) for the `editorReadySelector` wait in `loadAndNavigate`. When
* omitted, Playwright's default action timeout applies.
*/
editorReadyTimeout?: number;
}

export function createFixtureClient(options: FixtureClientOptions = {}) {
const endpoint = options.endpoint ?? '/dev/e2e-fixtures';
const editorReadySelector = options.editorReadySelector ?? null;
const editorReadyTimeout = options.editorReadyTimeout;

async function load(request: APIRequestContext, fixture: string): Promise<FixtureLoadResponse> {
const response = await request.post(`${endpoint}/load`, { form: { fixture } });
Expand Down Expand Up @@ -71,17 +77,25 @@ export function createFixtureClient(options: FixtureClientOptions = {}) {
if (!response.ok()) {
throw new Error(`Fixture reset failed (${response.status()}): ${await response.text()}`);
}

const body = (await response.json()) as { success: boolean; error?: string };
if (!body.success) {
throw new Error(`Fixture reset failed: ${body.error ?? 'unknown error'}`);
}
}

async function loadAndNavigate(
page: Page,
request: APIRequestContext,
fixture: string,
request: APIRequestContext = page.request,
): Promise<FixtureLoadResponse> {
const result = await load(request, fixture);
await page.goto(`/admin/pages/edit/show/${result.pageId}`);
if (editorReadySelector !== null) {
await page.locator(editorReadySelector).waitFor({ state: 'hidden' });
await page.locator(editorReadySelector).waitFor({
state: 'hidden',
timeout: editorReadyTimeout,
});
}

return result;
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
}],
"require": {
"php": "^8.3",
"silverstripe/framework": "^6",
"silverstripe/htmleditor-tinymce": "^1.1"
"silverstripe/framework": "^6"
},
"require-dev": {
"cambis/silverstan": "^2.1",
Expand All @@ -25,9 +24,13 @@
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^11.3",
"rregeer/phpunit-coverage-check": "^0.3",
"silverstripe/htmleditor-tinymce": "^1.1",
"tomasvotruba/type-coverage": "^2.0",
"wernerkrauss/silverstripe-rector": "^1.0"
},
"suggest": {
"silverstripe/htmleditor-tinymce": "Required only for the generate-tinymce-combined CI task; fixtures-only consumers do not need it (^1.1)."
},
"autoload": {
"psr-4": {
"WeDevelop\\E2e\\": "src/"
Expand Down
15 changes: 15 additions & 0 deletions src/Tasks/GenerateTinyMCECombinedTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ class GenerateTinyMCECombinedTask extends BuildTask

protected function execute(InputInterface $input, PolyOutput $output): int
{
// silverstripe/htmleditor-tinymce is a suggested (optional) dependency:
// only this CI helper needs it, while fixtures-only consumers do not.
// Skip cleanly when it is absent rather than fataling on the TinyMCE
// classes used below.
if (!class_exists(TinyMCECombinedGenerator::class)) {
// @codeCoverageIgnoreStart
// Unreachable in this module's own test run, which require-dev's TinyMCE.
$output->writeln(
'silverstripe/htmleditor-tinymce is not installed; skipping TinyMCE asset generation',
);

return Command::SUCCESS;
// @codeCoverageIgnoreEnd
}

TinyMCECombinedGenerator::flush();

$editorConfigs = HTMLEditorConfig::get_available_configs_map();
Expand Down
4 changes: 2 additions & 2 deletions tests/E2E/specs/fixture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ test('loads a fixture over HTTP and returns the created page id', async ({ reque
expect(result.fixtureMap.Page).toBeDefined();
});

test('navigates to the loaded page in the CMS', async ({ page, request }) => {
const result = await fixtures.loadAndNavigate(page, request, 'demo-home');
test('navigates to the loaded page in the CMS', async ({ page }) => {
const result = await fixtures.loadAndNavigate(page, 'demo-home');

await expect(page).toHaveURL(new RegExp(`/admin/pages/edit/show/${result.pageId}`));
await expect(page.getByText('E2E Home').first()).toBeVisible();
Expand Down
Loading