browsertime 26.3.1 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +71 -2
  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 +39 -0
  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 +239 -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 +269 -137
  31. package/lib/firefox/networkManager.js +22 -30
  32. package/lib/firefox/webdriver/builder.js +14 -0
  33. package/lib/firefox/webdriver/firefox.js +1 -1
  34. package/lib/safari/har.js +131 -0
  35. package/lib/safari/native/ios-capture +0 -0
  36. package/lib/safari/native/ios-capture.m +340 -0
  37. package/lib/safari/safariInspectorClient.js +322 -0
  38. package/lib/safari/webdriver/safari.js +303 -13
  39. package/lib/safari/webkitToCdpAdapter.js +241 -0
  40. package/lib/support/cli.js +13 -0
  41. package/lib/support/engineUtils.js +16 -5
  42. package/lib/support/getViewPort.js +5 -0
  43. package/lib/support/har/index.js +41 -3
  44. package/lib/support/usbPower.js +2 -3
  45. package/lib/video/postprocessing/finetune/addTextToVideo.js +4 -0
  46. package/lib/video/postprocessing/finetune/getFont.js +3 -3
  47. package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -0
  48. package/lib/video/postprocessing/finetune/index.js +26 -9
  49. package/lib/video/screenRecording/ios/iosDeviceRecorder.js +109 -0
  50. package/lib/video/screenRecording/recorder.js +2 -2
  51. package/lib/video/screenRecording/setOrangeBackground.js +3 -0
  52. package/package.json +12 -12
  53. package/types/core/engine/command/addText.d.ts +35 -6
  54. package/types/core/engine/command/addText.d.ts.map +1 -1
  55. package/types/core/engine/command/click.d.ts +87 -16
  56. package/types/core/engine/command/click.d.ts.map +1 -1
  57. package/types/core/engine/command/commandHelper.d.ts +16 -0
  58. package/types/core/engine/command/commandHelper.d.ts.map +1 -0
  59. package/types/core/engine/command/cookie.d.ts +65 -0
  60. package/types/core/engine/command/cookie.d.ts.map +1 -0
  61. package/types/core/engine/command/element.d.ts +19 -1
  62. package/types/core/engine/command/element.d.ts.map +1 -1
  63. package/types/core/engine/command/javaScript.d.ts +4 -0
  64. package/types/core/engine/command/javaScript.d.ts.map +1 -1
  65. package/types/core/engine/command/measure/screenshots.d.ts +15 -0
  66. package/types/core/engine/command/measure/screenshots.d.ts.map +1 -0
  67. package/types/core/engine/command/measure/video.d.ts +26 -0
  68. package/types/core/engine/command/measure/video.d.ts.map +1 -0
  69. package/types/core/engine/command/measure.d.ts +59 -33
  70. package/types/core/engine/command/measure.d.ts.map +1 -1
  71. package/types/core/engine/command/mouse/clickAndHold.d.ts.map +1 -1
  72. package/types/core/engine/command/mouse/contextClick.d.ts +28 -4
  73. package/types/core/engine/command/mouse/contextClick.d.ts.map +1 -1
  74. package/types/core/engine/command/mouse/doubleClick.d.ts +24 -4
  75. package/types/core/engine/command/mouse/doubleClick.d.ts.map +1 -1
  76. package/types/core/engine/command/mouse/mouseMove.d.ts +30 -5
  77. package/types/core/engine/command/mouse/mouseMove.d.ts.map +1 -1
  78. package/types/core/engine/command/mouse/singleClick.d.ts +50 -13
  79. package/types/core/engine/command/mouse/singleClick.d.ts.map +1 -1
  80. package/types/core/engine/command/navigation.d.ts +3 -3
  81. package/types/core/engine/command/select.d.ts +45 -8
  82. package/types/core/engine/command/select.d.ts.map +1 -1
  83. package/types/core/engine/command/selectorParser.d.ts +20 -0
  84. package/types/core/engine/command/selectorParser.d.ts.map +1 -0
  85. package/types/core/engine/command/set.d.ts +34 -7
  86. package/types/core/engine/command/set.d.ts.map +1 -1
  87. package/types/core/engine/command/wait.d.ts +29 -6
  88. package/types/core/engine/command/wait.d.ts.map +1 -1
  89. package/types/core/engine/commands.d.ts +296 -28
  90. package/types/core/engine/commands.d.ts.map +1 -1
  91. package/types/support/engineUtils.d.ts.map +1 -1
  92. package/types/support/getViewPort.d.ts.map +1 -1
  93. package/types/support/usbPower.d.ts.map +1 -1
  94. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  95. package/types/video/postprocessing/finetune/getTimingMetrics.d.ts.map +1 -1
  96. package/types/video/postprocessing/finetune/index.d.ts.map +1 -1
  97. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts +28 -0
  98. package/types/video/screenRecording/ios/iosDeviceRecorder.d.ts.map +1 -0
  99. package/types/video/screenRecording/recorder.d.ts +2 -2
  100. package/types/video/screenRecording/recorder.d.ts.map +1 -1
  101. package/types/video/screenRecording/setOrangeBackground.d.ts.map +1 -1
  102. package/types/video/video.d.ts +1 -1
  103. package/types/video/video.d.ts.map +1 -1
  104. package/visualmetrics/visualmetrics-portable.py +136 -136
  105. package/lib/video/screenRecording/ios/convertToMp4.js +0 -22
  106. 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
