@lowdefy/e2e-utils 6.0.0 → 7.0.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.
@@ -0,0 +1,380 @@
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
+ */ // One table of DOM snippets, as Lowdefy blocks render them, with the journey target each one's
16
+ // element describes as. The engine's describeElement and the PostHog chain parser are both tested
17
+ // against it, so the two cannot drift from the journey runner's resolution.
18
+ //
19
+ // Each fixture:
20
+ // - html: the snippet, set as document.body.innerHTML.
21
+ // - element: a selector for the element the target names (what a click step acts on).
22
+ // - clicked: a selector for where a person's click lands, when that is inside the element (a
23
+ // label's text, a button's span). posthog-js records the clicked element.
24
+ // - target: the canonical target the element describes as, in describe form.
25
+ // - blockIds: the enclosing block ids, innermost first.
26
+ // - resolvable: whether the target resolves back to the element as a click. A dropdown option
27
+ // does not: it folds into a select step.
28
+ // - variants: other runner targets that resolve to the same element.
29
+ function target(fields) {
30
+ return {
31
+ block_id: null,
32
+ row: null,
33
+ column: null,
34
+ text: null,
35
+ nth: null,
36
+ option: false,
37
+ ...fields
38
+ };
39
+ }
40
+ const grid = `
41
+ <div id="bl-tasks" class="ag-theme-quartz">
42
+ <div class="ag-center-cols-container">
43
+ <div class="ag-row" row-index="0">
44
+ <div class="ag-cell" col-id="name">Write report</div>
45
+ <div class="ag-cell" col-id="done"><div class="ag-checkbox"><input type="checkbox" class="ag-checkbox-input" /></div></div>
46
+ <div class="ag-cell" col-id="move"><button aria-label="Up"></button><button aria-label="Down"></button></div>
47
+ <div class="ag-cell" col-id="actions"><button><span>Edit</span></button><button><span>Delete</span></button></div>
48
+ </div>
49
+ <div class="ag-row" row-index="1">
50
+ <div class="ag-cell" col-id="name">Send invoice</div>
51
+ <div class="ag-cell" col-id="done"><div class="ag-checkbox"><input type="checkbox" class="ag-checkbox-input" /></div></div>
52
+ <div class="ag-cell" col-id="move"><button aria-label="Up"></button><button aria-label="Down"></button></div>
53
+ <div class="ag-cell" col-id="actions"><button><span>Edit</span></button><button><span>Delete</span></button></div>
54
+ </div>
55
+ </div>
56
+ </div>`;
57
+ function selectorOptions({ blockId, options }) {
58
+ return `
59
+ <div id="bl-${blockId}">
60
+ <div class="ant-select ant-select-open">
61
+ <div class="ant-select-selector"><input id="${blockId}_input" role="combobox" type="search" /></div>
62
+ </div>
63
+ <div id="${blockId}_123_popup">
64
+ <div class="ant-select-dropdown">
65
+ <div class="rc-virtual-list">
66
+ ${options.map((option)=>`<div class="ant-select-item ant-select-item-option" title="${option}"><div class="ant-select-item-option-content">${option}</div></div>`).join('')}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>`;
71
+ }
72
+ const targetFixtures = [
73
+ {
74
+ name: 'control inside a block',
75
+ html: `
76
+ <div id="bl-page">
77
+ <div id="bl-save_button"><button type="button" class="ant-btn"><span>Save</span></button></div>
78
+ </div>`,
79
+ element: '#bl-save_button button',
80
+ clicked: '#bl-save_button button span',
81
+ target: target({
82
+ block_id: 'save_button',
83
+ text: 'Save'
84
+ }),
85
+ blockIds: [
86
+ 'save_button',
87
+ 'page'
88
+ ],
89
+ resolvable: true,
90
+ variants: [
91
+ {
92
+ block_id: 'save_button'
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ name: 'segmented option by label',
98
+ html: `
99
+ <div id="bl-period">
100
+ <div class="ant-segmented">
101
+ <div class="ant-segmented-group">
102
+ <label class="ant-segmented-item"><input class="ant-segmented-item-input" type="radio" /><div class="ant-segmented-item-label" title="Monthly">Monthly</div></label>
103
+ <label class="ant-segmented-item"><input class="ant-segmented-item-input" type="radio" /><div class="ant-segmented-item-label" title="Yearly">Yearly</div></label>
104
+ </div>
105
+ </div>
106
+ </div>`,
107
+ element: '#bl-period label:nth-of-type(2)',
108
+ clicked: '#bl-period label:nth-of-type(2) div',
109
+ target: target({
110
+ block_id: 'period',
111
+ text: 'Yearly'
112
+ }),
113
+ blockIds: [
114
+ 'period'
115
+ ],
116
+ resolvable: true,
117
+ variants: [
118
+ {
119
+ block_id: 'period',
120
+ nth: 1
121
+ }
122
+ ]
123
+ },
124
+ {
125
+ name: 'cell button by row and text',
126
+ html: grid,
127
+ element: '[row-index="1"] [col-id="actions"] button:nth-of-type(1)',
128
+ clicked: '[row-index="1"] [col-id="actions"] button:nth-of-type(1) span',
129
+ target: target({
130
+ block_id: 'tasks',
131
+ row: 1,
132
+ column: 'actions',
133
+ text: 'Edit'
134
+ }),
135
+ blockIds: [
136
+ 'tasks'
137
+ ],
138
+ resolvable: true,
139
+ variants: [
140
+ {
141
+ block_id: 'tasks',
142
+ row: 1,
143
+ text: 'Edit'
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ name: 'control in a cell by row and column',
149
+ html: grid,
150
+ element: '[row-index="0"] [col-id="done"] input',
151
+ target: target({
152
+ block_id: 'tasks',
153
+ row: 0,
154
+ column: 'done'
155
+ }),
156
+ blockIds: [
157
+ 'tasks'
158
+ ],
159
+ resolvable: true,
160
+ variants: []
161
+ },
162
+ {
163
+ name: 'nth control in a cell',
164
+ html: grid,
165
+ element: '[row-index="1"] [col-id="move"] button:nth-of-type(2)',
166
+ target: target({
167
+ block_id: 'tasks',
168
+ row: 1,
169
+ column: 'move',
170
+ nth: 1
171
+ }),
172
+ blockIds: [
173
+ 'tasks'
174
+ ],
175
+ resolvable: true,
176
+ variants: []
177
+ },
178
+ {
179
+ name: 'first of several controls in a cell, by nth 0',
180
+ html: grid,
181
+ element: '[row-index="1"] [col-id="move"] button:nth-of-type(1)',
182
+ target: target({
183
+ block_id: 'tasks',
184
+ row: 1,
185
+ column: 'move',
186
+ nth: 0
187
+ }),
188
+ blockIds: [
189
+ 'tasks'
190
+ ],
191
+ resolvable: true,
192
+ variants: []
193
+ },
194
+ {
195
+ name: 'first of several same-text controls in a block, by nth 0',
196
+ html: `
197
+ <div id="bl-toolbar">
198
+ <button type="button" class="ant-btn"><span>Delete</span></button>
199
+ <button type="button" class="ant-btn"><span>Delete</span></button>
200
+ </div>`,
201
+ element: '#bl-toolbar button:nth-of-type(1)',
202
+ clicked: '#bl-toolbar button:nth-of-type(1) span',
203
+ target: target({
204
+ block_id: 'toolbar',
205
+ text: 'Delete',
206
+ nth: 0
207
+ }),
208
+ blockIds: [
209
+ 'toolbar'
210
+ ],
211
+ resolvable: true,
212
+ variants: [
213
+ {
214
+ block_id: 'toolbar',
215
+ nth: 0
216
+ }
217
+ ]
218
+ },
219
+ {
220
+ name: 'page-wide text in a dialog',
221
+ html: `
222
+ <div id="bl-page"><div id="bl-delete_button"><button>Delete</button></div></div>
223
+ <div class="ant-modal-root">
224
+ <div role="dialog" class="ant-modal">
225
+ <div class="ant-modal-footer"><button><span>Cancel</span></button><button><span>Delete</span></button></div>
226
+ </div>
227
+ </div>`,
228
+ element: '[role="dialog"] button:nth-of-type(2)',
229
+ clicked: '[role="dialog"] button:nth-of-type(2) span',
230
+ target: target({
231
+ text: 'Delete'
232
+ }),
233
+ blockIds: [],
234
+ resolvable: true,
235
+ variants: []
236
+ },
237
+ {
238
+ name: 'page-wide text in a menu over a dialog',
239
+ html: `
240
+ <div role="dialog" class="ant-modal"><button>Archive</button></div>
241
+ <ul role="menu" class="ant-dropdown-menu"><li role="menuitem" class="ant-dropdown-menu-item">Archive</li></ul>`,
242
+ element: '[role="menuitem"]',
243
+ target: target({
244
+ text: 'Archive'
245
+ }),
246
+ blockIds: [],
247
+ resolvable: true,
248
+ variants: []
249
+ },
250
+ {
251
+ name: 'page-wide text on the page',
252
+ html: `
253
+ <div id="bl-page"><div id="bl-intro"><p>Welcome</p></div></div>
254
+ <div class="ant-popover"><div class="ant-popconfirm-buttons"><button><span>Confirm</span></button></div></div>`,
255
+ element: '.ant-popover button',
256
+ target: target({
257
+ text: 'Confirm'
258
+ }),
259
+ blockIds: [],
260
+ resolvable: true,
261
+ variants: []
262
+ },
263
+ {
264
+ name: 'text input',
265
+ html: `
266
+ <div id="bl-name">
267
+ <span class="ant-input-affix-wrapper"><input id="name_input" type="text" class="ant-input" /></span>
268
+ </div>`,
269
+ element: '#name_input',
270
+ target: target({
271
+ block_id: 'name'
272
+ }),
273
+ blockIds: [
274
+ 'name'
275
+ ],
276
+ resolvable: true,
277
+ variants: []
278
+ },
279
+ {
280
+ name: 'Box with its own onClick and no inner control',
281
+ html: `<div id="bl-order_card"><div class="box"><h4>Order 12</h4></div></div>`,
282
+ element: '#bl-order_card',
283
+ clicked: '#bl-order_card h4',
284
+ target: target({
285
+ block_id: 'order_card'
286
+ }),
287
+ blockIds: [
288
+ 'order_card'
289
+ ],
290
+ resolvable: true,
291
+ variants: []
292
+ },
293
+ {
294
+ name: 'Selector option inside its popup container',
295
+ html: selectorOptions({
296
+ blockId: 'country',
297
+ options: [
298
+ 'Germany',
299
+ 'France'
300
+ ]
301
+ }),
302
+ element: '#bl-country [title="France"] .ant-select-item-option-content',
303
+ target: target({
304
+ block_id: 'country',
305
+ text: 'France',
306
+ option: true
307
+ }),
308
+ blockIds: [
309
+ 'country'
310
+ ],
311
+ resolvable: false,
312
+ variants: []
313
+ },
314
+ {
315
+ name: 'AutoComplete option inside its popup container',
316
+ html: selectorOptions({
317
+ blockId: 'fruit',
318
+ options: [
319
+ 'Apple',
320
+ 'Banana'
321
+ ]
322
+ }),
323
+ element: '#bl-fruit [title="Apple"]',
324
+ clicked: '#bl-fruit [title="Apple"] .ant-select-item-option-content',
325
+ target: target({
326
+ block_id: 'fruit',
327
+ text: 'Apple',
328
+ option: true
329
+ }),
330
+ blockIds: [
331
+ 'fruit'
332
+ ],
333
+ resolvable: false,
334
+ variants: []
335
+ },
336
+ {
337
+ name: 'Title inside a clickable Card',
338
+ html: `
339
+ <div id="bl-card">
340
+ <div class="ant-card"><div class="ant-card-body">
341
+ <div id="bl-card_title"><h4 class="ant-typography">Orders</h4></div>
342
+ </div></div>
343
+ </div>`,
344
+ element: '#bl-card_title',
345
+ clicked: '#bl-card_title h4',
346
+ target: target({
347
+ block_id: 'card_title'
348
+ }),
349
+ blockIds: [
350
+ 'card_title',
351
+ 'card'
352
+ ],
353
+ resolvable: true,
354
+ variants: []
355
+ },
356
+ {
357
+ name: 'pinned grid row',
358
+ html: `
359
+ <div id="bl-totals">
360
+ <div class="ag-floating-top">
361
+ <div class="ag-row ag-row-pinned" row-index="t-0">
362
+ <div class="ag-cell" col-id="total"><button><span>Recalculate</span></button></div>
363
+ </div>
364
+ </div>
365
+ </div>`,
366
+ element: '[col-id="total"] button',
367
+ clicked: '[col-id="total"] button span',
368
+ target: target({
369
+ block_id: 'totals',
370
+ column: 'total',
371
+ text: 'Recalculate'
372
+ }),
373
+ blockIds: [
374
+ 'totals'
375
+ ],
376
+ resolvable: true,
377
+ variants: []
378
+ }
379
+ ];
380
+ export default targetFixtures;
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import fs from 'fs';
16
16
  import path from 'path';
17
+ import readBasePath from '../core/readBasePath.js';
17
18
  import extractBlockMap from './extractBlockMap.js';
18
19
  function generateManifest({ buildDir = '.lowdefy' }) {
19
20
  const typesPath = path.join(buildDir, 'types.json');
@@ -28,8 +29,19 @@ function generateManifest({ buildDir = '.lowdefy' }) {
28
29
  const blockMetas = fs.existsSync(blockMetasPath) ? JSON.parse(fs.readFileSync(blockMetasPath, 'utf-8')) : {};
29
30
  const pagesDir = path.join(buildDir, 'pages');
30
31
  const manifest = {
31
- pages: {}
32
+ basePath: readBasePath({
33
+ buildDir
34
+ }),
35
+ pages: {},
36
+ paths: {}
32
37
  };
38
+ // The path of every page served at a path other than its id, for building its URL.
39
+ const routes = JSON.parse(fs.readFileSync(path.join(buildDir, 'routes.json'), 'utf-8'));
40
+ routes.forEach((route)=>{
41
+ if (route.path !== route.pageId) {
42
+ manifest.paths[route.pageId] = route.path;
43
+ }
44
+ });
33
45
  if (fs.existsSync(pagesDir)) {
34
46
  // Recursively find all .json page config files under pages/
35
47
  // Page artifacts are written as pages/{pageId}.json where pageId may contain slashes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/e2e-utils",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy E2E Testing Utilities for Playwright",
6
6
  "homepage": "https://lowdefy.com",
@@ -31,20 +31,29 @@
31
31
  ".": "./dist/index.js",
32
32
  "./config": "./dist/config.js",
33
33
  "./fixtures": "./dist/fixtures/index.js",
34
- "./runtime": "./dist/runtime.js"
34
+ "./runtime": "./dist/runtime.js",
35
+ "./startServer": "./dist/startServer.js",
36
+ "./targets": "./dist/targets/index.js"
35
37
  },
36
38
  "files": [
37
39
  "dist/*"
38
40
  ],
39
41
  "dependencies": {
40
- "@lowdefy/helpers": "6.0.0",
41
- "js-yaml": "4.1.0",
42
+ "@lowdefy/helpers": "7.0.0",
43
+ "@lowdefy/node-utils": "7.0.0",
44
+ "js-yaml": "4.3.2",
42
45
  "prompts": "2.4.2"
43
46
  },
44
47
  "peerDependencies": {
45
48
  "@playwright/test": "1.59.1"
46
49
  },
50
+ "peerDependenciesMeta": {
51
+ "@playwright/test": {
52
+ "optional": true
53
+ }
54
+ },
47
55
  "devDependencies": {
56
+ "@jest/globals": "28.1.3",
48
57
  "@swc/cli": "0.8.1",
49
58
  "@swc/core": "1.15.32",
50
59
  "copyfiles": "2.4.1",