testblocks 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/executor.d.ts +32 -0
  4. package/dist/cli/executor.js +517 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +411 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-4hbFPUhP.js +2087 -0
  10. package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
  11. package/dist/client/assets/index-Dnk1ti7l.css +1 -0
  12. package/dist/client/index.html +25 -0
  13. package/dist/core/blocks/api.d.ts +2 -0
  14. package/dist/core/blocks/api.js +610 -0
  15. package/dist/core/blocks/data-driven.d.ts +2 -0
  16. package/dist/core/blocks/data-driven.js +245 -0
  17. package/dist/core/blocks/index.d.ts +15 -0
  18. package/dist/core/blocks/index.js +71 -0
  19. package/dist/core/blocks/lifecycle.d.ts +2 -0
  20. package/dist/core/blocks/lifecycle.js +199 -0
  21. package/dist/core/blocks/logic.d.ts +2 -0
  22. package/dist/core/blocks/logic.js +357 -0
  23. package/dist/core/blocks/playwright.d.ts +2 -0
  24. package/dist/core/blocks/playwright.js +764 -0
  25. package/dist/core/blocks/procedures.d.ts +5 -0
  26. package/dist/core/blocks/procedures.js +321 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.js +44 -0
  29. package/dist/core/plugins.d.ts +66 -0
  30. package/dist/core/plugins.js +118 -0
  31. package/dist/core/types.d.ts +153 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/server/codegenManager.d.ts +54 -0
  34. package/dist/server/codegenManager.js +259 -0
  35. package/dist/server/codegenParser.d.ts +17 -0
  36. package/dist/server/codegenParser.js +598 -0
  37. package/dist/server/executor.d.ts +37 -0
  38. package/dist/server/executor.js +672 -0
  39. package/dist/server/globals.d.ts +85 -0
  40. package/dist/server/globals.js +273 -0
  41. package/dist/server/index.d.ts +2 -0
  42. package/dist/server/index.js +361 -0
  43. package/dist/server/plugins.d.ts +55 -0
  44. package/dist/server/plugins.js +206 -0
  45. package/package.json +103 -0
