browsertime 26.3.2 → 27.0.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/browserscripts/timings/softNavigations.js +94 -0
  4. package/lib/chrome/har.js +36 -5
  5. package/lib/chrome/settings/chromeAndroidOptions.js +2 -1
  6. package/lib/chrome/webdriver/chromium.js +40 -1
  7. package/lib/chrome/webdriver/setupChromiumOptions.js +86 -31
  8. package/lib/core/engine/collector.js +340 -317
  9. package/lib/core/engine/command/addText.js +93 -12
  10. package/lib/core/engine/command/click.js +268 -139
  11. package/lib/core/engine/command/commandHelper.js +45 -0
  12. package/lib/core/engine/command/cookie.js +85 -0
  13. package/lib/core/engine/command/element.js +37 -2
  14. package/lib/core/engine/command/javaScript.js +14 -24
  15. package/lib/core/engine/command/measure/screenshots.js +114 -0
  16. package/lib/core/engine/command/measure/video.js +85 -0
  17. package/lib/core/engine/command/measure.js +104 -180
  18. package/lib/core/engine/command/mouse/clickAndHold.js +6 -3
  19. package/lib/core/engine/command/mouse/contextClick.js +56 -4
  20. package/lib/core/engine/command/mouse/doubleClick.js +49 -1
  21. package/lib/core/engine/command/mouse/mouseMove.js +50 -1
  22. package/lib/core/engine/command/mouse/singleClick.js +165 -129
  23. package/lib/core/engine/command/navigation.js +6 -6
  24. package/lib/core/engine/command/select.js +95 -3
  25. package/lib/core/engine/command/selectorParser.js +58 -0
  26. package/lib/core/engine/command/set.js +55 -1
  27. package/lib/core/engine/command/wait.js +41 -2
  28. package/lib/core/engine/commands.js +550 -42
  29. package/lib/core/engine/index.js +206 -150
  30. package/lib/core/engine/iteration.js +277 -137
  31. package/lib/firefox/networkManager.js +22 -30
  32. package/lib/firefox/webdriver/builder.js +14 -0
  33. package/lib/safari/har.js +131 -0
  34. package/lib/safari/native/ios-capture +0 -0
  35. package/lib/safari/native/ios-capture.m +340 -0
  36. package/lib/safari/safariInspectorClient.js +322 -0
  37. package/lib/safari/webdriver/safari.js +303 -13
  38. package/lib/safari/webkitToCdpAdapter.js +241 -0
  39. package/lib/support/cli.js +13 -0
  40. package/lib/support/engineUtils.js +16 -5
  41. package/lib/support/getViewPort.js +5 -0
  42. package/lib/support/har/index.js +41 -3
  43. package/lib/support/usbPower.js +2 -3
  44. package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
  45. package/lib/video/postprocessing/finetune/getFont.js +3 -3
  46. package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
  47. package/lib/video/postprocessing/finetune/index.js +26 -9
  48. package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
  49. package/lib/video/screenRecording/recorder.js +2 -2
  50. package/lib/video/screenRecording/setOrangeBackground.js +3 -0
  51. package/package.json +11 -11
  52. package/types/core/engine/command/addText.d.ts +35 -6
  53. package/types/core/engine/command/addText.d.ts.map +1 -1
  54. package/types/core/engine/command/click.d.ts +87 -16
  55. package/types/core/engine/command/click.d.ts.map +1 -1
  56. package/types/core/engine/command/commandHelper.d.ts +16 -0
  57. package/types/core/engine/command/commandHelper.d.ts.map +1 -0
  58. package/types/core/engine/command/cookie.d.ts +65 -0
  59. package/types/core/engine/command/cookie.d.ts.map +1 -0
  60. package/types/core/engine/command/element.d.ts +19 -1
  61. package/types/core/engine/command/element.d.ts.map +1 -1
  62. package/types/core/engine/command/javaScript.d.ts +4 -0
  63. package/types/core/engine/command/javaScript.d.ts.map +1 -1
  64. package/types/core/engine/command/measure/screenshots.d.ts +15 -0
  65. package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
  66. package/types/core/engine/command/measure/video.d.ts +26 -0
  67. package/types/core/engine/command/measure/video.d.ts.map +1 -0
  68. package/types/core/engine/command/measure.d.ts +59 -33
  69. package/types/core/engine/command/measure.d.ts.map +1 -1
  70. package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
  71. package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
  72. package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
  73. package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
  74. package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
  75. package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
  76. package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
  77. package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
  78. package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
  79. package/types/core/engine/command/navigation.d.ts +3 -3
  80. package/types/core/engine/command/select.d.ts +45 -8
  81. package/types/core/engine/command/select.d.ts.map +1 -1
  82. package/types/core/engine/command/selectorParser.d.ts +20 -0
  83. package/types/core/engine/command/selectorParser.d.ts.map +1 -0
  84. package/types/core/engine/command/set.d.ts +34 -7
  85. package/types/core/engine/command/set.d.ts.map +1 -1
  86. package/types/core/engine/command/wait.d.ts +29 -6
  87. package/types/core/engine/command/wait.d.ts.map +1 -1
  88. package/types/core/engine/commands.d.ts +296 -28
  89. package/types/core/engine/commands.d.ts.map +1 -1
  90. package/types/support/engineUtils.d.ts.map +1 -1
  91. package/types/support/getViewPort.d.ts.map +1 -1
  92. package/types/support/usbPower.d.ts.map +1 -1
  93. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  94. package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
  95. package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
  96. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
  97. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
  98. package/types/video/screenRecording/recorder.d.ts +2 -2
  99. package/types/video/screenRecording/recorder.d.ts.map +1 -1
  100. package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
  101. package/types/video/video.d.ts +1 -1
  102. package/types/video/video.d.ts.map +1 -1
  103. package/visualmetrics/visualmetrics-portable.py +136 -136
  104. package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
  105. package/lib/video/screenRecording/ios/iosRecorder.js +0 -68
