@lowdefy/e2e-utils 0.0.0-experimental-20260311112138 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -40,7 +40,12 @@ function createConfig({ appDir = './', buildDir = '.lowdefy/server/build', comma
40
40
  use: {
41
41
  baseURL: `http://localhost:${port}`,
42
42
  trace: 'on-first-retry',
43
- screenshot
43
+ screenshot,
44
+ ...process.env.SLOW_MO && {
45
+ launchOptions: {
46
+ slowMo: Number(process.env.SLOW_MO)
47
+ }
48
+ }
44
49
  },
45
50
  projects: [
46
51
  {
@@ -102,7 +107,12 @@ function createMultiAppConfig({ apps = [], commandPrefix = '', testDir = 'e2e',
102
107
  outputDir,
103
108
  use: {
104
109
  trace: 'on-first-retry',
105
- screenshot
110
+ screenshot,
111
+ ...process.env.SLOW_MO && {
112
+ launchOptions: {
113
+ slowMo: Number(process.env.SLOW_MO)
114
+ }
115
+ }
106
116
  },
107
117
  projects,
108
118
  webServer
@@ -0,0 +1,18 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ function escapeId(id) {
16
+ return id.replace(/([^\w-])/g, '\\$1');
17
+ }
18
+ export { escapeId };
@@ -12,7 +12,8 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ function getBlock(page, blockId) {
16
- return page.locator(`#bl-${blockId}`);
15
+ */ import { escapeId } from './escapeId.js';
16
+ function getBlock(page, blockId) {
17
+ return page.locator(`#bl-${escapeId(blockId)}`);
17
18
  }
18
19
  export { getBlock };
package/dist/index.js CHANGED
@@ -13,12 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ // Core helpers
16
+ import { escapeId } from './core/escapeId.js';
16
17
  import { getBlock } from './core/locators.js';
17
18
  import { goto, waitForReady, expectNavigation, waitForPage } from './core/navigation.js';
18
19
  import { getRequestState, getRequestResponse, expectRequest } from './core/requests.js';
19
20
  import { getState, getBlockState, setState, expectState } from './core/state.js';
20
21
  import { getValidation, expectValidationStatus, expectValidationError, expectValidationWarning, expectValidationSuccess } from './core/validation.js';
21
22
  import { expectUrl, expectUrlQuery, setUrlQuery } from './core/url.js';
23
+ export { escapeId };
22
24
  export { getBlock };
23
25
  export { goto, waitForReady, expectNavigation, waitForPage };
24
26
  export { getRequestState, getRequestResponse, expectRequest };
@@ -66,10 +66,18 @@ function updatePackageJson({ appPath, cwd, useExperimental, useMongoDB }) {
66
66
  pkg.scripts.e2e = 'playwright test --config=e2e/playwright.config.js';
67
67
  updated = true;
68
68
  }
69
+ if (!pkg.scripts['e2e:headed']) {
70
+ pkg.scripts['e2e:headed'] = 'SLOW_MO=500 playwright test --headed --workers=1 --config=e2e/playwright.config.js';
71
+ updated = true;
72
+ }
69
73
  if (!pkg.scripts['e2e:ui']) {
70
74
  pkg.scripts['e2e:ui'] = 'playwright test --ui --config=e2e/playwright.config.js';
71
75
  updated = true;
72
76
  }
77
+ if (!pkg.scripts['e2e:server']) {
78
+ pkg.scripts['e2e:server'] = 'lowdefy build --server e2e && lowdefy start --port 3000 --log-level warn';
79
+ updated = true;
80
+ }
73
81
  // Add devDependencies
74
82
  const tag = useExperimental ? 'experimental' : 'latest';
75
83
  pkg.devDependencies = pkg.devDependencies || {};
@@ -142,8 +142,10 @@ async function init() {
142
142
  const app = selectedApps[0];
143
143
  console.log(`Run tests from ${app.path}:`);
144
144
  console.log(` cd ${app.path}`);
145
- console.log(' pnpm e2e');
146
- console.log(' pnpm e2e:ui # for UI mode');
145
+ console.log(' pnpm e2e # Run all tests');
146
+ console.log(' pnpm e2e:headed # Run with visible browser');
147
+ console.log(' pnpm e2e:ui # Playwright UI mode');
148
+ console.log(' pnpm e2e:server # Start server for reuse');
147
149
  } else {
148
150
  console.log('Run tests from each app directory:');
149
151
  for (const app of selectedApps){
@@ -5,10 +5,32 @@ End-to-end tests for your Lowdefy app using Playwright.
5
5
  ## Running Tests
6
6
 
7
7
  ```bash
8
- pnpm e2e # Run all tests
9
- pnpm e2e:ui # Run in UI mode (interactive)
8
+ pnpm e2e # Run all tests
9
+ pnpm e2e:headed # Run with visible browser (slow motion)
10
+ pnpm e2e:ui # Playwright UI mode (interactive)
11
+ pnpm e2e:server # Start server for reuse (skip rebuilds)
10
12
  ```
11
13
 
14
+ ## Faster Test Runs
15
+
16
+ By default, `pnpm e2e` does a full build before running tests. To skip the build during development:
17
+
18
+ 1. **Start the server once** in a separate terminal:
19
+
20
+ ```bash
21
+ pnpm e2e:server
22
+ ```
23
+
24
+ 2. **Run tests** in another terminal — the config has `reuseExistingServer: true`, so Playwright detects the running server and skips the build:
25
+
26
+ ```bash
27
+ pnpm e2e
28
+ ```
29
+
30
+ This saves 60-90 seconds per run. Keep the server running while iterating on tests.
31
+
32
+ > **Tip:** If port 3000 is already in use, change the port in both the `e2e:server` script in `package.json` and the `port` option in `playwright.config.js`.
33
+
12
34
  ## File Structure
13
35
 
14
36
  ```
@@ -136,6 +158,28 @@ await ldf.mock.request('fetch_user', {
136
158
  });
137
159
  ```
138
160
 
161
+ ## Common Patterns
162
+
163
+ ### Testing Page Navigation
164
+
165
+ When a form submit navigates away from the page (e.g., `Link: back`, redirects), use `Promise.all` with `page.waitForURL()` and the click action:
166
+
167
+ ```javascript
168
+ test('submits form and navigates back', async ({ ldf, page }) => {
169
+ await ldf.goto('/my-form');
170
+
171
+ await ldf.block('name_input').do.fill('Test');
172
+
173
+ // Wait for navigation and click simultaneously
174
+ await Promise.all([
175
+ page.waitForURL((url) => !url.pathname.includes('/my-form'), { timeout: 30000 }),
176
+ ldf.block('submit_btn').do.click(),
177
+ ]);
178
+ });
179
+ ```
180
+
181
+ **Why not `ldf.request().expect.toFinish()`?** When the page navigates away, the page context is destroyed. Any pending request assertions will fail because the Lowdefy runtime that tracks requests no longer exists. Use `page.waitForURL()` instead to assert that the navigation happened.
182
+
139
183
  ## Playwright Configuration
140
184
 
141
185
  Edit `playwright.config.js` to customize test behavior.
@@ -144,7 +188,7 @@ Edit `playwright.config.js` to customize test behavior.
144
188
 
145
189
  | Option | Default | Description |
146
190
  |--------|---------|-------------|
147
- | `appDir` | `'../'` | Path to your Lowdefy app root (relative to config file) |
191
+ | `appDir` | `'./'` | Path to your Lowdefy app root (relative to cwd) |
148
192
  | `buildDir` | `'.lowdefy/server/build'` | Build output directory (relative to appDir) |
149
193
  | `mocksFile` | `'e2e/mocks.yaml'` | Static mocks file path (relative to appDir) |
150
194
  | `port` | `3000` | Port for the test server |
@@ -160,7 +204,7 @@ Edit `playwright.config.js` to customize test behavior.
160
204
  import { createConfig } from '@lowdefy/e2e-utils/config';
161
205
 
162
206
  export default createConfig({
163
- appDir: '../',
207
+ appDir: './',
164
208
  port: 3001, // Use different port
165
209
  timeout: 300000, // 5 min timeout for slow builds
166
210
  screenshot: 'on', // Always capture screenshots
@@ -177,8 +221,8 @@ import { createMultiAppConfig } from '@lowdefy/e2e-utils/config';
177
221
 
178
222
  export default createMultiAppConfig({
179
223
  apps: [
180
- { name: 'admin', appDir: '../apps/admin', port: 3001 },
181
- { name: 'customer', appDir: '../apps/customer', port: 3002 },
224
+ { name: 'admin', appDir: './apps/admin', port: 3001 },
225
+ { name: 'customer', appDir: './apps/customer', port: 3002 },
182
226
  ],
183
227
  testDir: '.',
184
228
  timeout: 180000,
@@ -1,10 +1,8 @@
1
1
  import { test, expect } from '@lowdefy/e2e-utils/fixtures';
2
2
 
3
- test('homepage loads', async ({ ldf }) => {
4
- await ldf.goto('/');
5
-
6
- // Replace with your own assertions
7
- await expect(ldf.page).toHaveTitle(/./);
3
+ test('app is running', async ({ ldf }) => {
4
+ const response = await ldf.page.goto('/api/auth/session');
5
+ expect(response.status()).toBe(200);
8
6
  });
9
7
 
10
8
  // Example: Testing a form
@@ -1,5 +1,6 @@
1
1
  import { test as ldfTest, expect } from '@lowdefy/e2e-utils/fixtures';
2
- import { mdbFixture } from '@lowdefy/community-plugin-e2e-mdb';
2
+ import { mdbFixtures } from '@lowdefy/community-plugin-e2e-mdb/fixtures';
3
+ import { mergeTests } from '@playwright/test';
3
4
 
4
- export const test = ldfTest.extend(mdbFixture);
5
+ export const test = mergeTests(ldfTest, mdbFixtures);
5
6
  export { expect };
@@ -1,7 +1,7 @@
1
1
  import { createConfig } from '@lowdefy/e2e-utils/config';
2
2
 
3
3
  export default createConfig({
4
- appDir: '../', // Go up from e2e/ to app root
4
+ appDir: './', // Resolved relative to cwd (where pnpm e2e runs)
5
5
  testDir: '.', // Tests are in same dir as config
6
6
  port: 3000,
7
7
  // If your app needs environment variables (e.g., via infisical, dotenv-vault, aws-vault):
@@ -26,28 +26,16 @@ function generateManifest({ buildDir = '.lowdefy' }) {
26
26
  pages: {}
27
27
  };
28
28
  if (fs.existsSync(pagesDir)) {
29
- // Recursively find all .json page config files under pages/
30
- // Page artifacts are written as pages/{pageId}.json where pageId may contain slashes.
31
- // Skip files inside /requests/ subdirectories (those are request artifacts).
32
- function findPageFiles(dir, prefix) {
33
- for (const entry of fs.readdirSync(dir, {
34
- withFileTypes: true
35
- })){
36
- if (entry.isDirectory()) {
37
- if (entry.name === 'requests') continue;
38
- findPageFiles(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
39
- } else if (entry.isFile() && entry.name.endsWith('.json')) {
40
- const pageId = prefix ? `${prefix}/${entry.name.replace(/\.json$/, '')}` : entry.name.replace(/\.json$/, '');
41
- const pageConfigPath = path.join(dir, entry.name);
42
- const pageConfig = JSON.parse(fs.readFileSync(pageConfigPath, 'utf-8'));
43
- manifest.pages[pageId] = extractBlockMap({
44
- pageConfig,
45
- typesBlocks: types.blocks ?? {}
46
- });
47
- }
29
+ for (const pageId of fs.readdirSync(pagesDir)){
30
+ const pageConfigPath = path.join(pagesDir, pageId, `${pageId}.json`);
31
+ if (fs.existsSync(pageConfigPath)) {
32
+ const pageConfig = JSON.parse(fs.readFileSync(pageConfigPath, 'utf-8'));
33
+ manifest.pages[pageId] = extractBlockMap({
34
+ pageConfig,
35
+ typesBlocks: types.blocks ?? {}
36
+ });
48
37
  }
49
38
  }
50
- findPageFiles(pagesDir, '');
51
39
  }
52
40
  const manifestPath = path.join(buildDir, 'e2e-manifest.json');
53
41
  const tempPath = path.join(buildDir, `e2e-manifest.${process.pid}.tmp`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/e2e-utils",
3
- "version": "0.0.0-experimental-20260311112138",
3
+ "version": "4.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy E2E Testing Utilities for Playwright",
6
6
  "homepage": "https://lowdefy.com",
@@ -36,7 +36,7 @@
36
36
  "dist/*"
37
37
  ],
38
38
  "dependencies": {
39
- "@lowdefy/helpers": "0.0.0-experimental-20260311112138",
39
+ "@lowdefy/helpers": "4.7.0",
40
40
  "js-yaml": "4.1.0",
41
41
  "prompts": "2.4.2"
42
42
  },
@@ -46,7 +46,8 @@
46
46
  "devDependencies": {
47
47
  "@swc/cli": "0.1.63",
48
48
  "@swc/core": "1.3.99",
49
- "copyfiles": "2.4.1"
49
+ "copyfiles": "2.4.1",
50
+ "jest": "28.1.3"
50
51
  },
51
52
  "publishConfig": {
52
53
  "access": "public"
@@ -54,6 +55,7 @@
54
55
  "scripts": {
55
56
  "build": "swc src --out-dir dist --config-file ../../../.swcrc --delete-dir-on-start && pnpm copyfiles",
56
57
  "clean": "rm -rf dist",
57
- "copyfiles": "copyfiles -u 1 \"./src/init/templates/*\" dist"
58
+ "copyfiles": "copyfiles -u 1 \"./src/init/templates/*\" dist",
59
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
58
60
  }
59
61
  }