# @lowdefy/e2e-utils

> Lowdefy E2E Testing Utilities for Playwright

Latest version **5.6.0** (published 2026-08-28) · Apache-2.0 license · 0 weekly downloads

## Install

```sh
npm install @lowdefy/e2e-utils
pnpm add @lowdefy/e2e-utils
yarn add @lowdefy/e2e-utils
bun add @lowdefy/e2e-utils
```

Provides the command `lowdefy-e2e`.

## Health

**Score 65/100 (B)** — status: active.

Positive: esm support; no vulnerabilities; has provenance; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 5.6.0 |
| Published | 2026-08-28 |
| First published | 2026-02-02 |
| Weekly downloads | 0 |
| License | Apache-2.0 |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 3 |
| Unpacked size | 112.2 KB |
| Known vulnerabilities | 0 (+5 in 1 direct dependencies) |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 2999 |
| Maintainers | samtolmay, gervwyk, machielvdw |
| Keywords | lowdefy, e2e, playwright, testing |

## Links

- npm: https://www.npmjs.com/package/@lowdefy/e2e-utils
- Repository: https://github.com/lowdefy/lowdefy
- Homepage: https://lowdefy.com
- Issues: https://github.com/lowdefy/lowdefy/issues
- npm.io page: https://npm.io/package/@lowdefy/e2e-utils

## Dependencies (3)