@@ -0,0 +1,764 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.playwrightBlocks = void 0;
37
+ // Import Playwright's expect for assertions with auto-waiting
38
+ // We use dynamic import with string concatenation to prevent Vite from
39
+ // trying to bundle the playwright package (which is Node.js-only)
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ let playwrightExpect = null;
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ async function getExpect() {
44
+ if (!playwrightExpect) {
45
+ // Use string concatenation to hide import from Vite's static analysis
46
+ const moduleName = '@playwright' + '/test';
47
+ const { expect } = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
48
+ playwrightExpect = expect;
49
+ }
50
+ return playwrightExpect;
51
+ }
52
+ // Playwright Web Testing Blocks
53
+ exports.playwrightBlocks = [
54
+ // Navigate to URL
55
+ {
56
+ type: 'web_navigate',
57
+ category: 'Web',
58
+ color: '#E91E63',
59
+ tooltip: 'Navigate to a URL',
60
+ inputs: [
61
+ { name: 'URL', type: 'field', fieldType: 'text', required: true },
62
+ { name: 'WAIT_UNTIL', type: 'field', fieldType: 'dropdown', options: [['Load', 'load'], ['DOM Content Loaded', 'domcontentloaded'], ['Network Idle', 'networkidle']], default: 'load' },
63
+ ],
64
+ previousStatement: true,
65
+ nextStatement: true,
66
+ execute: async (params, context) => {
67
+ const page = context.page;
68
+ const url = resolveVariables(params.URL, context);
69
+ const waitUntil = params.WAIT_UNTIL;
70
+ context.logger.info(`Navigating to ${url}`);
71
+ await page.goto(url, { waitUntil });
72
+ return {
73
+ _summary: url,
74
+ url,
75
+ waitUntil,
76
+ };
77
+ },
78
+ },
79
+ // Click Element
80
+ {
81
+ type: 'web_click',
82
+ category: 'Web',
83
+ color: '#E91E63',
84
+ tooltip: 'Click on an element',
85
+ inputs: [
86
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
87
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
88
+ ],
89
+ previousStatement: true,
90
+ nextStatement: true,
91
+ execute: async (params, context) => {
92
+ const page = context.page;
93
+ const selector = resolveSelector(params, context);
94
+ const timeout = params.TIMEOUT;
95
+ context.logger.info(`Clicking: ${selector}`);
96
+ await page.click(selector, { timeout });
97
+ return {
98
+ _summary: params.SELECTOR,
99
+ selector,
100
+ };
101
+ },
102
+ },
103
+ // Fill Input
104
+ {
105
+ type: 'web_fill',
106
+ category: 'Web',
107
+ color: '#E91E63',
108
+ tooltip: 'Fill an input field (clears existing value)',
109
+ inputs: [
110
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
111
+ { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
112
+ ],
113
+ previousStatement: true,
114
+ nextStatement: true,
115
+ execute: async (params, context) => {
116
+ const page = context.page;
117
+ const selector = resolveSelector(params, context);
118
+ const value = resolveVariables(params.VALUE, context);
119
+ context.logger.info(`Filling ${selector} with "${value}"`);
120
+ await page.fill(selector, value);
121
+ const displayValue = value.length > 30 ? value.substring(0, 30) + '...' : value;
122
+ return {
123
+ _summary: `${params.SELECTOR} = "${displayValue}"`,
124
+ selector,
125
+ value,
126
+ };
127
+ },
128
+ },
129
+ // Type Text
130
+ {
131
+ type: 'web_type',
132
+ category: 'Web',
133
+ color: '#E91E63',
134
+ tooltip: 'Type text character by character',
135
+ inputs: [
136
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
137
+ { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
138
+ { name: 'DELAY', type: 'field', fieldType: 'number', default: 50 },
139
+ ],
140
+ previousStatement: true,
141
+ nextStatement: true,
142
+ execute: async (params, context) => {
143
+ const page = context.page;
144
+ const selector = resolveSelector(params, context);
145
+ const text = resolveVariables(params.TEXT, context);
146
+ const delay = params.DELAY;
147
+ context.logger.info(`Typing "${text}" into ${selector}`);
148
+ await page.type(selector, text, { delay });
149
+ const displayText = text.length > 30 ? text.substring(0, 30) + '...' : text;
150
+ return {
151
+ _summary: `${params.SELECTOR} = "${displayText}"`,
152
+ selector,
153
+ text,
154
+ delay,
155
+ };
156
+ },
157
+ },
158
+ // Press Key
159
+ {
160
+ type: 'web_press_key',
161
+ category: 'Web',
162
+ color: '#E91E63',
163
+ tooltip: 'Press a keyboard key',
164
+ inputs: [
165
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
166
+ { name: 'KEY', type: 'field', fieldType: 'dropdown', options: [['Enter', 'Enter'], ['Tab', 'Tab'], ['Escape', 'Escape'], ['Backspace', 'Backspace'], ['ArrowUp', 'ArrowUp'], ['ArrowDown', 'ArrowDown'], ['ArrowLeft', 'ArrowLeft'], ['ArrowRight', 'ArrowRight']] },
167
+ ],
168
+ previousStatement: true,
169
+ nextStatement: true,
170
+ execute: async (params, context) => {
171
+ const page = context.page;
172
+ const selector = resolveSelector(params, context);
173
+ const key = params.KEY;
174
+ context.logger.info(`Pressing ${key} on ${selector}`);
175
+ await page.press(selector, key);
176
+ return {
177
+ _summary: `${key} on ${params.SELECTOR}`,
178
+ selector,
179
+ key,
180
+ };
181
+ },
182
+ },
183
+ // Select Option
184
+ {
185
+ type: 'web_select',
186
+ category: 'Web',
187
+ color: '#E91E63',
188
+ tooltip: 'Select an option from a dropdown',
189
+ inputs: [
190
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
191
+ { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
192
+ ],
193
+ previousStatement: true,
194
+ nextStatement: true,
195
+ execute: async (params, context) => {
196
+ const page = context.page;
197
+ const selector = resolveSelector(params, context);
198
+ const value = resolveVariables(params.VALUE, context);
199
+ context.logger.info(`Selecting "${value}" in ${selector}`);
200
+ await page.selectOption(selector, value);
201
+ return {
202
+ _summary: `${params.SELECTOR} = "${value}"`,
203
+ selector,
204
+ value,
205
+ };
206
+ },
207
+ },
208
+ // Check/Uncheck Checkbox
209
+ {
210
+ type: 'web_checkbox',
211
+ category: 'Web',
212
+ color: '#E91E63',
213
+ tooltip: 'Check or uncheck a checkbox',
214
+ inputs: [
215
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
216
+ { name: 'ACTION', type: 'field', fieldType: 'dropdown', options: [['Check', 'check'], ['Uncheck', 'uncheck']] },
217
+ ],
218
+ previousStatement: true,
219
+ nextStatement: true,
220
+ execute: async (params, context) => {
221
+ const page = context.page;
222
+ const selector = resolveSelector(params, context);
223
+ const action = params.ACTION;
224
+ context.logger.info(`${action === 'check' ? 'Checking' : 'Unchecking'} ${selector}`);
225
+ if (action === 'check') {
226
+ await page.check(selector);
227
+ }
228
+ else {
229
+ await page.uncheck(selector);
230
+ }
231
+ return {
232
+ _summary: `${action} ${params.SELECTOR}`,
233
+ selector,
234
+ action,
235
+ };
236
+ },
237
+ },
238
+ // Hover
239
+ {
240
+ type: 'web_hover',
241
+ category: 'Web',
242
+ color: '#E91E63',
243
+ tooltip: 'Hover over an element',
244
+ inputs: [
245
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
246
+ ],
247
+ previousStatement: true,
248
+ nextStatement: true,
249
+ execute: async (params, context) => {
250
+ const page = context.page;
251
+ const selector = resolveSelector(params, context);
252
+ context.logger.info(`Hovering over ${selector}`);
253
+ await page.hover(selector);
254
+ return {
255
+ _summary: params.SELECTOR,
256
+ selector,
257
+ };
258
+ },
259
+ },
260
+ // Wait for Element
261
+ {
262
+ type: 'web_wait_for_element',
263
+ category: 'Web',
264
+ color: '#9C27B0',
265
+ tooltip: 'Wait for an element to appear/disappear',
266
+ inputs: [
267
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
268
+ { name: 'STATE', type: 'field', fieldType: 'dropdown', options: [['Visible', 'visible'], ['Hidden', 'hidden'], ['Attached', 'attached'], ['Detached', 'detached']] },
269
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
270
+ ],
271
+ previousStatement: true,
272
+ nextStatement: true,
273
+ execute: async (params, context) => {
274
+ const page = context.page;
275
+ const selector = resolveSelector(params, context);
276
+ const state = params.STATE;
277
+ const timeout = params.TIMEOUT;
278
+ context.logger.info(`Waiting for ${selector} to be ${state}`);
279
+ await page.waitForSelector(selector, { state, timeout });
280
+ return {
281
+ _summary: `${params.SELECTOR} is ${state}`,
282
+ selector,
283
+ state,
284
+ };
285
+ },
286
+ },
287
+ // Wait for URL
288
+ {
289
+ type: 'web_wait_for_url',
290
+ category: 'Web',
291
+ color: '#9C27B0',
292
+ tooltip: 'Wait for URL to match',
293
+ inputs: [
294
+ { name: 'URL', type: 'field', fieldType: 'text', required: true },
295
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
296
+ ],
297
+ previousStatement: true,
298
+ nextStatement: true,
299
+ execute: async (params, context) => {
300
+ const page = context.page;
301
+ const url = resolveVariables(params.URL, context);
302
+ const timeout = params.TIMEOUT;
303
+ context.logger.info(`Waiting for URL to match: ${url}`);
304
+ await page.waitForURL(url, { timeout });
305
+ return {
306
+ _summary: url,
307
+ url,
308
+ };
309
+ },
310
+ },
311
+ // Wait (pause)
312
+ {
313
+ type: 'web_wait',
314
+ category: 'Web',
315
+ color: '#9C27B0',
316
+ tooltip: 'Wait for a specified time',
317
+ inputs: [
318
+ { name: 'MILLISECONDS', type: 'field', fieldType: 'number', default: 1000 },
319
+ ],
320
+ previousStatement: true,
321
+ nextStatement: true,
322
+ execute: async (params, context) => {
323
+ const page = context.page;
324
+ const ms = params.MILLISECONDS;
325
+ context.logger.info(`Waiting ${ms}ms`);
326
+ await page.waitForTimeout(ms);
327
+ return {
328
+ _summary: `${ms}ms`,
329
+ milliseconds: ms,
330
+ };
331
+ },
332
+ },
333
+ // Take Screenshot
334
+ {
335
+ type: 'web_screenshot',
336
+ category: 'Web',
337
+ color: '#607D8B',
338
+ tooltip: 'Take a screenshot',
339
+ inputs: [
340
+ { name: 'NAME', type: 'field', fieldType: 'text', default: 'screenshot' },
341
+ { name: 'FULL_PAGE', type: 'field', fieldType: 'checkbox', default: false },
342
+ ],
343
+ previousStatement: true,
344
+ nextStatement: true,
345
+ execute: async (params, context) => {
346
+ const page = context.page;
347
+ const name = params.NAME;
348
+ const fullPage = params.FULL_PAGE;
349
+ context.logger.info(`Taking screenshot: ${name}`);
350
+ const buffer = await page.screenshot({ fullPage });
351
+ return {
352
+ _summary: `${name}${fullPage ? ' (full page)' : ''}`,
353
+ name,
354
+ fullPage,
355
+ buffer: buffer.toString('base64'),
356
+ };
357
+ },
358
+ },
359
+ // Get Text Content
360
+ {
361
+ type: 'web_get_text',
362
+ category: 'Web',
363
+ color: '#2196F3',
364
+ tooltip: 'Get text content of an element',
365
+ inputs: [
366
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
367
+ ],
368
+ output: { type: 'String' },
369
+ execute: async (params, context) => {
370
+ const page = context.page;
371
+ const selector = resolveSelector(params, context);
372
+ const text = await page.textContent(selector);
373
+ context.logger.debug(`Text content of ${selector}: "${text}"`);
374
+ const displayText = text && text.length > 40 ? text.substring(0, 40) + '...' : text;
375
+ return {
376
+ _summary: `"${displayText}"`,
377
+ _value: text,
378
+ selector,
379
+ text,
380
+ };
381
+ },
382
+ },
383
+ // Get Attribute
384
+ {
385
+ type: 'web_get_attribute',
386
+ category: 'Web',
387
+ color: '#2196F3',
388
+ tooltip: 'Get attribute value of an element',
389
+ inputs: [
390
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
391
+ { name: 'ATTRIBUTE', type: 'field', fieldType: 'text', required: true },
392
+ ],
393
+ output: { type: 'String' },
394
+ execute: async (params, context) => {
395
+ const page = context.page;
396
+ const selector = resolveSelector(params, context);
397
+ const attribute = params.ATTRIBUTE;
398
+ const value = await page.getAttribute(selector, attribute);
399
+ context.logger.debug(`Attribute ${attribute} of ${selector}: "${value}"`);
400
+ return {
401
+ _summary: `${attribute} = "${value}"`,
402
+ _value: value,
403
+ selector,
404
+ attribute,
405
+ value,
406
+ };
407
+ },
408
+ },
409
+ // Get Input Value
410
+ {
411
+ type: 'web_get_input_value',
412
+ category: 'Web',
413
+ color: '#2196F3',
414
+ tooltip: 'Get current value of an input field',
415
+ inputs: [
416
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
417
+ ],
418
+ output: { type: 'String' },
419
+ execute: async (params, context) => {
420
+ const page = context.page;
421
+ const selector = resolveSelector(params, context);
422
+ const value = await page.inputValue(selector);
423
+ context.logger.debug(`Input value of ${selector}: "${value}"`);
424
+ const displayValue = value.length > 40 ? value.substring(0, 40) + '...' : value;
425
+ return {
426
+ _summary: `"${displayValue}"`,
427
+ _value: value,
428
+ selector,
429
+ value,
430
+ };
431
+ },
432
+ },
433
+ // Get Page Title
434
+ {
435
+ type: 'web_get_title',
436
+ category: 'Web',
437
+ color: '#2196F3',
438
+ tooltip: 'Get the page title',
439
+ inputs: [],
440
+ output: { type: 'String' },
441
+ execute: async (params, context) => {
442
+ const page = context.page;
443
+ const title = await page.title();
444
+ context.logger.debug(`Page title: "${title}"`);
445
+ return {
446
+ _summary: `"${title}"`,
447
+ _value: title,
448
+ title,
449
+ };
450
+ },
451
+ },
452
+ // Get Current URL
453
+ {
454
+ type: 'web_get_url',
455
+ category: 'Web',
456
+ color: '#2196F3',
457
+ tooltip: 'Get the current URL',
458
+ inputs: [],
459
+ output: { type: 'String' },
460
+ execute: async (params, context) => {
461
+ const page = context.page;
462
+ const url = page.url();
463
+ context.logger.debug(`Current URL: "${url}"`);
464
+ return {
465
+ _summary: url,
466
+ _value: url,
467
+ url,
468
+ };
469
+ },
470
+ },
471
+ // Assert Element Visible
472
+ {
473
+ type: 'web_assert_visible',
474
+ category: 'Web',
475
+ color: '#FF9800',
476
+ tooltip: 'Assert that an element is visible (auto-waits)',
477
+ inputs: [
478
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
479
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
480
+ ],
481
+ previousStatement: true,
482
+ nextStatement: true,
483
+ execute: async (params, context) => {
484
+ const page = context.page;
485
+ const selector = resolveSelector(params, context);
486
+ const displaySelector = getDisplaySelector(params, context);
487
+ const timeout = params.TIMEOUT;
488
+ const expect = await getExpect();
489
+ const locator = page.locator(selector);
490
+ context.logger.info(`Asserting ${displaySelector} is visible`);
491
+ await expect(locator).toBeVisible({ timeout });
492
+ context.logger.info(`✓ Element ${displaySelector} is visible`);
493
+ return {
494
+ _summary: `${displaySelector} is visible`,
495
+ selector,
496
+ isVisible: true,
497
+ };
498
+ },
499
+ },
500
+ // Assert Element Not Visible
501
+ {
502
+ type: 'web_assert_not_visible',
503
+ category: 'Web',
504
+ color: '#FF9800',
505
+ tooltip: 'Assert that an element is not visible (auto-waits)',
506
+ inputs: [
507
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
508
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
509
+ ],
510
+ previousStatement: true,
511
+ nextStatement: true,
512
+ execute: async (params, context) => {
513
+ const page = context.page;
514
+ const selector = resolveSelector(params, context);
515
+ const displaySelector = getDisplaySelector(params, context);
516
+ const timeout = params.TIMEOUT;
517
+ const expect = await getExpect();
518
+ const locator = page.locator(selector);
519
+ context.logger.info(`Asserting ${displaySelector} is not visible`);
520
+ await expect(locator).toBeHidden({ timeout });
521
+ context.logger.info(`✓ Element ${displaySelector} is not visible`);
522
+ return {
523
+ _summary: `${displaySelector} is not visible`,
524
+ selector,
525
+ isVisible: false,
526
+ };
527
+ },
528
+ },
529
+ // Assert Text Contains
530
+ {
531
+ type: 'web_assert_text_contains',
532
+ category: 'Web',
533
+ color: '#FF9800',
534
+ tooltip: 'Assert that element text contains expected value (auto-waits)',
535
+ inputs: [
536
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
537
+ { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
538
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
539
+ ],
540
+ previousStatement: true,
541
+ nextStatement: true,
542
+ execute: async (params, context) => {
543
+ const page = context.page;
544
+ const selector = resolveSelector(params, context);
545
+ const displaySelector = getDisplaySelector(params, context);
546
+ const expectedText = resolveVariables(params.TEXT, context);
547
+ const timeout = params.TIMEOUT;
548
+ const expect = await getExpect();
549
+ const locator = page.locator(selector);
550
+ context.logger.info(`Asserting ${displaySelector} contains "${expectedText}"`);
551
+ await expect(locator).toContainText(expectedText, { timeout });
552
+ context.logger.info(`✓ Element ${displaySelector} contains text "${expectedText}"`);
553
+ return {
554
+ _summary: `"${expectedText}" found`,
555
+ selector,
556
+ expectedText,
557
+ };
558
+ },
559
+ },
560
+ // Assert Text Equals
561
+ {
562
+ type: 'web_assert_text_equals',
563
+ category: 'Web',
564
+ color: '#FF9800',
565
+ tooltip: 'Assert that element text equals expected value (auto-waits)',
566
+ inputs: [
567
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
568
+ { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
569
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
570
+ ],
571
+ previousStatement: true,
572
+ nextStatement: true,
573
+ execute: async (params, context) => {
574
+ const page = context.page;
575
+ const selector = resolveSelector(params, context);
576
+ const displaySelector = getDisplaySelector(params, context);
577
+ const expectedText = resolveVariables(params.TEXT, context);
578
+ const timeout = params.TIMEOUT;
579
+ const expect = await getExpect();
580
+ const locator = page.locator(selector);
581
+ context.logger.info(`Asserting ${displaySelector} text equals "${expectedText}"`);
582
+ await expect(locator).toHaveText(expectedText, { timeout });
583
+ context.logger.info(`✓ Element ${displaySelector} text equals "${expectedText}"`);
584
+ return {
585
+ _summary: `"${expectedText}" matches`,
586
+ selector,
587
+ expectedText,
588
+ };
589
+ },
590
+ },
591
+ // Assert URL Contains
592
+ {
593
+ type: 'web_assert_url_contains',
594
+ category: 'Web',
595
+ color: '#FF9800',
596
+ tooltip: 'Assert that current URL contains expected value (auto-waits)',
597
+ inputs: [
598
+ { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
599
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
600
+ ],
601
+ previousStatement: true,
602
+ nextStatement: true,
603
+ execute: async (params, context) => {
604
+ const page = context.page;
605
+ const expectedText = resolveVariables(params.TEXT, context);
606
+ const timeout = params.TIMEOUT;
607
+ const expect = await getExpect();
608
+ // Escape special regex characters and create a regex pattern
609
+ const escapedText = expectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
610
+ const urlPattern = new RegExp(escapedText);
611
+ context.logger.info(`Asserting URL contains "${expectedText}"`);
612
+ await expect(page).toHaveURL(urlPattern, { timeout });
613
+ context.logger.info(`✓ URL contains "${expectedText}"`);
614
+ return {
615
+ _summary: `"${expectedText}" in URL`,
616
+ expectedText,
617
+ actualUrl: page.url(),
618
+ };
619
+ },
620
+ },
621
+ // Assert Title Contains
622
+ {
623
+ type: 'web_assert_title_contains',
624
+ category: 'Web',
625
+ color: '#FF9800',
626
+ tooltip: 'Assert that page title contains expected value (auto-waits)',
627
+ inputs: [
628
+ { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
629
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
630
+ ],
631
+ previousStatement: true,
632
+ nextStatement: true,
633
+ execute: async (params, context) => {
634
+ const page = context.page;
635
+ const expectedText = resolveVariables(params.TEXT, context);
636
+ const timeout = params.TIMEOUT;
637
+ const expect = await getExpect();
638
+ // Escape special regex characters and create a regex pattern
639
+ const escapedText = expectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
640
+ const titlePattern = new RegExp(escapedText);
641
+ context.logger.info(`Asserting title contains "${expectedText}"`);
642
+ await expect(page).toHaveTitle(titlePattern, { timeout });
643
+ context.logger.info(`✓ Title contains "${expectedText}"`);
644
+ return {
645
+ _summary: `"${expectedText}" in title`,
646
+ expectedText,
647
+ };
648
+ },
649
+ },
650
+ // Assert Element Enabled
651
+ {
652
+ type: 'web_assert_enabled',
653
+ category: 'Web',
654
+ color: '#FF9800',
655
+ tooltip: 'Assert that an element is enabled (auto-waits)',
656
+ inputs: [
657
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
658
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
659
+ ],
660
+ previousStatement: true,
661
+ nextStatement: true,
662
+ execute: async (params, context) => {
663
+ const page = context.page;
664
+ const selector = resolveSelector(params, context);
665
+ const displaySelector = getDisplaySelector(params, context);
666
+ const timeout = params.TIMEOUT;
667
+ const expect = await getExpect();
668
+ const locator = page.locator(selector);
669
+ context.logger.info(`Asserting ${displaySelector} is enabled`);
670
+ await expect(locator).toBeEnabled({ timeout });
671
+ context.logger.info(`✓ Element ${displaySelector} is enabled`);
672
+ return {
673
+ _summary: `${displaySelector} is enabled`,
674
+ selector,
675
+ isEnabled: true,
676
+ };
677
+ },
678
+ },
679
+ // Assert Checkbox Checked
680
+ {
681
+ type: 'web_assert_checked',
682
+ category: 'Web',
683
+ color: '#FF9800',
684
+ tooltip: 'Assert that a checkbox is checked (auto-waits)',
685
+ inputs: [
686
+ { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
687
+ { name: 'EXPECTED', type: 'field', fieldType: 'checkbox', default: true },
688
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
689
+ ],
690
+ previousStatement: true,
691
+ nextStatement: true,
692
+ execute: async (params, context) => {
693
+ const page = context.page;
694
+ const selector = resolveSelector(params, context);
695
+ const displaySelector = getDisplaySelector(params, context);
696
+ const expected = params.EXPECTED;
697
+ const timeout = params.TIMEOUT;
698
+ const expect = await getExpect();
699
+ const locator = page.locator(selector);
700
+ context.logger.info(`Asserting ${displaySelector} is ${expected ? 'checked' : 'unchecked'}`);
701
+ if (expected) {
702
+ await expect(locator).toBeChecked({ timeout });
703
+ }
704
+ else {
705
+ await expect(locator).not.toBeChecked({ timeout });
706
+ }
707
+ context.logger.info(`✓ Checkbox ${displaySelector} is ${expected ? 'checked' : 'unchecked'}`);
708
+ return {
709
+ _summary: `${displaySelector} is ${expected ? 'checked' : 'unchecked'}`,
710
+ selector,
711
+ expected,
712
+ };
713
+ },
714
+ },
715
+ ];
716
+ // Helper function - supports dot notation for object properties (e.g., ${user.email})
717
+ function resolveVariables(text, context) {
718
+ return text.replace(/\$\{([\w.]+)\}/g, (match, path) => {
719
+ const parts = path.split('.');
720
+ const varName = parts[0];
721
+ let value = context.variables.get(varName);
722
+ // Handle dot notation for nested object access
723
+ if (parts.length > 1 && value !== undefined && value !== null) {
724
+ for (let i = 1; i < parts.length; i++) {
725
+ if (value === undefined || value === null)
726
+ break;
727
+ value = value[parts[i]];
728
+ }
729
+ }
730
+ if (value === undefined || value === null)
731
+ return match;
732
+ if (typeof value === 'object')
733
+ return JSON.stringify(value);
734
+ return String(value);
735
+ });
736
+ }
737
+ /**
738
+ * Resolve a selector based on its type
739
+ * - For 'testid:value' format: constructs [testIdAttribute="value"] using the global testIdAttribute
740
+ * - For other formats: returns the selector as-is
741
+ */
742
+ function resolveSelector(params, context) {
743
+ const rawSelector = resolveVariables(params.SELECTOR, context);
744
+ // Check for testid: prefix (e.g., "testid:nav-sign-in")
745
+ if (rawSelector.startsWith('testid:')) {
746
+ const testIdValue = rawSelector.substring(7); // Remove 'testid:' prefix
747
+ const testIdAttr = context.testIdAttribute || 'data-testid';
748
+ return `[${testIdAttr}="${testIdValue}"]`;
749
+ }
750
+ return rawSelector;
751
+ }
752
+ /**
753
+ * Get a display-friendly version of the selector (strips testid: prefix)
754
+ */
755
+ function getDisplaySelector(params, context) {
756
+ const rawSelector = params.SELECTOR;
757
+ // For testid: prefix, show the actual CSS selector that will be used
758
+ if (rawSelector.startsWith('testid:')) {
759
+ const testIdValue = rawSelector.substring(7);
760
+ const testIdAttr = context.testIdAttribute || 'data-testid';
761
+ return `[${testIdAttr}="${testIdValue}"]`;
762
+ }
763
+ return rawSelector;
764
+ }