+ * Uses the Selenium Actions API to generate real OS-level mouse events, which means
10
+ * the element must be visible and interactable. If you need to click a hidden element,
11
+ * use {@link JavaScript#run commands.js.run} to trigger a JavaScript click instead.
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,399 @@ 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
+ await driver.actions({ async: true }).click(element).perform();
59
+ return driver.actions().clear();
60
+ }
61
+
62
+ /**
63
+ * Clicks on an element using a unified selector string.
64
+ * Supports CSS selectors (default), and prefix-based strategies:
65
+ * 'id:myId', 'xpath://button', 'text:Submit', 'link:Click here', 'name:email', 'class:btn'.
66
+ *
67
+ * @async
68
+ * @param {string} selector - The selector string. CSS by default, or use a prefix like 'id:', 'xpath:', 'text:', 'link:', 'name:', 'class:'.
69
+ * @param {Object} [options] - Options for the click action.
70
+ * @param {boolean} [options.waitForNavigation=false] - If true, waits for the page complete check after clicking.
71
+ * @returns {Promise<void>} A promise that resolves when the click action is performed.
72
+ * @throws {Error} Throws an error if the element is not found.
73
+ */
74
+ async run(selector, options = {}) {
75
+ const { locator, description } = parseSelector(selector);
76
+ return executeCommand(
77
+ log,
78
+ 'Could not find element by %s',
79
+ description,
80
+ async () => {
81
+ const driver = this.browser.getDriver();
82
+ await this._waitForElement(driver, locator);
83
+ const element = await driver.findElement(locator);
84
+ await this._clickElement(element);
85
+ if (options.waitForNavigation) {
86
+ await this.browser.extraWait(this.pageCompleteCheck);
87
+ }
88
+ },
89
+ this.browser
90
+ );
29
91
  }
30
92
 
31
93
  /**
32
94
  * Clicks on an element identified by its class name.
33
95
  *
34
96
  * @async
97
+ * @private
35
98
  * @param {string} className - The class name of the element to click.
36
99
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
37
100
  * @throws {Error} Throws an error if the element is not found.
38
101
  */
39
102
  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
- }
103
+ return executeCommand(
104
+ log,
105
+ 'Could not find element by class name %s',
106
+ className,
107
+ async () => {
108
+ const driver = this.browser.getDriver();
109
+ const locator = By.className(className);
110
+ await this._waitForElement(driver, locator);
111
+ const element = await driver.findElement(locator);
112
+ return this._clickElement(element);
113
+ },
114
+ this.browser
115
+ );
48
116
  }
49
117
 
50
118
  /**
51
119
  * Clicks on an element identified by its class name and waits for the page complete check to finish.
52
120
  *
53
121
  * @async
122
+ * @private
54
123
  * @param {string} className - The class name of the element to click.
55
124
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
56
125
  * @throws {Error} Throws an error if the element is not found.
57
126
  */
58
127
  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
- }
128
+ return this._andWait(this.byClassName, className);
68
129
  }
69
130
 
70
131
  /**
71
132
  * Clicks on a link whose visible text matches the given string.
72
133
  *
73
134
  * @async
135
+ * @private
74
136
  * @param {string} text - The visible text of the link to click.
75
137
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
76
138
  * @throws {Error} Throws an error if the link is not found.
77
139
  */
78
140
  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
- }
141
+ return executeCommand(
142
+ log,
143
+ 'Could not find link by text %s',
144
+ text,
145
+ () => this.byXpath(`//a[text()='${text}']`),
146
+ this.browser
147
+ );
87
148
  }
88
149
 