- [js-yaml](https://npm.io/package/js-yaml.md) 4.1.0
- [prompts](https://npm.io/package/prompts.md) 2.4.2
- [@lowdefy/helpers](https://npm.io/package/@lowdefy/helpers.md) 5.6.0

## Alternatives

- [@snazzah/davey](https://npm.io/package/@snazzah/davey.md) — 1.5M weekly downloads
- [@vendure/testing](https://npm.io/package/@vendure/testing.md) — 8.3K weekly downloads
- [vue-simple-context-menu](https://npm.io/package/vue-simple-context-menu.md) — 6.6K weekly downloads
- [cypress-webpack-preprocessor-v5](https://npm.io/package/cypress-webpack-preprocessor-v5.md) — 2.1K weekly downloads
- [@backstage/plugin-catalog-backend-module-puppetdb](https://npm.io/package/@backstage/plugin-catalog-backend-module-puppetdb.md) — 1.3K weekly downloads

## Recent versions

- 5.6.0 (latest) — 2026-08-28
- 0.0.0-experimental-20260908154018 (experimental) — 2026-09-08
- 0.0.0-experimental-20260908140341 — 2026-09-08
- 0.0.0-experimental-20260908113207 — 2026-09-08
- 0.0.0-experimental-20260908103554 — 2026-09-08
- 0.0.0-experimental-20260906143808 — 2026-09-06
- 0.0.0-experimental-20260905100314 — 2026-09-05
- 0.0.0-experimental-20260905094305 — 2026-09-05
- 0.0.0-experimental-20260904141432 — 2026-09-04
- 0.0.0-experimental-20260903174536 — 2026-09-03
- 0.0.0-experimental-20260903141334 — 2026-09-03
- 0.0.0-experimental-20260831092225 — 2026-08-31
- 0.0.0-experimental-20260831061457 — 2026-08-31
- 0.0.0-experimental-20260831053108 — 2026-08-31
- 0.0.0-experimental-20260830202815 — 2026-08-30
- … 223 more at https://npm.io/package/@lowdefy/e2e-utils/versions

## README

# @lowdefy/e2e-utils

Playwright testing utilities for Lowdefy applications with a polymorphic block API.

## Installation

```bash
pnpm add -D @lowdefy/e2e-utils @playwright/test
npx playwright install chromium
```

## Quick Start

Initialize e2e testing in your Lowdefy app:

```bash
npx lowdefy-e2e init
```

This creates:

- `e2e/playwright.config.js` - Pre-configured Playwright config
- `e2e/example.spec.js` - Starter test to copy from

Run tests:

```bash
pnpm e2e        # Run tests
pnpm e2e:ui     # Run with Playwright UI
```

## Writing Tests

Import `test` from our fixtures - it includes the `ldf` helper automatically:

```javascript
import { test, expect } from '@lowdefy/e2e-utils/fixtures';

test('homepage loads', async ({ ldf }) => {
  await ldf.goto('/');
  await expect(ldf.page).toHaveTitle(/My App/);
});
```

### Block Interactions (`ldf.block('id').do.*`)

Interact with blocks using Playwright-style action verbs:

```javascript
test('submit form', async ({ ldf }) => {
  await ldf.goto('/contact');

  // Text inputs use .fill()
  await ldf.block('name_input').do.fill('John Doe');
  await ldf.block('email_input').do.fill('john@example.com');

  // Selectors use .select()
  await ldf.block('category').do.select('Support');

  // Buttons use .click()
  await ldf.block('submit_btn').do.click();

  // Clear an input
  await ldf.block('name_input').do.clear();
});
```

### Block Assertions (`ldf.block('id').expect.*`)

Common assertions like `visible`, `hidden`, `disabled`, `enabled` are auto-provided for every block. Validation assertions work automatically too:

```javascript
test('validates required fields', async ({ ldf }) => {
  await ldf.goto('/contact');

  await ldf.block('submit_btn').do.click();

  // Common assertions - available on every block
  await ldf.block('name_input').expect.visible();
  await ldf.block('submit_btn').expect.disabled();

  // Validation - available on every block
  await ldf.block('name_input').expect.validationError();
  await ldf.block('email_input').expect.validationError({ message: 'Email is required' });

  // Block-specific assertions
  await ldf.block('name_input').expect.value('');
  await ldf.block('submit_btn').expect.loading();
});
```

### Testing List Children (`ldf.list()` / `ldf.block()` with indexed ids)

List blocks render their slot children with runtime-prefixed ids
(`legal_rows.0.toggle`, `legal_rows.1.toggle`, ...). You can pass these indexed ids
straight into `ldf.block()` — the manifest stores `legal_rows.$.toggle` and the runtime
resolves the numeric segment automatically:

```javascript
await ldf.block('legal_rows.0.toggle').do.toggle();
await ldf.block('legal_rows.0.toggle').expect.checked();
```

For ergonomic row addressing, use `ldf.list(listId)`:

```javascript
// Number of rows in state
const count = await ldf.list('legal_rows').count();

// Positional access (synchronous)
await ldf.list('legal_rows').row(0).block('toggle').do.toggle();

// By key (async — reads list state to find the index)
const row = await ldf.list('legal_rows').rowBy('_id', 'bbbee');
await row.block('toggle').do.toggle();
await row.block('toggle').expect.checked();

// By predicate (async)
const archived = await ldf.list('legal_rows').rowWhere((it) => it.archived === true);
await archived.block('restore_btn').do.click();
```

Nested lists work the same way — every level of nesting adds another numeric
segment, all resolved through one templated lookup:

```javascript
await ldf.block('outer.0.inner.2.button').do.click();
```

### State (`ldf.state()`)

```javascript
// Assert state values
await ldf.state('submitted').expect.toBe(true);
await ldf.state('form.email').expect.toBe('john@example.com');

// Set state directly (useful for test setup)
await ldf.state('form.mode').do.set('edit');

// Get raw state value
const email = await ldf.state('form.email').value();
const fullState = await ldf.state().value();
```

### URL (`ldf.url()` / `ldf.urlQuery()`)

```javascript
// Assert URL
await ldf.url().expect.toBe('/thank-you');
await ldf.url().expect.toMatch(/\/tickets\/\d+/);

// Query params
await ldf.urlQuery('filter').expect.toBe('active');
await ldf.urlQuery('page').do.set('2');

// Get raw values
const currentUrl = ldf.url().value();
const filterValue = ldf.urlQuery('filter').value();
```

### Requests (`ldf.request()`)

```javascript
// Wait for request to finish
await ldf.request('send_message').expect.toFinish();

// Assert response
await ldf.request('fetch_users').expect.toHaveResponse([{ id: 1 }]);

// Assert payload sent to request
await ldf.request('search').expect.toHavePayload({ query: 'widget' });

// Get raw response
const response = await ldf.request('fetch_users').response();
```

### Mocking Requests

```javascript
// Mock for this test only (defaults to current page)
await ldf.mock.request('search_products', { response: [{ id: 1 }] });

// Mock with error
await ldf.mock.request('fetch_data', { error: 'Service unavailable' });

// Mock specific page's request
await ldf.mock.request('fetch_data', { pageId: 'admin-dashboard', response: [] });

// Mock API endpoints
await ldf.mock.api('external_api', { response: { status: 'ok' }, method: 'POST' });
```

### Raw Playwright Access

```javascript
// Raw Playwright page for custom assertions
await expect(ldf.page).toHaveTitle(/My App/);

// Raw block locator for custom assertions
const locator = ldf.block('submit_btn').locator();
await expect(locator).toHaveAttribute('data-loading', 'true');

// Block state and validation
const blockState = await ldf.block('name_input').state();
const validation = await ldf.block('email_input').validation();
```

## Complete Example

```javascript
import { test, expect } from '@lowdefy/e2e-utils/fixtures';

test('create ticket workflow', async ({ ldf }) => {
  await ldf.goto('/tickets/new');

  // Fill form
  await ldf.block('title_input').do.fill('Bug report');
  await ldf.block('description_input').do.fill('Login button not working');
  await ldf.block('priority_selector').do.select('High');

  // Submit
  await ldf.block('submit_btn').do.click();

  // Verify
  await ldf.request('create_ticket').expect.toFinish();
  await ldf.url().expect.toMatch(/\/tickets\/\d+/);
  await ldf.state('ticket.status').expect.toBe('open');
});
```

## Static Mocks (mocks.yaml)

Define mocks that apply to all tests:

```yaml
# e2e/mocks.yaml
requests:
  # Mock Atlas Search (can't run in tests)
  - requestId: atlas_search
    response:
      - _id: doc-1
        title: Search Result

  # Mock with pageId and wildcards
  - requestId: fetch_*
    pageId: admin-*
    response: []

api:
  - endpointId: external_api
    method: POST
    response:
      status: ok
```

## Testing Authentication

If your app uses auth, define a default test user in `mocks.yaml`:

```yaml
# e2e/mocks.yaml
user:
  name: Test User
  email: test@example.com
  roles:
    - admin
```

Override the user per test with `ldf.user()`, or clear it with `ldf.user(null)`:

```javascript
test('admin can access dashboard', async ({ ldf }) => {
  // Default user from mocks.yaml is used
  await ldf.goto('/dashboard');
  await ldf.url().expect.toBe('/dashboard');
});

test('unauthenticated user is redirected', async ({ ldf }) => {
  await ldf.user(null);
  await ldf.goto('/dashboard');
  await ldf.url().expect.toBe('/login');
});

test('viewer cannot access admin page', async ({ ldf }) => {
  await ldf.user({ name: 'Viewer', roles: ['viewer'] });
  await ldf.goto('/admin-settings');
  await ldf.url().expect.toBe('/login');
});
```

The user object maps directly to `lowdefy.user` — no session callbacks or transforms are applied.

## Playwright Config

```javascript
// e2e/playwright.config.js
import { createConfig } from '@lowdefy/e2e-utils/config';

export default createConfig({
  appDir: './', // Where lowdefy.yaml is (default: './')
  port: 3000, // Server port (default: 3000)
  testDir: 'e2e', // Test directory (default: 'e2e')
  testMatch: '**/*.spec.js',
  timeout: 180000, // WebServer timeout in ms (default: 180000)
});
```

## Adding E2E Support to a Block

Use `createBlockHelper` to define a block's e2e helper. Common methods (visible, hidden, disabled, enabled, validation) are auto-provided from the locator:

```javascript
// blocks-antd/src/blocks/MyBlock/e2e.js
import { createBlockHelper } from '@lowdefy/e2e-utils';
import { expect } from '@playwright/test';

const locator = (page, blockId) => page.locator(`#${blockId}_input`);

export default createBlockHelper({
  locator,
  do: {
    fill: (page, blockId, val) => locator(page, blockId).fill(val),
    clear: (page, blockId) => locator(page, blockId).clear(),
  },
  expect: {
    // Override defaults if needed (e.g., Selector uses CSS class for disabled)
    // disabled: (page, blockId) => expect(locator(page, blockId)).toHaveClass(/my-disabled/),
    value: (page, blockId, val) => expect(locator(page, blockId)).toHaveValue(val),
  },
});
```

Add the subpath export in `package.json`:

```json
"exports": {
  "./e2e/MyBlock": "./dist/blocks/MyBlock/e2e.js"
}
```

See [ARCHITECTURE.md](./ARCHITECTURE.md) for full technical details.

---
_Source: https://npm.io/package/@lowdefy/e2e-utils · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
