@testsmith/testblocks 0.8.9 → 0.9.1

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-DGmSW0q0.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-BCKY2YTp.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-C5yUtTzz.css">
21
21
  </head>
22
22
  <body>
@@ -11,7 +11,7 @@ exports.interactionBlocks = [
11
11
  type: 'web_click',
12
12
  category: 'Web',
13
13
  color: '#E91E63',
14
- tooltip: 'Click on an 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
17
  { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
@@ -23,7 +23,13 @@ exports.interactionBlocks = [
23
23
  const selector = (0, utils_1.resolveSelector)(params, context);
24
24
  const timeout = params.TIMEOUT;
25
25
  context.logger.info(`Clicking: ${selector}`);
26
- await page.click(selector, { timeout });
26
+ const locator = page.locator(selector);
27
+ // Wait for element to be visible and stable before clicking
28
+ await locator.waitFor({ state: 'visible', timeout });
29
+ // Scroll into view to ensure element is in viewport
30
+ await locator.scrollIntoViewIfNeeded({ timeout });
31
+ // Click with retry on intercept (handles overlays/animations)
32
+ await locator.click({ timeout });
27
33
  return {
28
34
  _summary: params.SELECTOR,
29
35
  selector,
@@ -35,10 +41,11 @@ exports.interactionBlocks = [
35
41
  type: 'web_fill',
36
42
  category: 'Web',
37
43
  color: '#E91E63',
38
- tooltip: 'Fill an input field (clears existing value)',
44
+ tooltip: 'Fill an input field (clears existing value, auto-waits)',
39
45
  inputs: [
40
46
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
41
47
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
48
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
42
49
  ],
43
50
  previousStatement: true,
44
51
  nextStatement: true,
@@ -47,8 +54,10 @@ exports.interactionBlocks = [
47
54
  const selector = (0, utils_1.resolveSelector)(params, context);
48
55
  const rawValue = params.VALUE;
49
56
  const value = (0, utils_1.resolveVariables)(rawValue, context);
57
+ const timeout = params.TIMEOUT;
50
58
  context.logger.info(`Filling ${selector} with "${value}"`);
51
- await page.fill(selector, value);
59
+ const locator = page.locator(selector);
60
+ await locator.fill(value, { timeout });
52
61
  const displayValue = value.length > 30 ? value.substring(0, 30) + '...' : value;
53
62
  return {
54
63
  _summary: `${params.SELECTOR} = "${displayValue}"`,
@@ -62,11 +71,12 @@ exports.interactionBlocks = [
62
71
  type: 'web_type',
63
72
  category: 'Web',
64
73
  color: '#E91E63',
65
- tooltip: 'Type text character by character',
74
+ tooltip: 'Type text character by character (auto-waits)',
66
75
  inputs: [
67
76
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
68
77
  { name: 'TEXT', type: 'field', fieldType: 'text', required: true },
69
78
  { name: 'DELAY', type: 'field', fieldType: 'number', default: 50 },
79
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
70
80
  ],
71
81
  previousStatement: true,
72
82
  nextStatement: true,
@@ -75,8 +85,10 @@ exports.interactionBlocks = [
75
85
  const selector = (0, utils_1.resolveSelector)(params, context);
76
86
  const text = (0, utils_1.resolveVariables)(params.TEXT, context);
77
87
  const delay = params.DELAY;
88
+ const timeout = params.TIMEOUT;
78
89
  context.logger.info(`Typing "${text}" into ${selector}`);
79
- await page.type(selector, text, { delay });
90
+ const locator = page.locator(selector);
91
+ await locator.pressSequentially(text, { delay, timeout });
80
92
  const displayText = text.length > 30 ? text.substring(0, 30) + '...' : text;
81
93
  return {
82
94
  _summary: `${params.SELECTOR} = "${displayText}"`,
@@ -91,10 +103,11 @@ exports.interactionBlocks = [
91
103
  type: 'web_press_key',
92
104
  category: 'Web',
93
105
  color: '#E91E63',
94
- tooltip: 'Press a keyboard key',
106
+ tooltip: 'Press a keyboard key (auto-waits)',
95
107
  inputs: [
96
108
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
97
109
  { name: 'KEY', type: 'field', fieldType: 'dropdown', options: [['Enter', 'Enter'], ['Tab', 'Tab'], ['Escape', 'Escape'], ['Backspace', 'Backspace'], ['ArrowUp', 'ArrowUp'], ['ArrowDown', 'ArrowDown'], ['ArrowLeft', 'ArrowLeft'], ['ArrowRight', 'ArrowRight']] },
110
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
98
111
  ],
99
112
  previousStatement: true,
100
113
  nextStatement: true,
@@ -102,8 +115,10 @@ exports.interactionBlocks = [
102
115
  const page = context.page;
103
116
  const selector = (0, utils_1.resolveSelector)(params, context);
104
117
  const key = params.KEY;
118
+ const timeout = params.TIMEOUT;
105
119
  context.logger.info(`Pressing ${key} on ${selector}`);
106
- await page.press(selector, key);
120
+ const locator = page.locator(selector);
121
+ await locator.press(key, { timeout });
107
122
  return {
108
123
  _summary: `${key} on ${params.SELECTOR}`,
109
124
  selector,
@@ -116,10 +131,11 @@ exports.interactionBlocks = [
116
131
  type: 'web_select',
117
132
  category: 'Web',
118
133
  color: '#E91E63',
119
- tooltip: 'Select an option from a dropdown',
134
+ tooltip: 'Select an option from a dropdown (auto-waits)',
120
135
  inputs: [
121
136
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
122
137
  { name: 'VALUE', type: 'field', fieldType: 'text', required: true },
138
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
123
139
  ],
124
140
  previousStatement: true,
125
141
  nextStatement: true,
@@ -127,8 +143,10 @@ exports.interactionBlocks = [
127
143
  const page = context.page;
128
144
  const selector = (0, utils_1.resolveSelector)(params, context);
129
145
  const value = (0, utils_1.resolveVariables)(params.VALUE, context);
146
+ const timeout = params.TIMEOUT;
130
147
  context.logger.info(`Selecting "${value}" in ${selector}`);
131
- await page.selectOption(selector, value);
148
+ const locator = page.locator(selector);
149
+ await locator.selectOption(value, { timeout });
132
150
  return {
133
151
  _summary: `${params.SELECTOR} = "${value}"`,
134
152
  selector,
@@ -141,10 +159,11 @@ exports.interactionBlocks = [
141
159
  type: 'web_checkbox',
142
160
  category: 'Web',
143
161
  color: '#E91E63',
144
- tooltip: 'Check or uncheck a checkbox',
162
+ tooltip: 'Check or uncheck a checkbox (auto-waits)',
145
163
  inputs: [
146
164
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
147
165
  { name: 'ACTION', type: 'field', fieldType: 'dropdown', options: [['Check', 'check'], ['Uncheck', 'uncheck']] },
166
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
148
167
  ],
149
168
  previousStatement: true,
150
169
  nextStatement: true,
@@ -152,12 +171,14 @@ exports.interactionBlocks = [
152
171
  const page = context.page;
153
172
  const selector = (0, utils_1.resolveSelector)(params, context);
154
173
  const action = params.ACTION;
174
+ const timeout = params.TIMEOUT;
155
175
  context.logger.info(`${action === 'check' ? 'Checking' : 'Unchecking'} ${selector}`);
176
+ const locator = page.locator(selector);
156
177
  if (action === 'check') {
157
- await page.check(selector);
178
+ await locator.check({ timeout });
158
179
  }
159
180
  else {
160
- await page.uncheck(selector);
181
+ await locator.uncheck({ timeout });
161
182
  }
162
183
  return {
163
184
  _summary: `${action} ${params.SELECTOR}`,
@@ -171,17 +192,20 @@ exports.interactionBlocks = [
171
192
  type: 'web_hover',
172
193
  category: 'Web',
173
194
  color: '#E91E63',
174
- tooltip: 'Hover over an element',
195
+ tooltip: 'Hover over an element (auto-waits)',
175
196
  inputs: [
176
197
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
198
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
177
199
  ],
178
200
  previousStatement: true,
179
201
  nextStatement: true,
180
202
  execute: async (params, context) => {
181
203
  const page = context.page;
182
204
  const selector = (0, utils_1.resolveSelector)(params, context);
205
+ const timeout = params.TIMEOUT;
183
206
  context.logger.info(`Hovering over ${selector}`);
184
- await page.hover(selector);
207
+ const locator = page.locator(selector);
208
+ await locator.hover({ timeout });
185
209
  return {
186
210
  _summary: params.SELECTOR,
187
211
  selector,
@@ -11,15 +11,19 @@ exports.retrievalBlocks = [
11
11
  type: 'web_get_text',
12
12
  category: 'Web',
13
13
  color: '#2196F3',
14
- tooltip: 'Get text content of an element',
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 },
17
18
  ],
18
19
  output: { type: 'String' },
19
20
  execute: async (params, context) => {
20
21
  const page = context.page;
21
22
  const selector = (0, utils_1.resolveSelector)(params, context);
22
- const text = await page.textContent(selector);
23
+ const timeout = params.TIMEOUT;
24
+ const locator = page.locator(selector);
25
+ await locator.waitFor({ state: 'visible', timeout });
26
+ const text = await locator.textContent({ timeout });
23
27
  context.logger.debug(`Text content of ${selector}: "${text}"`);
24
28
  const displayText = text && text.length > 40 ? text.substring(0, 40) + '...' : text;
25
29
  return {
@@ -35,17 +39,21 @@ exports.retrievalBlocks = [
35
39
  type: 'web_get_attribute',
36
40
  category: 'Web',
37
41
  color: '#2196F3',
38
- tooltip: 'Get attribute value of an element',
42
+ tooltip: 'Get attribute value of an element (auto-waits)',
39
43
  inputs: [
40
44
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
41
45
  { name: 'ATTRIBUTE', type: 'field', fieldType: 'text', required: true },
46
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
42
47
  ],
43
48
  output: { type: 'String' },
44
49
  execute: async (params, context) => {
45
50
  const page = context.page;
46
51
  const selector = (0, utils_1.resolveSelector)(params, context);
47
52
  const attribute = params.ATTRIBUTE;
48
- const value = await page.getAttribute(selector, attribute);
53
+ const timeout = params.TIMEOUT;
54
+ const locator = page.locator(selector);
55
+ await locator.waitFor({ state: 'attached', timeout });
56
+ const value = await locator.getAttribute(attribute, { timeout });
49
57
  context.logger.debug(`Attribute ${attribute} of ${selector}: "${value}"`);
50
58
  return {
51
59
  _summary: `${attribute} = "${value}"`,
@@ -61,15 +69,19 @@ exports.retrievalBlocks = [
61
69
  type: 'web_get_input_value',
62
70
  category: 'Web',
63
71
  color: '#2196F3',
64
- tooltip: 'Get current value of an input field',
72
+ tooltip: 'Get current value of an input field (auto-waits)',
65
73
  inputs: [
66
74
  { name: 'SELECTOR', type: 'field', fieldType: 'text', required: true },
75
+ { name: 'TIMEOUT', type: 'field', fieldType: 'number', default: 30000 },
67
76
  ],
68
77
  output: { type: 'String' },
69
78
  execute: async (params, context) => {
70
79
  const page = context.page;
71
80
  const selector = (0, utils_1.resolveSelector)(params, context);
72
- const value = await page.inputValue(selector);
81
+ const timeout = params.TIMEOUT;
82
+ const locator = page.locator(selector);
83
+ await locator.waitFor({ state: 'visible', timeout });
84
+ const value = await locator.inputValue({ timeout });
73
85
  context.logger.debug(`Input value of ${selector}: "${value}"`);
74
86
  const displayValue = value.length > 40 ? value.substring(0, 40) + '...' : value;
75
87
  return {
@@ -4,24 +4,57 @@
4
4
  export interface PlaywrightLocator {
5
5
  click(options?: {
6
6
  timeout?: number;
7
+ force?: boolean;
8
+ }): Promise<void>;
9
+ fill(value: string, options?: {
10
+ timeout?: number;
7
11
  }): Promise<void>;
8
- fill(value: string): Promise<void>;
9
12
  type(text: string, options?: {
10
13
  delay?: number;
14
+ timeout?: number;
15
+ }): Promise<void>;
16
+ pressSequentially(text: string, options?: {
17
+ delay?: number;
18
+ timeout?: number;
19
+ }): Promise<void>;
20
+ selectOption(values: string | string[], options?: {
21
+ timeout?: number;
22
+ }): Promise<string[]>;
23
+ check(options?: {
24
+ timeout?: number;
25
+ }): Promise<void>;
26
+ uncheck(options?: {
27
+ timeout?: number;
28
+ }): Promise<void>;
29
+ hover(options?: {
30
+ timeout?: number;
31
+ }): Promise<void>;
32
+ focus(options?: {
33
+ timeout?: number;
11
34
  }): Promise<void>;
12
- selectOption(values: string | string[]): Promise<string[]>;
13
- check(): Promise<void>;
14
- uncheck(): Promise<void>;
15
- hover(): Promise<void>;
16
- focus(): Promise<void>;
17
- press(key: string): Promise<void>;
18
- textContent(): Promise<string | null>;
19
- getAttribute(name: string): Promise<string | null>;
20
- inputValue(): Promise<string>;
35
+ press(key: string, options?: {
36
+ timeout?: number;
37
+ }): Promise<void>;
38
+ textContent(options?: {
39
+ timeout?: number;
40
+ }): Promise<string | null>;
41
+ getAttribute(name: string, options?: {
42
+ timeout?: number;
43
+ }): Promise<string | null>;
44
+ inputValue(options?: {
45
+ timeout?: number;
46
+ }): Promise<string>;
21
47
  isVisible(): Promise<boolean>;
22
48
  isEnabled(): Promise<boolean>;
23
49
  isChecked(): Promise<boolean>;
24
50
  count(): Promise<number>;
51
+ waitFor(options?: {
52
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
53
+ timeout?: number;
54
+ }): Promise<void>;
55
+ scrollIntoViewIfNeeded(options?: {
56
+ timeout?: number;
57
+ }): Promise<void>;
25
58
  }
26
59
  export interface PlaywrightPage {
27
60
  goto(url: string, options?: {
@@ -197,6 +197,30 @@ class TestExecutor {
197
197
  testIdAttribute: this.options.testIdAttribute,
198
198
  procedures: mergedProcedures,
199
199
  };
200
+ // Check if there are any enabled tests
201
+ const enabledTests = testFile.tests.filter(t => !t.disabled);
202
+ const hasEnabledTests = enabledTests.length > 0;
203
+ // Add skipped results for disabled tests first
204
+ for (const test of testFile.tests) {
205
+ if (test.disabled) {
206
+ results.push({
207
+ testId: test.id,
208
+ testName: test.name,
209
+ status: 'skipped',
210
+ duration: 0,
211
+ steps: [],
212
+ error: { message: 'Test is disabled' },
213
+ startedAt: new Date().toISOString(),
214
+ finishedAt: new Date().toISOString(),
215
+ });
216
+ }
217
+ }
218
+ // Only run hooks and tests if there are enabled tests
219
+ if (!hasEnabledTests) {
220
+ sharedContext.logger.info('All tests disabled, skipping hooks');
221
+ await this.cleanup();
222
+ return results;
223
+ }
200
224
  let beforeAllFailed = false;
201
225
  try {
202
226
  // Run beforeAll hooks
@@ -211,22 +235,8 @@ class TestExecutor {
211
235
  }
212
236
  // Only run tests if beforeAll succeeded
213
237
  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
- }
238
+ // Run each enabled test with beforeEach/afterEach
239
+ for (const test of enabledTests) {
230
240
  // Load data from file if specified
231
241
  let testData = test.data;
232
242
  if (test.dataFile && !testData) {
@@ -720,7 +730,7 @@ class TestExecutor {
720
730
  if (status === 'failed' && isWebStep && context.page) {
721
731
  try {
722
732
  const page = context.page;
723
- const screenshotBuffer = await page.screenshot({ fullPage: false });
733
+ const screenshotBuffer = await page.screenshot({ fullPage: true });
724
734
  screenshot = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
725
735
  }
726
736
  catch (screenshotErr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testsmith/testblocks",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
4
4
  "description": "Visual test automation tool with Blockly - API and Playwright testing",
5
5
  "author": "Roy de Kleijn",
6
6
  "license": "MIT",