codeceptjs 4.0.0-rc.7 → 4.0.0-rc.8

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.
@@ -1,12 +1,16 @@
1
1
  Moves cursor to element matched by locator.
2
2
  Extra shift can be set with offsetX and offsetY options.
3
3
 
4
+ An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent.
5
+ When the second argument is a non-number (string or locator object), it is treated as context.
6
+
4
7
  ```js
5
8
  I.moveCursorTo('.tooltip');
6
9
  I.moveCursorTo('#submit', 5,5);
10
+ I.moveCursorTo('#submit', '.container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
10
- @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
14
+ @param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator.
11
15
  @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
12
16
  @returns {void} automatically synchronized promise through #recorder
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import { simplifyHtmlElement } from '../html.js'
2
3
 
3
4
  /**
4
5
  * Unified WebElement class that wraps native element instances from different helpers
@@ -306,6 +307,57 @@ class WebElement {
306
307
  * @returns {string} Normalized CSS selector
307
308
  * @private
308
309
  */
310
+ async toAbsoluteXPath() {
311
+ const xpathFn = (el) => {
312
+ const parts = []
313
+ let current = el
314
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
315
+ let index = 0
316
+ let sibling = current.previousSibling
317
+ while (sibling) {
318
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
319
+ index++
320
+ }
321
+ sibling = sibling.previousSibling
322
+ }
323
+ const tagName = current.tagName.toLowerCase()
324
+ const pathIndex = index > 0 ? `[${index + 1}]` : ''
325
+ parts.unshift(`${tagName}${pathIndex}`)
326
+ current = current.parentElement
327
+ }
328
+ return '/' + parts.join('/')
329
+ }
330
+
331
+ switch (this.helperType) {
332
+ case 'playwright':
333
+ return this.element.evaluate(xpathFn)
334
+ case 'puppeteer':
335
+ return this.element.evaluate(xpathFn)
336
+ case 'webdriver':
337
+ return this.helper.browser.execute(xpathFn, this.element)
338
+ default:
339
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
340
+ }
341
+ }
342
+
343
+ async toOuterHTML() {
344
+ switch (this.helperType) {
345
+ case 'playwright':
346
+ return this.element.evaluate(el => el.outerHTML)
347
+ case 'puppeteer':
348
+ return this.element.evaluate(el => el.outerHTML)
349
+ case 'webdriver':
350
+ return this.helper.browser.execute(el => el.outerHTML, this.element)
351
+ default:
352
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
353
+ }
354
+ }
355
+
356
+ async toSimplifiedHTML(maxLength = 300) {
357
+ const outerHTML = await this.toOuterHTML()
358
+ return simplifyHtmlElement(outerHTML, maxLength)
359
+ }
360
+
309
361
  _normalizeLocator(locator) {
310
362
  if (typeof locator === 'string') {
311
363
  return locator
@@ -36,6 +36,7 @@ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefu
36
36
  import Popup from './extras/Popup.js'
37
37
  import Console from './extras/Console.js'
38
38
  import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
+ import { dropFile } from './scripts/dropFile.js'
39
40
  import WebElement from '../element/WebElement.js'
40
41
 
41
42
  let playwright
@@ -1494,8 +1495,23 @@ class Playwright extends Helper {
1494
1495
  *
1495
1496
  */
1496
1497
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
1497
- const el = await this._locateElement(locator)
1498
- assertElementExists(el, locator)
1498
+ let context = null
1499
+ if (typeof offsetX !== 'number') {
1500
+ context = offsetX
1501
+ offsetX = 0
1502
+ }
1503
+
1504
+ let el
1505
+ if (context) {
1506
+ const contextEls = await this._locate(context)
1507
+ assertElementExists(contextEls, context, 'Context element')
1508
+ el = await findElements.call(this, contextEls[0], locator)
1509
+ assertElementExists(el, locator)
1510
+ el = el[0]
1511
+ } else {
1512
+ el = await this._locateElement(locator)
1513
+ assertElementExists(el, locator)
1514
+ }
1499
1515
 
1500
1516
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
1501
1517
  const { x, y } = await clickablePoint(el)
@@ -1763,7 +1779,7 @@ class Playwright extends Helper {
1763
1779
  if (elements.length === 0) {
1764
1780
  throw new ElementNotFound(locator, 'Element', 'was not found')
1765
1781
  }
1766
- if (this.options.strict) assertOnlyOneElement(elements, locator)
1782
+ if (this.options.strict) assertOnlyOneElement(elements, locator, this)
1767
1783
  return elements[0]
1768
1784
  }
1769
1785
 
@@ -1779,7 +1795,7 @@ class Playwright extends Helper {
1779
1795
  const context = providedContext || (await this._getContext())
1780
1796
  const els = await findCheckable.call(this, locator, context)
1781
1797
  assertElementExists(els[0], locator, 'Checkbox or radio')
1782
- if (this.options.strict) assertOnlyOneElement(els, locator)
1798
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
1783
1799
  return els[0]
1784
1800
  }
1785
1801
 
@@ -2266,7 +2282,7 @@ class Playwright extends Helper {
2266
2282
  async fillField(field, value, context = null) {
2267
2283
  const els = await findFields.call(this, field, context)
2268
2284
  assertElementExists(els, field, 'Field')
2269
- if (this.options.strict) assertOnlyOneElement(els, field)
2285
+ if (this.options.strict) assertOnlyOneElement(els, field, this)
2270
2286
  const el = els[0]
2271
2287
 
2272
2288
  await el.clear()
@@ -2285,7 +2301,7 @@ class Playwright extends Helper {
2285
2301
  async clearField(locator, context = null) {
2286
2302
  const els = await findFields.call(this, locator, context)
2287
2303
  assertElementExists(els, locator, 'Field to clear')
2288
- if (this.options.strict) assertOnlyOneElement(els, locator)
2304
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
2289
2305
 
2290
2306
  const el = els[0]
2291
2307
 
@@ -2302,7 +2318,7 @@ class Playwright extends Helper {
2302
2318
  async appendField(field, value, context = null) {
2303
2319
  const els = await findFields.call(this, field, context)
2304
2320
  assertElementExists(els, field, 'Field')
2305
- if (this.options.strict) assertOnlyOneElement(els, field)
2321
+ if (this.options.strict) assertOnlyOneElement(els, field, this)
2306
2322
  await highlightActiveElement.call(this, els[0])
2307
2323
  await els[0].press('End')
2308
2324
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
@@ -2347,20 +2363,12 @@ class Playwright extends Helper {
2347
2363
 
2348
2364
  const targetEls = els.length ? els : await this._locate(locator)
2349
2365
  assertElementExists(targetEls, locator, 'Element')
2350
- const base64Content = base64EncodeFile(file)
2351
- const fileName = path.basename(file)
2352
- const mimeType = getMimeType(fileName)
2353
- await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
2354
- const binaryStr = atob(base64Content)
2355
- const bytes = new Uint8Array(binaryStr.length)
2356
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
2357
- const fileObj = new File([bytes], fileName, { type: mimeType })
2358
- const dataTransfer = new DataTransfer()
2359
- dataTransfer.items.add(fileObj)
2360
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
2361
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
2362
- el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
2363
- }, { base64Content, fileName, mimeType })
2366
+ const fileData = {
2367
+ base64Content: base64EncodeFile(file),
2368
+ fileName: path.basename(file),
2369
+ mimeType: getMimeType(path.basename(file)),
2370
+ }
2371
+ await targetEls[0].evaluate(dropFile, fileData)
2364
2372
  return this._waitForAction()
2365
2373
  }
2366
2374
 
@@ -4300,7 +4308,7 @@ async function findClickable(matcher, locator) {
4300
4308
 
4301
4309
  if (!matchedLocator.isFuzzy()) {
4302
4310
  const els = await findElements.call(this, matcher, matchedLocator)
4303
- if (this.options.strict) assertOnlyOneElement(els, locator)
4311
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4304
4312
  return els
4305
4313
  }
4306
4314
 
@@ -4310,7 +4318,7 @@ async function findClickable(matcher, locator) {
4310
4318
  try {
4311
4319
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4312
4320
  if (els.length) {
4313
- if (this.options.strict) assertOnlyOneElement(els, locator)
4321
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4314
4322
  return els
4315
4323
  }
4316
4324
  } catch (err) {
@@ -4320,7 +4328,7 @@ async function findClickable(matcher, locator) {
4320
4328
  try {
4321
4329
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4322
4330
  if (els.length) {
4323
- if (this.options.strict) assertOnlyOneElement(els, locator)
4331
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4324
4332
  return els
4325
4333
  }
4326
4334
  } catch (err) {
@@ -4329,20 +4337,20 @@ async function findClickable(matcher, locator) {
4329
4337
 
4330
4338
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4331
4339
  if (els.length) {
4332
- if (this.options.strict) assertOnlyOneElement(els, locator)
4340
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4333
4341
  return els
4334
4342
  }
4335
4343
 
4336
4344
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4337
4345
  if (els.length) {
4338
- if (this.options.strict) assertOnlyOneElement(els, locator)
4346
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4339
4347
  return els
4340
4348
  }
4341
4349
 
4342
4350
  try {
4343
4351
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4344
4352
  if (els.length) {
4345
- if (this.options.strict) assertOnlyOneElement(els, locator)
4353
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
4346
4354
  return els
4347
4355
  }
4348
4356
  } catch (err) {
@@ -4619,9 +4627,10 @@ function assertElementExists(res, locator, prefix, suffix) {
4619
4627
  }
4620
4628
  }
4621
4629
 
4622
- function assertOnlyOneElement(elements, locator) {
4630
+ function assertOnlyOneElement(elements, locator, helper) {
4623
4631
  if (elements.length > 1) {
4624
- throw new MultipleElementsFound(locator, elements)
4632
+ const webElements = elements.map(el => new WebElement(el, helper))
4633
+ throw new MultipleElementsFound(locator, webElements)
4625
4634
  }
4626
4635
  }
4627
4636
 
@@ -33,11 +33,13 @@ import {
33
33
  } from '../utils.js'
34
34
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
35
35
  import ElementNotFound from './errors/ElementNotFound.js'
36
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
36
37
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
37
38
  import Popup from './extras/Popup.js'
38
39
  import Console from './extras/Console.js'
39
40
  import { highlightElement } from './scripts/highlightElement.js'
40
41
  import { blurElement } from './scripts/blurElement.js'
42
+ import { dropFile } from './scripts/dropFile.js'
41
43
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
42
44
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
43
45
  import WebElement from '../element/WebElement.js'
@@ -270,6 +272,7 @@ class Puppeteer extends Helper {
270
272
  show: false,
271
273
  defaultPopupAction: 'accept',
272
274
  highlightElement: false,
275
+ strict: false,
273
276
  }
274
277
 
275
278
  return Object.assign(defaults, config)
@@ -818,9 +821,26 @@ class Puppeteer extends Helper {
818
821
  * {{ react }}
819
822
  */
820
823
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
821
- const el = await this._locateElement(locator)
822
- if (!el) {
823
- throw new ElementNotFound(locator, 'Element to move cursor to')
824
+ let context = null
825
+ if (typeof offsetX !== 'number') {
826
+ context = offsetX
827
+ offsetX = 0
828
+ }
829
+
830
+ let el
831
+ if (context) {
832
+ const contextEls = await findElements.call(this, this.page, context)
833
+ assertElementExists(contextEls, context, 'Context element')
834
+ const els = await findElements.call(this, contextEls[0], locator)
835
+ if (!els || els.length === 0) {
836
+ throw new ElementNotFound(locator, 'Element to move cursor to')
837
+ }
838
+ el = els[0]
839
+ } else {
840
+ el = await this._locateElement(locator)
841
+ if (!el) {
842
+ throw new ElementNotFound(locator, 'Element to move cursor to')
843
+ }
824
844
  }
825
845
 
826
846
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -988,6 +1008,14 @@ class Puppeteer extends Helper {
988
1008
  */
989
1009
  async _locateElement(locator) {
990
1010
  const context = await this.context
1011
+ if (this.options.strict) {
1012
+ const elements = await findElements.call(this, context, locator)
1013
+ if (elements.length === 0) {
1014
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1015
+ }
1016
+ assertOnlyOneElement(elements, locator, this)
1017
+ return elements[0]
1018
+ }
991
1019
  return findElement.call(this, context, locator)
992
1020
  }
993
1021
 
@@ -1005,6 +1033,7 @@ class Puppeteer extends Helper {
1005
1033
  if (!els || els.length === 0) {
1006
1034
  throw new ElementNotFound(locator, 'Checkbox or radio')
1007
1035
  }
1036
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
1008
1037
  return els[0]
1009
1038
  }
1010
1039
 
@@ -1564,6 +1593,7 @@ class Puppeteer extends Helper {
1564
1593
  async fillField(field, value, context = null) {
1565
1594
  const els = await findVisibleFields.call(this, field, context)
1566
1595
  assertElementExists(els, field, 'Field')
1596
+ if (this.options.strict) assertOnlyOneElement(els, field, this)
1567
1597
  const el = els[0]
1568
1598
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1569
1599
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
@@ -1594,6 +1624,7 @@ class Puppeteer extends Helper {
1594
1624
  async appendField(field, value, context = null) {
1595
1625
  const els = await findVisibleFields.call(this, field, context)
1596
1626
  assertElementExists(els, field, 'Field')
1627
+ if (this.options.strict) assertOnlyOneElement(els, field, this)
1597
1628
  highlightActiveElement.call(this, els[0], await this._getContext())
1598
1629
  await els[0].press('End')
1599
1630
  await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
@@ -1639,20 +1670,12 @@ class Puppeteer extends Helper {
1639
1670
 
1640
1671
  const targetEls = els.length ? els : await this._locate(locator)
1641
1672
  assertElementExists(targetEls, locator, 'Element')
1642
- const base64Content = base64EncodeFile(file)
1643
- const fileName = path.basename(file)
1644
- const mimeType = getMimeType(fileName)
1645
- await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
1646
- const binaryStr = atob(base64Content)
1647
- const bytes = new Uint8Array(binaryStr.length)
1648
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
1649
- const fileObj = new File([bytes], fileName, { type: mimeType })
1650
- const dataTransfer = new DataTransfer()
1651
- dataTransfer.items.add(fileObj)
1652
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
1653
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
1654
- el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
1655
- }, { base64Content, fileName, mimeType })
1673
+ const fileData = {
1674
+ base64Content: base64EncodeFile(file),
1675
+ fileName: path.basename(file),
1676
+ mimeType: getMimeType(path.basename(file)),
1677
+ }
1678
+ await targetEls[0].evaluate(dropFile, fileData)
1656
1679
  return this._waitForAction()
1657
1680
  }
1658
1681
 
@@ -3098,6 +3121,7 @@ async function proceedClick(locator, context = null, options = {}) {
3098
3121
  } else {
3099
3122
  assertElementExists(els, locator, 'Clickable element')
3100
3123
  }
3124
+ if (this.options.strict) assertOnlyOneElement(els, locator, this)
3101
3125
 
3102
3126
  highlightActiveElement.call(this, els[0], await this._getContext())
3103
3127
 
@@ -3425,6 +3449,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3425
3449
  }
3426
3450
  }
3427
3451
 
3452
+ function assertOnlyOneElement(elements, locator, helper) {
3453
+ if (elements.length > 1) {
3454
+ const webElements = elements.map(el => new WebElement(el, helper))
3455
+ throw new MultipleElementsFound(locator, webElements)
3456
+ }
3457
+ }
3458
+
3428
3459
  function $XPath(element, selector) {
3429
3460
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3430
3461
  const res = []
@@ -30,12 +30,14 @@ import {
30
30
  } from '../utils.js'
31
31
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
32
32
  import ElementNotFound from './errors/ElementNotFound.js'
33
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
33
34
  import ConnectionRefused from './errors/ConnectionRefused.js'
34
35
  import Locator from '../locator.js'
35
36
  import { highlightElement } from './scripts/highlightElement.js'
36
37
  import { focusElement } from './scripts/focusElement.js'
37
38
  import { blurElement } from './scripts/blurElement.js'
38
39
  import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
40
+ import { dropFile } from './scripts/dropFile.js'
39
41
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
40
42
  import WebElement from '../element/WebElement.js'
41
43
 
@@ -503,6 +505,7 @@ class WebDriver extends Helper {
503
505
  keepBrowserState: false,
504
506
  deprecationWarnings: false,
505
507
  highlightElement: false,
508
+ strict: false,
506
509
  }
507
510
 
508
511
  // override defaults with config
@@ -1090,6 +1093,7 @@ class WebDriver extends Helper {
1090
1093
  } else {
1091
1094
  assertElementExists(res, locator, 'Clickable element')
1092
1095
  }
1096
+ if (this.options.strict) assertOnlyOneElement(res, locator, this)
1093
1097
  const elem = usingFirstElement(res)
1094
1098
  highlightActiveElement.call(this, elem)
1095
1099
  return this.browser[clickMethod](getElementId(elem))
@@ -1109,6 +1113,7 @@ class WebDriver extends Helper {
1109
1113
  } else {
1110
1114
  assertElementExists(res, locator, 'Clickable element')
1111
1115
  }
1116
+ if (this.options.strict) assertOnlyOneElement(res, locator, this)
1112
1117
  const elem = usingFirstElement(res)
1113
1118
  highlightActiveElement.call(this, elem)
1114
1119
 
@@ -1136,6 +1141,7 @@ class WebDriver extends Helper {
1136
1141
  } else {
1137
1142
  assertElementExists(res, locator, 'Clickable element')
1138
1143
  }
1144
+ if (this.options.strict) assertOnlyOneElement(res, locator, this)
1139
1145
 
1140
1146
  const elem = usingFirstElement(res)
1141
1147
  highlightActiveElement.call(this, elem)
@@ -1156,6 +1162,7 @@ class WebDriver extends Helper {
1156
1162
  } else {
1157
1163
  assertElementExists(res, locator, 'Clickable element')
1158
1164
  }
1165
+ if (this.options.strict) assertOnlyOneElement(res, locator, this)
1159
1166
 
1160
1167
  const el = usingFirstElement(res)
1161
1168
 
@@ -1272,6 +1279,7 @@ class WebDriver extends Helper {
1272
1279
  async fillField(field, value, context = null) {
1273
1280
  const res = await findFields.call(this, field, context)
1274
1281
  assertElementExists(res, field, 'Field')
1282
+ if (this.options.strict) assertOnlyOneElement(res, field, this)
1275
1283
  const elem = usingFirstElement(res)
1276
1284
  highlightActiveElement.call(this, elem)
1277
1285
  try {
@@ -1295,6 +1303,7 @@ class WebDriver extends Helper {
1295
1303
  async appendField(field, value, context = null) {
1296
1304
  const res = await findFields.call(this, field, context)
1297
1305
  assertElementExists(res, field, 'Field')
1306
+ if (this.options.strict) assertOnlyOneElement(res, field, this)
1298
1307
  const elem = usingFirstElement(res)
1299
1308
  highlightActiveElement.call(this, elem)
1300
1309
  return elem.addValue(value.toString())
@@ -1307,6 +1316,7 @@ class WebDriver extends Helper {
1307
1316
  async clearField(field, context = null) {
1308
1317
  const res = await findFields.call(this, field, context)
1309
1318
  assertElementExists(res, field, 'Field')
1319
+ if (this.options.strict) assertOnlyOneElement(res, field, this)
1310
1320
  const elem = usingFirstElement(res)
1311
1321
  highlightActiveElement.call(this, elem)
1312
1322
  return elem.clearValue(getElementId(elem))
@@ -1376,20 +1386,12 @@ class WebDriver extends Helper {
1376
1386
  const targetRes = res.length ? res : await this._locate(locator)
1377
1387
  assertElementExists(targetRes, locator, 'Element')
1378
1388
  const targetEl = usingFirstElement(targetRes)
1379
- const base64Content = base64EncodeFile(file)
1380
- const fileName = path.basename(file)
1381
- const mimeType = getMimeType(fileName)
1382
- return this.browser.execute(function (el, data) {
1383
- var binaryStr = atob(data.base64Content)
1384
- var bytes = new Uint8Array(binaryStr.length)
1385
- for (var i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
1386
- var fileObj = new File([bytes], data.fileName, { type: data.mimeType })
1387
- var dataTransfer = new DataTransfer()
1388
- dataTransfer.items.add(fileObj)
1389
- el.dispatchEvent(new DragEvent('dragenter', { dataTransfer: dataTransfer, bubbles: true }))
1390
- el.dispatchEvent(new DragEvent('dragover', { dataTransfer: dataTransfer, bubbles: true }))
1391
- el.dispatchEvent(new DragEvent('drop', { dataTransfer: dataTransfer, bubbles: true }))
1392
- }, targetEl, { base64Content, fileName, mimeType })
1389
+ const fileData = {
1390
+ base64Content: base64EncodeFile(file),
1391
+ fileName: path.basename(file),
1392
+ mimeType: getMimeType(path.basename(file)),
1393
+ }
1394
+ return this.browser.execute(dropFile, targetEl, fileData)
1393
1395
  }
1394
1396
 
1395
1397
  /**
@@ -1974,8 +1976,22 @@ class WebDriver extends Helper {
1974
1976
  * {{> moveCursorTo }}
1975
1977
  */
1976
1978
  async moveCursorTo(locator, xOffset, yOffset) {
1977
- const res = await this._locate(withStrictLocator(locator), true)
1978
- assertElementExists(res, locator)
1979
+ let context = null
1980
+ if (typeof xOffset !== 'number' && xOffset !== undefined) {
1981
+ context = xOffset
1982
+ xOffset = undefined
1983
+ }
1984
+
1985
+ let res
1986
+ if (context) {
1987
+ const contextRes = await this._locate(withStrictLocator(context), true)
1988
+ assertElementExists(contextRes, context, 'Context element')
1989
+ res = await contextRes[0].$$(withStrictLocator(locator))
1990
+ assertElementExists(res, locator)
1991
+ } else {
1992
+ res = await this._locate(withStrictLocator(locator), true)
1993
+ assertElementExists(res, locator)
1994
+ }
1979
1995
  const elem = usingFirstElement(res)
1980
1996
  try {
1981
1997
  await elem.moveTo({ xOffset, yOffset })
@@ -3289,6 +3305,13 @@ function usingFirstElement(els) {
3289
3305
  return els[0]
3290
3306
  }
3291
3307
 
3308
+ function assertOnlyOneElement(elements, locator, helper) {
3309
+ if (elements.length > 1) {
3310
+ const webElements = Array.from(elements).map(el => new WebElement(el, helper))
3311
+ throw new MultipleElementsFound(locator, webElements)
3312
+ }
3313
+ }
3314
+
3292
3315
  function getElementId(el) {
3293
3316
  // W3C WebDriver web element identifier
3294
3317
  // https://w3c.github.io/webdriver/#dfn-web-element-identifier
@@ -1,40 +1,45 @@
1
1
  import Locator from '../../locator.js'
2
2
 
3
- /**
4
- * Error thrown when strict mode is enabled and multiple elements are found
5
- * for a single-element locator operation (click, fillField, etc.)
6
- */
7
3
  class MultipleElementsFound extends Error {
8
- /**
9
- * @param {Locator|string|object} locator - The locator used
10
- * @param {Array<HTMLElement>} elements - Array of Playwright element handles found
11
- */
12
- constructor(locator, elements) {
13
- super(`Multiple elements (${elements.length}) found for "${locator}". Call fetchDetails() for full information.`)
4
+ constructor(locator, webElements) {
5
+ const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
6
+ ? new Locator(locator).toString()
7
+ : String(locator)
8
+ super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
14
9
  this.name = 'MultipleElementsFound'
15
10
  this.locator = locator
16
- this.elements = elements
17
- this.count = elements.length
11
+ this.webElements = webElements
12
+ this.count = webElements.length
18
13
  this._detailsFetched = false
19
14
  }
20
15
 
21
- /**
22
- * Fetch detailed information about the found elements asynchronously
23
- * This updates the error message with XPath and element previews
24
- */
25
16
  async fetchDetails() {
26
17
  if (this._detailsFetched) return
27
18
 
28
19
  try {
29
- if (typeof this.locator === 'object' && !(this.locator instanceof Locator)) {
30
- this.locator = JSON.stringify(this.locator)
20
+ const items = []
21
+ const maxToShow = Math.min(this.count, 10)
22
+
23
+ for (let i = 0; i < maxToShow; i++) {
24
+ const webEl = this.webElements[i]
25
+ try {
26
+ const xpath = await webEl.toAbsoluteXPath()
27
+ const html = await webEl.toSimplifiedHTML()
28
+ items.push(` ${i + 1}. > ${xpath}\n ${html}`)
29
+ } catch (err) {
30
+ items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
31
+ }
31
32
  }
32
33
 
33
- const locatorObj = new Locator(this.locator)
34
- const elementList = await this._generateElementList(this.elements, this.count)
34
+ if (this.count > 10) {
35
+ items.push(` ... and ${this.count - 10} more`)
36
+ }
35
37
 
36
- this.message = `Multiple elements (${this.count}) found for "${locatorObj.toString()}" in strict mode.\n` +
37
- elementList +
38
+ const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
39
+ ? new Locator(this.locator).toString()
40
+ : String(this.locator)
41
+ this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
42
+ items.join('\n') +
38
43
  `\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