89
150
  /**
90
151
  * Clicks on a link whose visible text matches the given string and waits for the page complete check to finish.
91
152
  *
92
153
  * @async
154
+ * @private
93
155
  * @param {string} text - The visible text of the link to click.
94
156
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
95
157
  * @throws {Error} Throws an error if the link is not found.
96
158
  */
97
159
  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
- }
160
+ return executeCommand(
161
+ log,
162
+ 'Could not find link by text %s',
163
+ text,
164
+ () => this.byXpathAndWait(`//a[text()='${text}']`),
165
+ this.browser
166
+ );
106
167
  }
107
168
 
108
169
  /**
109
170
  * Clicks on a link whose visible text contains the given substring.
110
171
  *
111
172
  * @async
173
+ * @private
112
174
  * @param {string} text - The substring of the visible text of the link to click.
113
175
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
114
176
  * @throws {Error} Throws an error if the link is not found.
115
177
  */
116
178
  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
- }
179
+ return executeCommand(
180
+ log,
181
+ 'Could not find link by partial text %s',
182
+ text,
183
+ () => this.byXpath(`//a[contains(text(),'${text}')]`),
184
+ this.browser
185
+ );
125
186
  }
126
187
 
127
188
  /**
128
189
  * Clicks on a link whose visible text contains the given substring and waits for the page complete check to finish.
129
190
  *
130
191
  * @async
192
+ * @private
131
193
  * @param {string} text - The substring of the visible text of the link to click.
132
194
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
133
195
  * @throws {Error} Throws an error if the link is not found.
134
196
  */
135
197
  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
- }
198
+ return executeCommand(
199
+ log,
200
+ 'Could not find link by partial text %s',
201
+ text,
202
+ () => this.byXpathAndWait(`//a[contains(text(),'${text}')]`),
203
+ this.browser
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Clicks on an element whose visible text matches the given string.
209
+ * This works on any element type, not just links.
210
+ *
211
+ * @async
212
+ * @private
213
+ * @param {string} text - The visible text of the element to click.
214
+ * @returns {Promise<void>} A promise that resolves when the click action is performed.
215
+ * @throws {Error} Throws an error if the element is not found.
216
+ */
217
+ async byText(text) {
218
+ return executeCommand(
219
+ log,
220
+ 'Could not find element by text %s',
221
+ text,
222
+ () => this.byXpath(`//*[normalize-space(text())='${text}']`),
223
+ this.browser
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Clicks on an element whose visible text matches the given string and waits for the page complete check to finish.
229
+ * This works on any element type, not just links.
230
+ *
231
+ * @async
232
+ * @private
233
+ * @param {string} text - The visible text of the element to click.
234
+ * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
235
+ * @throws {Error} Throws an error if the element is not found.
236
+ */
237
+ async byTextAndWait(text) {
238
+ return executeCommand(
239
+ log,
240
+ 'Could not find element by text %s',
241
+ text,
242
+ () => this.byXpathAndWait(`//*[normalize-space(text())='${text}']`),
243
+ this.browser
244
+ );
144
245
  }
145
246
 
146
247
  /**
147
248
  * Clicks on an element that matches a given XPath selector.
148
249
  *
149
250
  * @async
251
+ * @private
150
252
  * @param {string} xpath - The XPath selector of the element to click.
151
253
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
152
254
  * @throws {Error} Throws an error if the element is not found.
153
255
  */
154
256
  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
- }
257
+ return executeCommand(
258
+ log,
259
+ 'Could not find element by xpath %s',
260
+ xpath,
261
+ async () => {
262
+ const driver = this.browser.getDriver();
263
+ const locator = By.xpath(xpath);
264
+ await this._waitForElement(driver, locator);
265
+ const element = await driver.findElement(locator);
266
+ return this._clickElement(element);
267
+ },
268
+ this.browser
269
+ );
165
270
  }
166
271
  /**
167
272
  * Clicks on an element that matches a given XPath selector and waits for the page complete check to finish.
168
273
  *
169
274
  * @async
275
+ * @private
170
276
  * @param {string} xpath - The XPath selector of the element to click.
171
277
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
172
278
  * @throws {Error} Throws an error if the element is not found.
173
279
  */
174
280
  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
- }
281
+ return this._andWait(this.byXpath, xpath);
186
282
  }
187
283
 
188
284
  /**
189
285
  * Clicks on an element located by evaluating a JavaScript expression.
190
286
  *
191
287
  * @async
288
+ * @private
192
289
  * @param {string} js - The JavaScript expression that evaluates to an element or list of elements.
193
290
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
194
291
  * @throws {Error} Throws an error if the element is not found.
195
292
  */
