@testsmith/testblocks 0.9.0 → 0.9.2

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.
@@ -16,7 +16,7 @@
16
16
  overflow: hidden;
17
17
  }
18
18
  </style>
19
- <script type="module" crossorigin src="/assets/index-Ivy7T1Qk.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-CVn_B7zc.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-C5yUtTzz.css">
21
21
  </head>
22
22
  <body>
@@ -14,7 +14,6 @@ exports.assertionBlocks = [
14
14
  tooltip: 'Assert that an element is visible (auto-waits)',
15
15
  inputs: [
16
16
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
17
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
18
17
  ],
19
18
  previousStatement: true,
20
19
  nextStatement: true,
@@ -22,7 +21,7 @@ exports.assertionBlocks = [
22
21
  const page = context.page;
23
22
  const selector = (0, utils_1.resolveSelector)(params, context);
24
23
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
25
- const timeout = params.TIMEOUT;
24
+ const timeout = (0, utils_1.getTimeout)(context);
26
25
  const expect = await (0, utils_1.getExpect)();
27
26
  const locator = page.locator(selector);
28
27
  context.logger.info(`Asserting ${displaySelector} is visible`);
@@ -43,7 +42,6 @@ exports.assertionBlocks = [
43
42
  tooltip: 'Assert that an element is not visible (auto-waits)',
44
43
  inputs: [
45
44
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
46
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
47
45
  ],
48
46
  previousStatement: true,
49
47
  nextStatement: true,
@@ -51,7 +49,7 @@ exports.assertionBlocks = [
51
49
  const page = context.page;
52
50
  const selector = (0, utils_1.resolveSelector)(params, context);
53
51
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
54
- const timeout = params.TIMEOUT;
52
+ const timeout = (0, utils_1.getTimeout)(context);
55
53
  const expect = await (0, utils_1.getExpect)();
56
54
  const locator = page.locator(selector);
57
55
  context.logger.info(`Asserting ${displaySelector} is not visible`);
@@ -73,7 +71,6 @@ exports.assertionBlocks = [
73
71
  inputs: [
74
72
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
75
73
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
76
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
77
74
  ],
78
75
  previousStatement: true,
79
76
  nextStatement: true,
@@ -82,7 +79,7 @@ exports.assertionBlocks = [
82
79
  const selector = (0, utils_1.resolveSelector)(params, context);
83
80
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
84
81
  const expectedText = (0, utils_1.resolveVariables)(params.TEXT, context);
85
- const timeout = params.TIMEOUT;
82
+ const timeout = (0, utils_1.getTimeout)(context);
86
83
  const expect = await (0, utils_1.getExpect)();
87
84
  const locator = page.locator(selector);
88
85
  context.logger.info(`Asserting ${displaySelector} contains "${expectedText}"`);
@@ -104,7 +101,6 @@ exports.assertionBlocks = [
104
101
  inputs: [
105
102
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
106
103
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
107
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
108
104
  ],
109
105
  previousStatement: true,
110
106
  nextStatement: true,
@@ -113,7 +109,7 @@ exports.assertionBlocks = [
113
109
  const selector = (0, utils_1.resolveSelector)(params, context);
114
110
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
115
111
  const expectedText = (0, utils_1.resolveVariables)(params.TEXT, context);
116
- const timeout = params.TIMEOUT;
112
+ const timeout = (0, utils_1.getTimeout)(context);
117
113
  const expect = await (0, utils_1.getExpect)();
118
114
  const locator = page.locator(selector);
119
115
  context.logger.info(`Asserting ${displaySelector} text equals "${expectedText}"`);
@@ -134,14 +130,13 @@ exports.assertionBlocks = [
134
130
  tooltip: 'Assert that current URL contains expected value (auto-waits)',
135
131
  inputs: [
136
132
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
137
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
138
133
  ],
139
134
  previousStatement: true,
140
135
  nextStatement: true,
141
136
  execute: async (params, context) => {
142
137
  const page = context.page;
143
138
  const expectedText = (0, utils_1.resolveVariables)(params.TEXT, context);
144
- const timeout = params.TIMEOUT;
139
+ const timeout = (0, utils_1.getTimeout)(context);
145
140
  const expect = await (0, utils_1.getExpect)();
146
141
  // Escape special regex characters and create a regex pattern
147
142
  const escapedText = expectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -164,14 +159,13 @@ exports.assertionBlocks = [
164
159
  tooltip: 'Assert that page title contains expected value (auto-waits)',
165
160
  inputs: [
166
161
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
167
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
168
162
  ],
169
163
  previousStatement: true,
170
164
  nextStatement: true,
171
165
  execute: async (params, context) => {
172
166
  const page = context.page;
173
167
  const expectedText = (0, utils_1.resolveVariables)(params.TEXT, context);
174
- const timeout = params.TIMEOUT;
168
+ const timeout = (0, utils_1.getTimeout)(context);
175
169
  const expect = await (0, utils_1.getExpect)();
176
170
  // Escape special regex characters and create a regex pattern
177
171
  const escapedText = expectedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -193,7 +187,6 @@ exports.assertionBlocks = [
193
187
  tooltip: 'Assert that an element is enabled (auto-waits)',
194
188
  inputs: [
195
189
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
196
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
197
190
  ],
198
191
  previousStatement: true,
199
192
  nextStatement: true,
@@ -201,7 +194,7 @@ exports.assertionBlocks = [
201
194
  const page = context.page;
202
195
  const selector = (0, utils_1.resolveSelector)(params, context);
203
196
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
204
- const timeout = params.TIMEOUT;
197
+ const timeout = (0, utils_1.getTimeout)(context);
205
198
  const expect = await (0, utils_1.getExpect)();
206
199
  const locator = page.locator(selector);
207
200
  context.logger.info(`Asserting ${displaySelector} is enabled`);
@@ -223,7 +216,6 @@ exports.assertionBlocks = [
223
216
  inputs: [
224
217
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
225
218
  { name: 'EXPECTED', type: 'field', fieldType: 'checkbox', default: true },
226
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
227
219
  ],
228
220
  previousStatement: true,
229
221
  nextStatement: true,
@@ -232,7 +224,7 @@ exports.assertionBlocks = [
232
224
  const selector = (0, utils_1.resolveSelector)(params, context);
233
225
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
234
226
  const expected = params.EXPECTED;
235
- const timeout = params.TIMEOUT;
227
+ const timeout = (0, utils_1.getTimeout)(context);
236
228
  const expect = await (0, utils_1.getExpect)();
237
229
  const locator = page.locator(selector);
238
230
  context.logger.info(`Asserting ${displaySelector} is ${expected ? 'checked' : 'unchecked'}`);
@@ -261,7 +253,6 @@ exports.assertionBlocks = [
261
253
  inputs: [
262
254
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
263
255
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
264
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
265
256
  ],
266
257
  previousStatement: true,
267
258
  nextStatement: true,
@@ -270,7 +261,7 @@ exports.assertionBlocks = [
270
261
  const selector = (0, utils_1.resolveSelector)(params, context);
271
262
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
272
263
  const expectedValue = (0, utils_1.resolveVariables)(params.VALUE, context);
273
- const timeout = params.TIMEOUT;
264
+ const timeout = (0, utils_1.getTimeout)(context);
274
265
  const expect = await (0, utils_1.getExpect)();
275
266
  const locator = page.locator(selector);
276
267
  context.logger.info(`Asserting ${displaySelector} has value "${expectedValue}"`);
@@ -294,7 +285,6 @@ exports.assertionBlocks = [
294
285
  inputs: [
295
286
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
296
287
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
297
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
298
288
  ],
299
289
  previousStatement: true,
300
290
  nextStatement: true,
@@ -303,7 +293,7 @@ exports.assertionBlocks = [
303
293
  const selector = (0, utils_1.resolveSelector)(params, context);
304
294
  const displaySelector = (0, utils_1.getDisplaySelector)(params, context);
305
295
  const expectedValue = (0, utils_1.resolveVariables)(params.VALUE, context);
306
- const timeout = params.TIMEOUT;
296
+ const timeout = (0, utils_1.getTimeout)(context);
307
297
  const expect = await (0, utils_1.getExpect)();
308
298
  const locator = page.locator(selector);
309
299
  context.logger.info(`Asserting ${displaySelector} value contains "${expectedValue}"`);
@@ -11,19 +11,23 @@ exports.interactionBlocks = [
11
11
  type: 'web_click',
12
12
  category: 'Web',
13
13
  color: '#E91E63',
14
- tooltip: 'Click on an element (auto-waits for element)',
14
+ tooltip: 'Click on an element (auto-waits for element to be visible and stable)',
15
15
  inputs: [
16
16
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
17
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
18
17
  ],
19
18
  previousStatement: true,
20
19
  nextStatement: true,
21
20
  execute: async (params, context) => {
22
21
  const page = context.page;
23
22
  const selector = (0, utils_1.resolveSelector)(params, context);
24
- const timeout = params.TIMEOUT;
23
+ const timeout = (0, utils_1.getTimeout)(context);
25
24
  context.logger.info(`Clicking: ${selector}`);
26
25
  const locator = page.locator(selector);
26
+ // Wait for element to be visible and stable before clicking
27
+ await locator.waitFor({ state: 'visible', timeout });
28
+ // Scroll into view to ensure element is in viewport
29
+ await locator.scrollIntoViewIfNeeded({ timeout });
30
+ // Click with retry on intercept (handles overlays/animations)
27
31
  await locator.click({ timeout });
28
32
  return {
29
33
  _summary: params.SELECTOR,
@@ -40,7 +44,6 @@ exports.interactionBlocks = [
40
44
  inputs: [
41
45
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
42
46
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
43
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
44
47
  ],
45
48
  previousStatement: true,
46
49
  nextStatement: true,
@@ -49,7 +52,7 @@ exports.interactionBlocks = [
49
52
  const selector = (0, utils_1.resolveSelector)(params, context);
50
53
  const rawValue = params.VALUE;
51
54
  const value = (0, utils_1.resolveVariables)(rawValue, context);
52
- const timeout = params.TIMEOUT;
55
+ const timeout = (0, utils_1.getTimeout)(context);
53
56
  context.logger.info(`Filling ${selector} with "${value}"`);
54
57
  const locator = page.locator(selector);
55
58
  await locator.fill(value, { timeout });
@@ -71,7 +74,6 @@ exports.interactionBlocks = [
71
74
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
72
75
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
73
76
  { name: 'DELAY', type: 'field', fieldType: 'number', default: 50 },
74
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
75
77
  ],
76
78
  previousStatement: true,
77
79
  nextStatement: true,
@@ -80,7 +82,7 @@ exports.interactionBlocks = [
80
82
  const selector = (0, utils_1.resolveSelector)(params, context);
81
83
  const text = (0, utils_1.resolveVariables)(params.TEXT, context);
82
84
  const delay = params.DELAY;
83
- const timeout = params.TIMEOUT;
85
+ const timeout = (0, utils_1.getTimeout)(context);
84
86
  context.logger.info(`Typing "${text}" into ${selector}`);
85
87
  const locator = page.locator(selector);
86
88
  await locator.pressSequentially(text, { delay, timeout });
@@ -102,7 +104,6 @@ exports.interactionBlocks = [
102
104
  inputs: [
103
105
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
104
106
  { name: 'KEY', type: 'field', fieldType: 'dropdown', options: [['Enter', 'Enter'], ['Tab', 'Tab'], ['Escape', 'Escape'], ['Backspace', 'Backspace'], ['ArrowUp', 'ArrowUp'], ['ArrowDown', 'ArrowDown'], ['ArrowLeft', 'ArrowLeft'], ['ArrowRight', 'ArrowRight']] },
105
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
106
107
  ],
107
108
  previousStatement: true,
108
109
  nextStatement: true,
@@ -110,7 +111,7 @@ exports.interactionBlocks = [
110
111
  const page = context.page;
111
112
  const selector = (0, utils_1.resolveSelector)(params, context);
112
113
  const key = params.KEY;
113
- const timeout = params.TIMEOUT;
114
+ const timeout = (0, utils_1.getTimeout)(context);
114
115
  context.logger.info(`Pressing ${key} on ${selector}`);
115
116
  const locator = page.locator(selector);
116
117
  await locator.press(key, { timeout });
@@ -130,7 +131,6 @@ exports.interactionBlocks = [
130
131
  inputs: [
131
132
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
132
133
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
133
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
134
134
  ],
135
135
  previousStatement: true,
136
136
  nextStatement: true,
@@ -138,7 +138,7 @@ exports.interactionBlocks = [
138
138
  const page = context.page;
139
139
  const selector = (0, utils_1.resolveSelector)(params, context);
140
140
  const value = (0, utils_1.resolveVariables)(params.VALUE, context);
141
- const timeout = params.TIMEOUT;
141
+ const timeout = (0, utils_1.getTimeout)(context);
142
142
  context.logger.info(`Selecting "${value}" in ${selector}`);
143
143
  const locator = page.locator(selector);
144
144
  await locator.selectOption(value, { timeout });
@@ -158,7 +158,6 @@ exports.interactionBlocks = [
158
158
  inputs: [
159
159
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
160
160
  { name: 'ACTION', type: 'field', fieldType: 'dropdown', options: [['Check', 'check'], ['Uncheck', 'uncheck']] },
161
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
162
161
  ],
163
162
  previousStatement: true,
164
163
  nextStatement: true,
@@ -166,7 +165,7 @@ exports.interactionBlocks = [
166
165
  const page = context.page;
167
166
  const selector = (0, utils_1.resolveSelector)(params, context);
168
167
  const action = params.ACTION;
169
- const timeout = params.TIMEOUT;
168
+ const timeout = (0, utils_1.getTimeout)(context);
170
169
  context.logger.info(`${action === 'check' ? 'Checking' : 'Unchecking'} ${selector}`);
171
170
  const locator = page.locator(selector);
172
171
  if (action === 'check') {
@@ -190,14 +189,13 @@ exports.interactionBlocks = [
190
189
  tooltip: 'Hover over an element (auto-waits)',
191
190
  inputs: [
192
191
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
193
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
194
192
  ],
195
193
  previousStatement: true,
196
194
  nextStatement: true,
197
195
  execute: async (params, context) => {
198
196
  const page = context.page;
199
197
  const selector = (0, utils_1.resolveSelector)(params, context);
200
- const timeout = params.TIMEOUT;
198
+ const timeout = (0, utils_1.getTimeout)(context);
201
199
  context.logger.info(`Hovering over ${selector}`);
202
200
  const locator = page.locator(selector);
203
201
  await locator.hover({ timeout });
@@ -40,7 +40,6 @@ exports.navigationBlocks = [
40
40
  inputs: [
41
41
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
42
42
  { name: 'STATE', type: 'field', fieldType: 'dropdown', options: [['Visible', 'visible'], ['Hidden', 'hidden'], ['Attached', 'attached'], ['Detached', 'detached']] },
43
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
44
43
  ],
45
44
  previousStatement: true,
46
45
  nextStatement: true,
@@ -48,7 +47,7 @@ exports.navigationBlocks = [
48
47
  const page = context.page;
49
48
  const selector = (0, utils_1.resolveSelector)(params, context);
50
49
  const state = params.STATE;
51
- const timeout = params.TIMEOUT;
50
+ const timeout = (0, utils_1.getTimeout)(context);
52
51
  context.logger.info(`Waiting for ${selector} to be ${state}`);
53
52
  await page.waitForSelector(selector, { state, timeout });
54
53
  return {
@@ -66,14 +65,13 @@ exports.navigationBlocks = [
66
65
  tooltip: 'Wait for URL to match',
67
66
  inputs: [
68
67
  { name: 'URL', type: 'field', fieldType: 'text', required: true },
69
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
70
68
  ],
71
69
  previousStatement: true,
72
70
  nextStatement: true,
73
71
  execute: async (params, context) => {
74
72
  const page = context.page;
75
73
  const url = (0, utils_1.resolveVariables)(params.URL, context);
76
- const timeout = params.TIMEOUT;
74
+ const timeout = (0, utils_1.getTimeout)(context);
77
75
  context.logger.info(`Waiting for URL to match: ${url}`);
78
76
  await page.waitForURL(url, { timeout });
79
77
  return {
@@ -14,13 +14,12 @@ exports.retrievalBlocks = [
14
14
  tooltip: 'Get text content of an element (auto-waits)',
15
15
  inputs: [
16
16
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
17
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
18
17
  ],
19
18
  output: { type: 'String' },
20
19
  execute: async (params, context) => {
21
20
  const page = context.page;
22
21
  const selector = (0, utils_1.resolveSelector)(params, context);
23
- const timeout = params.TIMEOUT;
22
+ const timeout = (0, utils_1.getTimeout)(context);
24
23
  const locator = page.locator(selector);
25
24
  await locator.waitFor({ state: 'visible', timeout });
26
25
  const text = await locator.textContent({ timeout });
@@ -43,14 +42,13 @@ exports.retrievalBlocks = [
43
42
  inputs: [
44
43
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
45
44
  { name: 'ATTRIBUTE', type: 'field', fieldType: 'text', required: true },
46
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
47
45
  ],
48
46
  output: { type: 'String' },
49
47
  execute: async (params, context) => {
50
48
  const page = context.page;
51
49
  const selector = (0, utils_1.resolveSelector)(params, context);
52
50
  const attribute = params.ATTRIBUTE;
53
- const timeout = params.TIMEOUT;
51
+ const timeout = (0, utils_1.getTimeout)(context);
54
52
  const locator = page.locator(selector);
55
53
  await locator.waitFor({ state: 'attached', timeout });
56
54
  const value = await locator.getAttribute(attribute, { timeout });
@@ -72,13 +70,12 @@ exports.retrievalBlocks = [
72
70
  tooltip: 'Get current value of an input field (auto-waits)',
73
71
  inputs: [
74
72
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
75
- { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
76
73
  ],
77
74
  output: { type: 'String' },
78
75
  execute: async (params, context) => {
79
76
  const page = context.page;
80
77
  const selector = (0, utils_1.resolveSelector)(params, context);
81
- const timeout = params.TIMEOUT;
78
+ const timeout = (0, utils_1.getTimeout)(context);
82
79
  const locator = page.locator(selector);
83
80
  await locator.waitFor({ state: 'visible', timeout });
84
81
  const value = await locator.inputValue({ timeout });
@@ -4,6 +4,7 @@
4
4
  export interface PlaywrightLocator {
5
5
  click(options?: {
6
6
  timeout?: number;
7
+ force?: boolean;
7
8
  }): Promise<void>;
8
9
  fill(value: string, options?: {
9
10
  timeout?: number;
@@ -51,6 +52,9 @@ export interface PlaywrightLocator {
51
52
  state?: 'attached' | 'detached' | 'visible' | 'hidden';
52
53
  timeout?: number;
53
54
  }): Promise<void>;
55
+ scrollIntoViewIfNeeded(options?: {
56
+ timeout?: number;
57
+ }): Promise<void>;
54
58
  }
55
59
  export interface PlaywrightPage {
56
60
  goto(url: string, options?: {
@@ -1,4 +1,8 @@
1
1
  import { ExecutionContext } from '../../types';
2
+ /**
3
+ * Get the global web timeout from context
4
+ */
5
+ export declare function getTimeout(context: ExecutionContext): number;
2
6
  export declare function getExpect(): Promise<any>;
3
7
  /**
4
8
  * Execute a web assertion with soft assertion support
@@ -33,11 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getTimeout = getTimeout;
36
37
  exports.getExpect = getExpect;
37
38
  exports.executeWebAssertion = executeWebAssertion;
38
39
  exports.resolveVariables = resolveVariables;
39
40
  exports.resolveSelector = resolveSelector;
40
41
  exports.getDisplaySelector = getDisplaySelector;
42
+ const DEFAULT_WEB_TIMEOUT = 30000;
43
+ /**
44
+ * Get the global web timeout from context
45
+ */
46
+ function getTimeout(context) {
47
+ return context.webTimeout ?? DEFAULT_WEB_TIMEOUT;
48
+ }
41
49
  // Import Playwright's expect for assertions with auto-waiting
42
50
  // We use dynamic import with string concatenation to prevent Vite from
43
51
  // trying to bundle the playwright package (which is Node.js-only)
@@ -106,6 +106,7 @@ export interface ExecutionContext {
106
106
  testIdAttribute?: string;
107
107
  softAssertions?: boolean;
108
108
  softAssertionErrors?: SoftAssertionError[];
109
+ webTimeout?: number;
109
110
  }
110
111
  export interface Logger {
111
112
  info(message: string, data?: unknown): void;
@@ -196,7 +196,32 @@ class TestExecutor {
196
196
  plugins: this.plugins,
197
197
  testIdAttribute: this.options.testIdAttribute,
198
198
  procedures: mergedProcedures,
199
+ webTimeout: this.options.timeout,
199
200
  };
201
+ // Check if there are any enabled tests
202
+ const enabledTests = testFile.tests.filter(t => !t.disabled);
203
+ const hasEnabledTests = enabledTests.length > 0;
204
+ // Add skipped results for disabled tests first
205
+ for (const test of testFile.tests) {
206
+ if (test.disabled) {
207
+ results.push({
208
+ testId: test.id,
209
+ testName: test.name,
210
+ status: 'skipped',
211
+ duration: 0,
212
+ steps: [],
213
+ error: { message: 'Test is disabled' },
214
+ startedAt: new Date().toISOString(),
215
+ finishedAt: new Date().toISOString(),
216
+ });
217
+ }
218
+ }
219
+ // Only run hooks and tests if there are enabled tests
220
+ if (!hasEnabledTests) {
221
+ sharedContext.logger.info('All tests disabled, skipping hooks');
222
+ await this.cleanup();
223
+ return results;
224
+ }
200
225
  let beforeAllFailed = false;
201
226
  try {
202
227
  // Run beforeAll hooks
@@ -211,22 +236,8 @@ class TestExecutor {
211
236
  }
212
237
  // Only run tests if beforeAll succeeded
213
238
  if (!beforeAllFailed) {
214
- // Run each test with beforeEach/afterEach
215
- for (const test of testFile.tests) {
216
- // Skip disabled tests
217
- if (test.disabled) {
218
- results.push({
219
- testId: test.id,
220
- testName: test.name,
221
- status: 'skipped',
222
- duration: 0,
223
- steps: [],
224
- error: { message: 'Test is disabled' },
225
- startedAt: new Date().toISOString(),
226
- finishedAt: new Date().toISOString(),
227
- });
228
- continue;
229
- }
239
+ // Run each enabled test with beforeEach/afterEach
240
+ for (const test of enabledTests) {
230
241
  // Load data from file if specified
231
242
  let testData = test.data;
232
243
  if (test.dataFile && !testData) {
@@ -416,6 +427,7 @@ class TestExecutor {
416
427
  // Enable soft assertions if configured on the test
417
428
  softAssertions: test.softAssertions || false,
418
429
  softAssertionErrors: [],
430
+ webTimeout: this.options.timeout,
419
431
  };
420
432
  // Run beforeTest hooks
421
433
  for (const plugin of this.plugins.values()) {
@@ -507,6 +519,7 @@ class TestExecutor {
507
519
  // Enable soft assertions if configured on the test
508
520
  softAssertions: test.softAssertions || false,
509
521
  softAssertionErrors: [],
522
+ webTimeout: this.options.timeout,
510
523
  };
511
524
  // Inject data values into variables
512
525
  for (const [key, value] of Object.entries(dataSet.values)) {
@@ -720,7 +733,7 @@ class TestExecutor {
720
733
  if (status === 'failed' && isWebStep && context.page) {
721
734
  try {
722
735
  const page = context.page;
723
- const screenshotBuffer = await page.screenshot({ fullPage: false });
736
+ const screenshotBuffer = await page.screenshot({ fullPage: true });
724
737
  screenshot = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
725
738
  }
726
739
  catch (screenshotErr) {
@@ -56,6 +56,10 @@ export declare function getGlobalProcedures(): Record<string, ProcedureDefinitio
56
56
  * Get the configured test ID attribute (defaults to 'data-testid')
57
57
  */
58
58
  export declare function getTestIdAttribute(): string;
59
+ /**
60
+ * Get the configured global timeout in milliseconds (defaults to 30000)
61
+ */
62
+ export declare function getGlobalTimeout(): number;
59
63
  /**
60
64
  * Set the test ID attribute and persist to globals.json
61
65
  */
@@ -47,6 +47,7 @@ exports.getGlobals = getGlobals;
47
47
  exports.getGlobalVariables = getGlobalVariables;
48
48
  exports.getGlobalProcedures = getGlobalProcedures;
49
49
  exports.getTestIdAttribute = getTestIdAttribute;
50
+ exports.getGlobalTimeout = getGlobalTimeout;
50
51
  exports.setTestIdAttribute = setTestIdAttribute;
51
52
  exports.discoverSnippets = discoverSnippets;
52
53
  exports.loadSnippet = loadSnippet;
@@ -118,6 +119,12 @@ function getGlobalProcedures() {
118
119
  function getTestIdAttribute() {
119
120
  return loadedGlobals.testIdAttribute || 'data-testid';
120
121
  }
122
+ /**
123
+ * Get the configured global timeout in milliseconds (defaults to 30000)
124
+ */
125
+ function getGlobalTimeout() {
126
+ return loadedGlobals.timeout || 30000;
127
+ }
121
128
  /**
122
129
  * Set the test ID attribute and persist to globals.json
123
130
  */
@@ -384,7 +384,8 @@ app.post('/api/reports/html', (req, res) => {
384
384
  summary: {
385
385
  totalTests: results.length,
386
386
  passed: results.filter(r => r.status === 'passed').length,
387
- failed: results.filter(r => r.status !== 'passed').length,
387
+ failed: results.filter(r => r.status !== 'passed' && r.status !== 'skipped').length,
388
+ skipped: results.filter(r => r.status === 'skipped').length,
388
389
  duration: results.reduce((sum, r) => sum + r.duration, 0),
389
390
  },
390
391
  testFiles: [{
@@ -420,7 +421,8 @@ app.post('/api/reports/junit', (req, res) => {
420
421
  summary: {
421
422
  totalTests: results.length,
422
423
  passed: results.filter(r => r.status === 'passed').length,
423
- failed: results.filter(r => r.status !== 'passed').length,
424
+ failed: results.filter(r => r.status !== 'passed' && r.status !== 'skipped').length,
425
+ skipped: results.filter(r => r.status === 'skipped').length,
424
426
  duration: results.reduce((sum, r) => sum + r.duration, 0),
425
427
  },
426
428
  testFiles: [{
@@ -244,7 +244,7 @@ async function startServer(options = {}) {
244
244
  const testIdAttr = (0, globals_1.getTestIdAttribute)();
245
245
  const executor = new executor_1.TestExecutor({
246
246
  headless: req.query.headless !== 'false',
247
- timeout: Number(req.query.timeout) || 30000,
247
+ timeout: Number(req.query.timeout) || (0, globals_1.getGlobalTimeout)(),
248
248
  variables: globalVars,
249
249
  procedures: globalProcs,
250
250
  testIdAttribute: testIdAttr,
@@ -294,7 +294,7 @@ async function startServer(options = {}) {
294
294
  const testIdAttr = (0, globals_1.getTestIdAttribute)();
295
295
  const executor = new executor_1.TestExecutor({
296
296
  headless: req.query.headless !== 'false',
297
- timeout: Number(req.query.timeout) || 30000,
297
+ timeout: Number(req.query.timeout) || (0, globals_1.getGlobalTimeout)(),
298
298
  variables: globalVars,
299
299
  procedures: globalProcs,
300
300
  testIdAttribute: testIdAttr,
@@ -429,7 +429,8 @@ async function startServer(options = {}) {
429
429
  summary: {
430
430
  totalTests: results.length,
431
431
  passed: results.filter(r => r.status === 'passed').length,
432
- failed: results.filter(r => r.status !== 'passed').length,
432
+ failed: results.filter(r => r.status !== 'passed' && r.status !== 'skipped').length,
433
+ skipped: results.filter(r => r.status === 'skipped').length,
433
434
  duration: results.reduce((sum, r) => sum + r.duration, 0),
434
435
  },
435
436
  testFiles: [{
@@ -464,7 +465,8 @@ async function startServer(options = {}) {
464
465
  summary: {
465
466
  totalTests: results.length,
466
467
  passed: results.filter(r => r.status === 'passed').length,
467
- failed: results.filter(r => r.status !== 'passed').length,
468
+ failed: results.filter(r => r.status !== 'passed' && r.status !== 'skipped').length,
469
+ skipped: results.filter(r => r.status === 'skipped').length,
468
470
  duration: results.reduce((sum, r) => sum + r.duration, 0),
469
471
  },
470
472
  testFiles: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/testblocks",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",