39
44
  } catch (err) {
40
45
  this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
@@ -42,94 +47,6 @@ class MultipleElementsFound extends Error {
42
47
 
43
48
  this._detailsFetched = true
44
49
  }
45
-
46
- /**
47
- * Generate a formatted list of found elements with their XPath and preview
48
- * @param {Array<HTMLElement>} elements
49
- * @param {number} count
50
- * @returns {Promise<string>}
51
- */
52
- async _generateElementList(elements, count) {
53
- const items = []
54
- const maxToShow = Math.min(count, 10)
55
-
56
- for (let i = 0; i < maxToShow; i++) {
57
- const el = elements[i]
58
- try {
59
- const info = await this._getElementInfo(el)
60
- items.push(` ${i + 1}. ${info.xpath} (${info.preview})`)
61
- } catch (err) {
62
- // Element might be detached or inaccessible
63
- items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
64
- }
65
- }
66
-
67
- if (count > 10) {
68
- items.push(` ... and ${count - 10} more`)
69
- }
70
-
71
- return items.join('\n')
72
- }
73
-
74
- /**
75
- * Get XPath and preview for an element by running JavaScript in browser context
76
- * @param {HTMLElement} element
77
- * @returns {Promise<{xpath: string, preview: string}>}
78
- */
79
- async _getElementInfo(element) {
80
- return element.evaluate((el) => {
81
- // Generate a unique XPath for this element
82
- const getUniqueXPath = (element) => {
83
- if (element.id) {
84
- return `//*[@id="${element.id}"]`
85
- }
86
-
87
- const parts = []
88
- let current = element
89
-
90
- while (current && current.nodeType === Node.ELEMENT_NODE) {
91
- let index = 0
92
- let sibling = current.previousSibling
93
-
94
- while (sibling) {
95
- if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
96
- index++
97
- }
98
- sibling = sibling.previousSibling
99
- }
100
-
101
- const tagName = current.tagName.toLowerCase()
102
- const pathIndex = index > 0 ? `[${index + 1}]` : ''
103
- parts.unshift(`${tagName}${pathIndex}`)
104
-
105
- current = current.parentElement
106
-
107
- // Stop at body to keep XPath reasonable
108
- if (current && current.tagName === 'BODY') {
109
- parts.unshift('body')
110
- break
111
- }
112
- }
113
-
114
- return '/' + parts.join('/')
115
- }
116
-
117
- // Get a preview of the element (tag, classes, id)
118
- const getPreview = (element) => {
119
- const tag = element.tagName.toLowerCase()
120
- const id = element.id ? `#${element.id}` : ''
121
- const classes = element.className
122
- ? '.' + element.className.split(' ').filter(c => c).join('.')
123
- : ''
124
- return `${tag}${id}${classes || ''}`
125
- }
126
-
127
- return {
128
- xpath: getUniqueXPath(el),
129
- preview: getPreview(el),
130
- }
131
- })
132
- }
133
50
  }