@@ -1,23 +1,20 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
+ import webdriver, { By } from 'selenium-webdriver';
3
+ import { executeCommand } from './commandHelper.js';
4
+ import { parseSelector } from './selectorParser.js';
2
5
  const log = getLogger('browsertime.command.click');
3
6
 
4
- function addClick(js) {
5
- const trimmed = js.trim();
6
- let script = `${trimmed}.click();`;
7
- if (trimmed.endsWith(';')) {
8
- script = `${trimmed.slice(0, -1)}.click();`;
9
- }
10
- return script;
11
- }
12
-
13
7
  /**
14
8
  * Provides functionality to perform click actions on elements in a web page using various selectors.
9
+ * Tries the Selenium Actions API first to generate real OS-level mouse events; if the element is
10
+ * not interactable (commonly because the page hides content before clicking — a recommended
11
+ * pattern for visual-metric scripts), falls back to a JavaScript click so the script keeps working.
15
12
  *
16
13
  * @class
17
14
  * @hideconstructor
18
15
  */
19
16
  export class Click {
20
- constructor(browser, pageCompleteCheck) {
17
+ constructor(browser, pageCompleteCheck, options) {
21
18
  /**
22
19
  * @private
23
20
  */
@@ -26,296 +23,428 @@ export class Click {
26
23
  * @private
27
24
  */
28
25
  this.pageCompleteCheck = pageCompleteCheck;
26
+ /**
27
+ * @private
28
+ */
29
+ this.options = options;
30
+ }
31
+
32
+ /**
33
+ * @private
34
+ */
35
+ async _waitForElement(driver, locator) {
36
+ const timeout = this.options?.timeouts?.elementWait ?? 0;
37
+ if (timeout > 0) {
38
+ await driver.wait(webdriver.until.elementLocated(locator), timeout);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @private
44
+ */
45
+ async _andWait(baseMethod, ...args) {
46
+ log.info(
47
+ 'The AndWait methods are deprecated. Use commands.click(selector, { wait: true }) instead.'
48
+ );
49
+ await baseMethod.apply(this, args);
50
+ return this.browser.extraWait(this.pageCompleteCheck);
51
+ }
52
+
53
+ /**
54
+ * @private
55
+ */
56
+ async _clickElement(element) {
57
+ const driver = this.browser.getDriver();
58
+ // The Actions API moves the pointer to the element's bounding-box center
59
+ // and clicks. A `display:none` element has a zero-size box, so the click
60
+ // silently lands at (0,0) instead of throwing — we have to detect that
61
+ // up front and use JS instead. Other interactability problems (overlays,
62
+ // pointer-events:none, etc.) that do throw are caught below.
63
+ const isDisplayed = await element.isDisplayed().catch(() => false);
64
+ if (!isDisplayed) {
65
+ log.info(
66
+ 'Element is not displayed — using a JavaScript click instead of the Actions API.'
67
+ );
68
+ return driver.executeScript('arguments[0].click();', element);
69
+ }
70
+ try {
71
+ await driver.actions({ async: true }).click(element).perform();
72
+ return driver.actions().clear();
73
+ } catch (error) {
74
+ log.info(
75
+ 'Actions API click failed (%s) — falling back to a JavaScript click.',
76
+ error && error.name
77
+ );
78
+ try {
79
+ await driver.actions().clear();
80
+ await driver.executeScript('arguments[0].click();', element);
81
+ } catch (jsError) {
82
+ log.error(
83
+ 'Both the Actions API and the JavaScript click failed: %s',
84
+ (jsError && jsError.message) || jsError
85
+ );
86
+ throw jsError;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Clicks on an element using a unified selector string.
93
+ * Supports CSS selectors (default), and prefix-based strategies:
94
+ * 'id:myId', 'xpath://button', 'text:Submit', 'link:Click here', 'name:email', 'class:btn'.
95
+ *
96
+ * @async
97
+ * @param {string} selector - The selector string. CSS by default, or use a prefix like 'id:', 'xpath:', 'text:', 'link:', 'name:', 'class:'.
98
+ * @param {Object} [options] - Options for the click action.
99
+ * @param {boolean} [options.waitForNavigation=false] - If true, waits for the page complete check after clicking.
100
+ * @returns {Promise<void>} A promise that resolves when the click action is performed.
101
+ * @throws {Error} Throws an error if the element is not found.
102
+ */
103
+ async run(selector, options = {}) {
104
+ const { locator, description } = parseSelector(selector);
105
+ return executeCommand(
106
+ log,
107
+ 'Could not find element by %s',
108
+ description,
109
+ async () => {
110
+ const driver = this.browser.getDriver();
111
+ await this._waitForElement(driver, locator);
112
+ const element = await driver.findElement(locator);
113
+ await this._clickElement(element);
114
+ if (options.waitForNavigation) {
115
+ await this.browser.extraWait(this.pageCompleteCheck);
116
+ }
117
+ },
118
+ this.browser
119
+ );
29
120
  }
30
121
 
31
122
  /**
32
123
  * Clicks on an element identified by its class name.
33
124
  *
34
125
  * @async
126
+ * @private
35
127
  * @param {string} className - The class name of the element to click.
36
128
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
37
129
  * @throws {Error} Throws an error if the element is not found.
38
130
  */
39
131
  async byClassName(className) {
40
- try {
41
- const script = `document.getElementsByClassName('${className}')[0].click();`;
42
- await this.browser.runScript(script, 'CUSTOM');
43
- } catch (error) {
44
- log.error('Could not find element by class name %s', className);
45
- log.verbose(error);
46
- throw new Error('Could not find element by class name ' + className);
47
- }
132
+ return executeCommand(
133
+ log,
134
+ 'Could not find element by class name %s',
135
+ className,
136
+ async () => {
137
+ const driver = this.browser.getDriver();
138
+ const locator = By.className(className);
139
+ await this._waitForElement(driver, locator);
140
+ const element = await driver.findElement(locator);
141
+ return this._clickElement(element);
142
+ },
143
+ this.browser
144
+ );
48
145
  }
49
146
 
50
147
  /**
51
148
  * Clicks on an element identified by its class name and waits for the page complete check to finish.
52
149
  *
53
150
  * @async
151
+ * @private
54
152
  * @param {string} className - The class name of the element to click.
55
153
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
56
154
  * @throws {Error} Throws an error if the element is not found.
57
155
  */
58
156
  async byClassNameAndWait(className) {
59
- try {
60
- const script = `document.getElementsByClassName('${className}')[0].click();`;
61
- await this.browser.runScript(script, 'CUSTOM');
62
- return this.browser.extraWait(this.pageCompleteCheck);
63
- } catch (error) {
64
- log.error('Could not find element by class name %s', className);
65
- log.verbose(error);
66
- throw new Error('Could not find element by class name ' + className);
67
- }
157
+ return this._andWait(this.byClassName, className);
68
158
  }
69
159
 
70
160
  /**
71
161
  * Clicks on a link whose visible text matches the given string.
72
162
  *
73
163
  * @async
164
+ * @private
74
165
  * @param {string} text - The visible text of the link to click.
75
166
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
76
167
  * @throws {Error} Throws an error if the link is not found.
77
168
  */
78
169
  async byLinkText(text) {
79
- try {
80
- const xpath = `//a[text()='${text}']`;
81
- return this.byXpath(xpath);
82
- } catch (error) {
83
- log.error('Could not find link by text %s', text);
84
- log.verbose(error);
85
- throw new Error('Could not find link by text ' + text);
86
- }
170
+ return executeCommand(
171
+ log,
172
+ 'Could not find link by text %s',
173
+ text,
174
+ () => this.byXpath(`//a[text()='${text}']`),
175
+ this.browser
176
+ );
87
177
  }
88
178
 
89
179
  /**
90
180
  * Clicks on a link whose visible text matches the given string and waits for the page complete check to finish.
91
181
  *
92
182
  * @async
183
+ * @private
93
184
  * @param {string} text - The visible text of the link to click.
94
185
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
95
186
  * @throws {Error} Throws an error if the link is not found.
96
187
  */
97
188
  async byLinkTextAndWait(text) {
98
- try {
99
- const xpath = `//a[text()='${text}']`;
100
- return this.byXpathAndWait(xpath);
101
- } catch (error) {
102
- log.error('Could not find link with text %s', text);
103
- log.verbose(error);
104
- throw new Error('Could not find link by text ' + text);
105
- }
189
+ return executeCommand(
190
+ log,
191
+ 'Could not find link by text %s',
192
+ text,
193
+ () => this.byXpathAndWait(`//a[text()='${text}']`),
194
+ this.browser
195
+ );
106
196
  }
107
197
 
108
198
  /**
109
199
  * Clicks on a link whose visible text contains the given substring.
110
200
  *
111
201
  * @async
202
+ * @private
112
203
  * @param {string} text - The substring of the visible text of the link to click.
113
204
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
114
205
  * @throws {Error} Throws an error if the link is not found.
115
206
  */
116
207
  async byPartialLinkText(text) {
117
- try {
118
- const xpath = `//a[contains(text(),'${text}')]`;
119
- return this.byXpath(xpath);
120
- } catch (error) {
121
- log.error('Could not find link by partial text %s', text);
122
- log.verbose(error);
123
- throw new Error('Could not find link by partial text ' + text);
124
- }
208
+ return executeCommand(
209
+ log,
210
+ 'Could not find link by partial text %s',
211
+ text,
212
+ () => this.byXpath(`//a[contains(text(),'${text}')]`),
213
+ this.browser
214
+ );
125
215
  }
126
216
 
127
217
  /**
128
218
  * Clicks on a link whose visible text contains the given substring and waits for the page complete check to finish.
129
219
  *
130
220
  * @async
221
+ * @private
131
222
  * @param {string} text - The substring of the visible text of the link to click.
132
223
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
133
224
  * @throws {Error} Throws an error if the link is not found.
134
225
  */
135
226
  async byPartialLinkTextAndWait(text) {
136
- try {
137
- const xpath = `//a[contains(text(),'${text}')]`;
138
- return this.byXpathAndWait(xpath);
139
- } catch (error) {
140
- log.error('Could not find link by partial text %s', text);
141
- log.verbose(error);
142
- throw new Error('Could not find link by partial text ' + text);
143
- }
227
+ return executeCommand(
228
+ log,
229
+ 'Could not find link by partial text %s',
230
+ text,
231
+ () => this.byXpathAndWait(`//a[contains(text(),'${text}')]`),
232
+ this.browser
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Clicks on an element whose visible text matches the given string.
238
+ * This works on any element type, not just links.
239
+ *
240
+ * @async
241
+ * @private
242
+ * @param {string} text - The visible text of the element to click.
243
+ * @returns {Promise<void>} A promise that resolves when the click action is performed.
244
+ * @throws {Error} Throws an error if the element is not found.
245
+ */
246
+ async byText(text) {
247
+ return executeCommand(
248
+ log,
249
+ 'Could not find element by text %s',
250
+ text,
251
+ () => this.byXpath(`//*[normalize-space(text())='${text}']`),
252
+ this.browser
253
+ );
254
+ }
255
+
256
+ /**
257
+ * Clicks on an element whose visible text matches the given string and waits for the page complete check to finish.
258
+ * This works on any element type, not just links.
259
+ *
260
+ * @async
261
+ * @private
262
+ * @param {string} text - The visible text of the element to click.
263
+ * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
264
+ * @throws {Error} Throws an error if the element is not found.
265
+ */
266
+ async byTextAndWait(text) {
267
+ return executeCommand(
268
+ log,
269
+ 'Could not find element by text %s',
270
+ text,
271
+ () => this.byXpathAndWait(`//*[normalize-space(text())='${text}']`),
272
+ this.browser
273
+ );
144
274
  }
145
275
 
146
276
  /**
147
277
  * Clicks on an element that matches a given XPath selector.
148
278
  *
149
279
  * @async
280
+ * @private
150
281
  * @param {string} xpath - The XPath selector of the element to click.
151
282
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
152
283
  * @throws {Error} Throws an error if the element is not found.
153
284
  */
154
285
  async byXpath(xpath) {
155
- try {
156
- // This is how Selenium do internally
157
- const replaced = xpath.replaceAll('"', "'");
158
- const script = `document.evaluate("${replaced}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click();`;
159
- return this.browser.runScript(script, 'CUSTOM');
160
- } catch (error) {
161
- log.error('Could not find element by xpath %s', xpath);
162
- log.verbose(error);
163
- throw new Error('Could not find element by xpath ' + xpath);
164
- }
286
+ return executeCommand(
287
+ log,
288
+ 'Could not find element by xpath %s',
289
+ xpath,
290
+ async () => {
291
+ const driver = this.browser.getDriver();
292
+ const locator = By.xpath(xpath);
293
+ await this._waitForElement(driver, locator);
294
+ const element = await driver.findElement(locator);
295
+ return this._clickElement(element);
296
+ },
297
+ this.browser
298
+ );
165
299
  }
166
300
  /**
167
301
  * Clicks on an element that matches a given XPath selector and waits for the page complete check to finish.
168
302
  *
169
303
  * @async
304
+ * @private
170
305
  * @param {string} xpath - The XPath selector of the element to click.
171
306
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
172
307
  * @throws {Error} Throws an error if the element is not found.
173
308
  */
174
309
  async byXpathAndWait(xpath) {
175
- try {
176
- // This is how Selenium do internally
177
- const replaced = xpath.replaceAll('"', "'");
178
- const script = `document.evaluate("${replaced}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click();`;
179
- await this.browser.runScript(script, 'CUSTOM');
180
- return this.browser.extraWait(this.pageCompleteCheck);
181
- } catch (error) {
182
- log.error('Could not find element by xpath %s', xpath);
183
- log.verbose(error);
184
- throw new Error('Could not find element by xpath ' + xpath);
185
- }
310
+ return this._andWait(this.byXpath, xpath);
186
311
  }
187
312
 
188
313
  /**
189
314
  * Clicks on an element located by evaluating a JavaScript expression.
190
315
  *
191
316
  * @async
317
+ * @private
192
318
  * @param {string} js - The JavaScript expression that evaluates to an element or list of elements.
193
319
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
194
320
  * @throws {Error} Throws an error if the element is not found.
195
321
  */
196
322
  async byJs(js) {
197
- try {
198
- const script = addClick(js);
199
- await this.browser.runScript(script, 'CUSTOM');
200
- } catch (error) {
201
- log.error('Could not find element by JavaScript %s', js);
202
- log.verbose(error);
203
- throw new Error('Could not find element by JavaScript ' + js);
204
- }
323
+ return executeCommand(
324
+ log,
325
+ 'Could not find element by JavaScript %s',
326
+ js,
327
+ async () => {
328
+ const trimmed = js.trim();
329
+ const script = trimmed.endsWith(';')
330
+ ? `return ${trimmed.slice(0, -1)};`
331
+ : `return ${trimmed};`;
332
+ const element = await this.browser.getDriver().executeScript(script);
333
+ return this._clickElement(element);
334
+ },
335
+ this.browser
336
+ );
205
337
  }
206
338
 
207
339
  /**
208
340
  * Clicks on an element located by evaluating a JavaScript expression and waits for the page complete check to finish.
209
341
  *
210
342
  * @async
343
+ * @private
211
344
  * @param {string} js - The JavaScript expression that evaluates to an element or list of elements.
212
345
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
213
346
  * @throws {Error} Throws an error if the element is not found.
214
347
  */
215
348
  async byJsAndWait(js) {
216
- try {
217
- const script = addClick(js);
218
- await this.browser.runScript(script, 'CUSTOM');
219
- return this.browser.extraWait(this.pageCompleteCheck);
220
- } catch (error) {
221
- log.error('Could not find element by JavaScript %s', js);
222
- log.verbose(error);
223
- throw new Error('Could not find element by JavaScript ' + js);
224
- }
349
+ return this._andWait(this.byJs, js);
225
350
  }
226
351
 
227
352
  /**
228
353
  * Clicks on an element located by its ID.
229
354
  *
230
355
  * @async
356
+ * @private
231
357
  * @param {string} id - The ID of the element to click.
232
358
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
233
359
  * @throws {Error} Throws an error if the element is not found.
234
360
  */
235
361
  async byId(id) {
236
- try {
237
- const script = `document.getElementById('${id}').click();`;
238
- await this.browser.runScript(script, 'CUSTOM');
239
- } catch (error) {
240
- log.error('Could not find element by id %s', id);
241
- log.verbose(error);
242
- throw new Error('Could not find element by id ' + id);
243
- }
362
+ return executeCommand(
363
+ log,
364
+ 'Could not find element by id %s',
365
+ id,
366
+ async () => {
367
+ const driver = this.browser.getDriver();
368
+ const locator = By.id(id);
369
+ await this._waitForElement(driver, locator);
370
+ const element = await driver.findElement(locator);
371
+ return this._clickElement(element);
372
+ },
373
+ this.browser
374
+ );
244
375
  }
245
376
 
246
377
  /**
247
378
  * Clicks on an element located by its name attribute.
248
379
  *
249
380
  * @async
381
+ * @private
250
382
  * @param {string} name - The name attribute of the element to click.
251
383
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
252
384
  * @throws {Error} Throws an error if the element is not found.
253
385
  */
254
386
  async byName(name) {
255
- try {
256
- const script = `document.querySelector("[name='${name}']").click()`;
257
- await this.browser.runScript(script, 'CUSTOM');
258
- } catch (error) {
259
- log.error('Could not find element by name %s', name);
260
- log.verbose(error);
261
- throw new Error('Could not find element by name ' + name);
262
- }
387
+ return executeCommand(
388
+ log,
389
+ 'Could not find element by name %s',
390
+ name,
391
+ async () => {
392
+ const driver = this.browser.getDriver();
393
+ const locator = By.name(name);
394
+ await this._waitForElement(driver, locator);
395
+ const element = await driver.findElement(locator);
396
+ return this._clickElement(element);
397
+ },
398
+ this.browser
399
+ );
263
400
  }
264
401
 
265
402
  /**
266
403
  * Click on link located by the ID attribute. Uses document.getElementById() to find the element. And wait for page complete check to finish.
404
+ * @private
267
405
  * @param {string} id
268
406
  * @returns {Promise<void>} Promise object represents when the element has been clicked and the pageCompleteCheck has finished.
269
407
  * @throws Will throw an error if the element is not found
270
408
  */
271
409
  async byIdAndWait(id) {
272
- try {
273
- const script = `document.getElementById('${id}').click();`;
274
- await this.browser.runScript(script, 'CUSTOM');
275
- return this.browser.extraWait(this.pageCompleteCheck);
276
- } catch (error) {
277
- log.error('Could not find element by id %s', id);
278
- log.verbose(error);
279
- throw new Error('Could not find element by id ' + id);
280
- }
410
+ return this._andWait(this.byId, id);
281
411
  }
282
412
 
283
413
  /**
284
414
  * Clicks on an element located by its CSS selector.
285
415
  *
286
416
  * @async
417
+ * @private
287
418
  * @param {string} selector - The CSS selector of the element to click.
288
419
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
289
420
  * @throws {Error} Throws an error if the element is not found.
290
421
  */
291
422
  async bySelector(selector) {
292
- try {
293
- const script = `document.querySelector('${selector}').click();`;
294
- await this.browser.runScript(script, 'CUSTOM');
295
- } catch (error) {
296
- log.error('Could not click using selector %s', selector);
297
- log.verbose(error);
298
- throw new Error('Could not click using selector ' + selector);
299
- }
423
+ return executeCommand(
424
+ log,
425
+ 'Could not click using selector %s',
426
+ selector,
427
+ async () => {
428
+ const driver = this.browser.getDriver();
429
+ const locator = By.css(selector);
430
+ await this._waitForElement(driver, locator);
431
+ const element = await driver.findElement(locator);
432
+ return this._clickElement(element);
433
+ },
434
+ this.browser
435
+ );
300
436
  }
301
437
 
302
438
  /**
303
439
  * Clicks on an element located by its CSS selector and waits for the page complete check to finish.
304
440
  *
305
441
  * @async
442
+ * @private
306
443
  * @param {string} selector - The CSS selector of the element to click.
307
444
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
308
445
  * @throws {Error} Throws an error if the element is not found.
309
446
  */
310
447
  async bySelectorAndWait(selector) {
311
- try {
312
- const script = `document.querySelector('${selector}').click();`;
313
- await this.browser.runScript(script, 'CUSTOM');
314
- return this.browser.extraWait(this.pageCompleteCheck);
315
- } catch (error) {
316
- log.error('Could not click using selector %s', selector);
317
- log.verbose(error);
318
- throw new Error('Could not click using selector ' + selector);
319
- }
448
+ return this._andWait(this.bySelector, selector);
320
449
  }
321
450
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared error handling wrapper for command methods.
3
+ *
4
+ * Most command methods follow the same try/catch pattern:
5
+ * log.error + log.verbose + throw new Error. This helper
6
+ * eliminates that boilerplate.
7
+ *
8
+ * @param {Object} log - The logger instance.
9
+ * @param {string} errorMessage - printf-style message (with %s placeholder).
10
+ * @param {string|undefined} identifier - Value to interpolate into the message, or undefined if none.
11
+ * @param {Function} fn - The async function to execute.
12
+ * @param {Object} [browser] - Optional browser instance. When provided, the current page URL is appended to the error message.
13
+ * @returns {Promise<*>} The return value of fn.
14
+ */
15
+ export async function executeCommand(
16
+ log,
17
+ errorMessage,
18
+ identifier,
19
+ fn,
20
+ browser
21
+ ) {
22
+ try {
23
+ return await fn();
24
+ } catch (error) {
25
+ let url = '';
26
+ if (browser) {
27
+ try {
28
+ url = ` on ${await browser.getDriver().getCurrentUrl()}`;
29
+ } catch {
30
+ // If we can't get the URL, just skip it
31
+ }
32
+ }
33
+ const baseMessage =
34
+ identifier === undefined
35
+ ? errorMessage
36
+ : errorMessage.replace('%s', String(identifier));
37
+ if (identifier === undefined) {
38
+ log.error(errorMessage);
39
+ } else {
40
+ log.error(errorMessage, identifier);
41
+ }
42
+ log.verbose(error);
43
+ throw new Error(`${baseMessage}${url}`);
44
+ }
45
+ }