196
293
  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
- }
294
+ return executeCommand(
295
+ log,
296
+ 'Could not find element by JavaScript %s',
297
+ js,
298
+ async () => {
299
+ const trimmed = js.trim();
300
+ const script = trimmed.endsWith(';')
301
+ ? `return ${trimmed.slice(0, -1)};`
302
+ : `return ${trimmed};`;
303
+ const element = await this.browser.getDriver().executeScript(script);
304
+ return this._clickElement(element);
305
+ },
306
+ this.browser
307
+ );
205
308
  }
206
309
 
207
310
  /**
208
311
  * Clicks on an element located by evaluating a JavaScript expression and waits for the page complete check to finish.
209
312
  *
210
313
  * @async
314
+ * @private
211
315
  * @param {string} js - The JavaScript expression that evaluates to an element or list of elements.
212
316
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
213
317
  * @throws {Error} Throws an error if the element is not found.
214
318
  */
215
319
  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
- }
320
+ return this._andWait(this.byJs, js);
225
321
  }
226
322
 
227
323
  /**
228
324
  * Clicks on an element located by its ID.
229
325
  *
230
326
  * @async
327
+ * @private
231
328
  * @param {string} id - The ID of the element to click.
232
329
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
233
330
  * @throws {Error} Throws an error if the element is not found.
234
331
  */
235
332
  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
- }
333
+ return executeCommand(
334
+ log,
335
+ 'Could not find element by id %s',
336
+ id,
337
+ async () => {
338
+ const driver = this.browser.getDriver();
339
+ const locator = By.id(id);
340
+ await this._waitForElement(driver, locator);
341
+ const element = await driver.findElement(locator);
342
+ return this._clickElement(element);
343
+ },
344
+ this.browser
345
+ );
244
346
  }
245
347
 
246
348
  /**
247
349
  * Clicks on an element located by its name attribute.
248
350
  *
249
351
  * @async
352
+ * @private
250
353
  * @param {string} name - The name attribute of the element to click.
251
354
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
252
355
  * @throws {Error} Throws an error if the element is not found.
253
356
  */
254
357
  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
- }
358
+ return executeCommand(
359
+ log,
360
+ 'Could not find element by name %s',
361
+ name,
362
+ async () => {
363
+ const driver = this.browser.getDriver();
364
+ const locator = By.name(name);
365
+ await this._waitForElement(driver, locator);
366
+ const element = await driver.findElement(locator);
367
+ return this._clickElement(element);
368
+ },
369
+ this.browser
370
+ );
263
371
  }
264
372
 
265
373
  /**
266
374
  * Click on link located by the ID attribute. Uses document.getElementById() to find the element. And wait for page complete check to finish.
375
+ * @private
267
376
  * @param {string} id
268
377
  * @returns {Promise<void>} Promise object represents when the element has been clicked and the pageCompleteCheck has finished.
269
378
  * @throws Will throw an error if the element is not found
270
379
  */
271
380
  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
- }
381
+ return this._andWait(this.byId, id);
281
382
  }
282
383
 
283
384
  /**
284
385
  * Clicks on an element located by its CSS selector.
285
386
  *
286
387
  * @async
388
+ * @private
287
389
  * @param {string} selector - The CSS selector of the element to click.
288
390
  * @returns {Promise<void>} A promise that resolves when the click action is performed.
289
391
  * @throws {Error} Throws an error if the element is not found.
290
392
  */
291
393
  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
- }
394
+ return executeCommand(
395
+ log,
396
+ 'Could not click using selector %s',
397
+ selector,
398
+ async () => {
399
+ const driver = this.browser.getDriver();
400
+ const locator = By.css(selector);
401
+ await this._waitForElement(driver, locator);
402
+ const element = await driver.findElement(locator);
403
+ return this._clickElement(element);
404
+ },
405
+ this.browser
406
+ );
300
407
  }
301
408
 
302
409
  /**
303
410
  * Clicks on an element located by its CSS selector and waits for the page complete check to finish.
304
411
  *
305
412
  * @async
413
+ * @private
306
414
  * @param {string} selector - The CSS selector of the element to click.
307
415
  * @returns {Promise<void>} A promise that resolves when the click action and page complete check are finished.
308
416
  * @throws {Error} Throws an error if the element is not found.
309
417
  */
310
418
  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
- }
419
+ return this._andWait(this.bySelector, selector);
320
420
  }
321
421
  }
@@ -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
+ }