134
51
 
135
52
  export default MultipleElementsFound
@@ -0,0 +1,11 @@
1
+ export const dropFile = (el, { base64Content, fileName, mimeType }) => {
2
+ const binaryStr = atob(base64Content)
3
+ const bytes = new Uint8Array(binaryStr.length)
4
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
5
+ const fileObj = new File([bytes], fileName, { type: mimeType })
6
+ const dataTransfer = new DataTransfer()
7
+ dataTransfer.items.add(fileObj)
8
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
9
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
10
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
11
+ }
package/lib/html.js CHANGED
@@ -245,4 +245,17 @@ function splitByChunks(text, chunkSize) {
245
245
  return chunks.map(chunk => chunk.trim())
246
246
  }
247
247
 
248
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml }
248
+ function simplifyHtmlElement(html, maxLength = 300) {
249
+ try {
250
+ html = removeNonInteractiveElements(html)
251
+ html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
252
+ } catch (e) {
253
+ // keep raw html if minification fails
254
+ }
255
+ if (html.length > maxLength) {
256
+ html = html.slice(0, maxLength) + '...'
257
+ }
258
+ return html
259
+ }
260
+
261
+ export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement }
package/lib/mocha/cli.js CHANGED
@@ -202,6 +202,16 @@ class Cli extends Base {
202
202
 
203
203
  // failures
204
204
  if (stats.failures) {
205
+ for (const test of this.failures) {
206
+ if (test.err && typeof test.err.fetchDetails === 'function') {
207
+ try {
208
+ await test.err.fetchDetails()
209
+ } catch (e) {
210
+ // ignore fetch errors
211
+ }
212
+ }
213
+ }
214
+
205
215
  // append step traces
206
216
  this.failures = this.failures.map(test => {
207
217
  // we will change the stack trace, so we need to clone the test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-rc.7",
3
+ "version": "4.0.0-rc.8",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [