Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ I.click('Delete', '.toolbar') // with context
|---|---|---|---|
| [click](/web-api#iclick) | [forceClick](/web-api#iforcecclick) | [doubleClick](/web-api#idoubleclick) | [rightClick](/web-api#irightclick) |
| [forceRightClick](/web-api#iforcerightclick) | [moveCursorTo](/web-api#imovecursorto) | [dragAndDrop](/web-api#idraganddrop) | [dragSlider](/web-api#idragslider) |
| [setSliderValue](/web-api#isetslidervalue) | | | |

Use **forceClick** when standard click fails (e.g., hidden elements, animations). Use **rightClick** for context menus, **doubleClick** for multi-select.

Expand Down Expand Up @@ -241,6 +242,22 @@ I.uncheckOption('Subscribe')

> [selectOption](/web-api#iselectoption) works with native `<select>` elements as well as custom components using `role="combobox"` or `role="listbox"`.

#### Sliders

```js
I.setSliderValue('Volume', 60) // absolute value
I.dragSlider('Volume', 40) // relative, by pixels
```

[setSliderValue](/web-api#isetslidervalue) sets a slider to an absolute value with the keyboard, so it works with `<input type="range">` and with custom widgets built on `role="slider"` alike — including thumbs that are too small to drag. [dragSlider](/web-api#idragslider) moves the scrubber by pixels and falls back to the keyboard when the element has no size.

Both accept a human-readable name. A component library must expose one: React libraries that render the thumb as a `<span role="slider">` give it no accessible name unless the application adds `aria-label` to the thumb, since a `<label for>` on the slider root points at a `<span>`, which is not labelable.

```js
// <Slider.Thumb aria-label="Brightness" />
I.setSliderValue('Brightness', 40)
```

### Assertions

CodeceptJS provides **built-in browser assertions** instead of generic `expect()` calls. This keeps tests readable and produces clear failure messages without extra assertion libraries.
Expand Down
6 changes: 4 additions & 2 deletions docs/webapi/dragSlider.mustache
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
Drag the scrubber of a slider to a given position
For fuzzy locators, fields are matched by label text, the "name" attribute, CSS, and XPath.
Drag the scrubber of a slider to a given position.
For fuzzy locators, sliders are matched by label text, `aria-label`, the "name" attribute, CSS, and XPath.
When the slider has no size to drag along, `offsetX` is applied as that many keyboard steps instead.

```js
I.dragSlider('#slider', 30);
I.dragSlider('#slider', -70);
I.dragSlider('Volume', 30);
```

@param {CodeceptJS.LocatorOrString} locator located by label|name|CSS|XPath|strict locator.
Expand Down
4 changes: 3 additions & 1 deletion docs/webapi/moveCursorTo.mustache
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
Moves cursor to element matched by locator.
Extra shift can be set with offsetX and offsetY options.
For fuzzy locators, elements are matched the same way as by `click` - by text, `aria-label`, `title`, CSS, and XPath.

An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent.
When the second argument is a non-number (string or locator object), it is treated as context.

```js
I.moveCursorTo('.tooltip');
I.moveCursorTo('Show details');
I.moveCursorTo('#submit', 5,5);
I.moveCursorTo('#submit', '.container');
```

@param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
@param {CodeceptJS.LocatorOrString} locator located by text|CSS|XPath|strict locator.
@param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator.
@param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
@returns {void} automatically synchronized promise through #recorder
18 changes: 18 additions & 0 deletions docs/webapi/setSliderValue.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Sets a slider to an absolute value.

The value is reached with the keyboard (`Home`/`End` plus arrow keys), so it also works for
sliders that have no size to drag along and for widgets that are not form fields, like
`<span role="slider">`. The range and the increment are read from `aria-valuemin`,
`aria-valuemax` and `step`, falling back to the `min` / `max` attributes of a native
`<input type="range">`. For fuzzy locators, sliders are matched by label text, `aria-label`,
the "name" attribute, CSS, and XPath.

```js
I.setSliderValue('Volume', 60);
I.setSliderValue('#slider', 0);
I.setSliderValue({ role: 'slider', name: 'Brightness' }, 40);
```

@param {CodeceptJS.LocatorOrString} locator located by label|name|CSS|XPath|strict locator.
@param {number} value value to set the slider to.
@returns {void} automatically synchronized promise through #recorder
76 changes: 68 additions & 8 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { findByPlaywrightLocator } from './extras/PlaywrightLocator.js'
import { dropFile } from './scripts/dropFile.js'
import WebElement from '../element/WebElement.js'
import { selectElement } from './extras/elementSelection.js'
import { readSliderState, setSliderToValue, nudgeSliderByKeyboard, isSliderWithoutBox } from './extras/slider.js'
import { fillRichEditor } from './extras/richTextEditor.js'

let playwright
Expand Down Expand Up @@ -1505,18 +1506,19 @@ class Playwright extends Helper {
offsetX = 0
}

let el
let matcher
if (context) {
const contextEls = await this._locate(context)
assertElementExists(contextEls, context, 'Context element')
el = await findElements.call(this, contextEls[0], locator)
assertElementExists(el, locator)
el = el[0]
matcher = contextEls[0]
} else {
el = await this._locateElement(locator)
assertElementExists(el, locator)
matcher = await this._getContext()
}

const els = await findClickable.call(this, matcher, locator)
assertElementExists(els, locator)
const el = selectElement(els, locator, this)

// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
const { x, y } = await clickablePoint(el)
await this.page.mouse.move(x + offsetX, y + offsetY)
Expand Down Expand Up @@ -2871,8 +2873,16 @@ class Playwright extends Helper {
*
*/
async dragSlider(locator, offsetX = 0) {
const src = await this._locateElement(locator)
assertElementExists(src, locator, 'Slider Element')
const els = await findSlider.call(this, await this._getContext(), locator)
assertElementExists(els, locator, 'Slider Element')
const src = selectElement(els, locator, this)
const actions = sliderActions.call(this, src)

if (isSliderWithoutBox(await actions.state())) {
this.debugSection('Slider', `${new Locator(locator)} has no size to drag along, moving it by ${offsetX} steps instead`)
await nudgeSliderByKeyboard(actions, offsetX)
return this._waitForAction()
}

// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
const sliderSource = await clickablePoint(src)
Expand All @@ -2888,6 +2898,19 @@ class Playwright extends Helper {
return this._waitForAction()
}

/**
* {{> setSliderValue }}
*
*/
async setSliderValue(locator, value) {
const els = await findSlider.call(this, await this._getContext(), locator)
assertElementExists(els, locator, 'Slider Element')
const el = selectElement(els, locator, this)

await setSliderToValue(sliderActions.call(this, el), new Locator(locator), value)
return this._waitForAction()
}

/**
* {{> grabAttributeFrom }}
*
Expand Down Expand Up @@ -4443,6 +4466,43 @@ async function findFields(locator, context = null) {
return locateFn({ css: locator })
}

function sliderActions(el) {
return {
state: () => el.evaluate(readSliderState),
focus: () => el.focus(),
press: key => this.page.keyboard.press(key),
}
}

async function findSlider(matcher, locator) {
const matchedLocator = new Locator(locator)
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)

let els
try {
els = await matcher.getByRole('slider', { name: matchedLocator.value, exact: true }).all()
if (els.length) return els
} catch (err) {
// getByRole not supported or failed
}

try {
els = await findFields.call(this, matchedLocator.value)
if (els.length) return els
} catch (err) {
// not a field
}

try {
els = await matcher.getByRole('slider', { name: matchedLocator.value }).all()
if (els.length) return els
} catch (err) {
// getByRole not supported or failed
}

return findElements.call(this, matcher, matchedLocator.value)
}

async function proceedSelect(context, el, option) {
const role = await el.getAttribute('role')
const options = Array.isArray(option) ? option : [option]
Expand Down
74 changes: 60 additions & 14 deletions lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElem
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
import WebElement from '../element/WebElement.js'
import { selectElement } from './extras/elementSelection.js'
import { readSliderState, setSliderToValue, nudgeSliderByKeyboard, isSliderWithoutBox } from './extras/slider.js'
import { fillRichEditor } from './extras/richTextEditor.js'

let puppeteer
Expand Down Expand Up @@ -829,21 +830,18 @@ class Puppeteer extends Helper {
offsetX = 0
}

let el
let matcher = await this.context
if (context) {
const contextEls = await findElements.call(this, this.page, context)
assertElementExists(contextEls, context, 'Context element')
const els = await findElements.call(this, contextEls[0], locator)
if (!els || els.length === 0) {
throw new ElementNotFound(locator, 'Element to move cursor to')
}
el = els[0]
} else {
el = await this._locateElement(locator)
if (!el) {
throw new ElementNotFound(locator, 'Element to move cursor to')
}
matcher = contextEls[0]
}

const els = await findClickable.call(this, matcher, locator)
if (!els || els.length === 0) {
throw new ElementNotFound(locator, 'Element to move cursor to')
}
const el = selectElement(els, locator, this)

// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
const { x, y } = await getClickablePoint(el)
Expand Down Expand Up @@ -2156,11 +2154,20 @@ class Puppeteer extends Helper {
* {{> dragSlider }}
*/
async dragSlider(locator, offsetX = 0) {
const src = await this._locate(locator)
assertElementExists(src, locator, 'Slider Element')
const els = await findSlider.call(this, await this.context, locator)
assertElementExists(els, locator, 'Slider Element')
const src = selectElement(els, locator, this)
const actions = sliderActions.call(this, src)

if (isSliderWithoutBox(await actions.state())) {
this.debugSection('Slider', `${new Locator(locator)} has no size to drag along, moving it by ${offsetX} steps instead`)
await nudgeSliderByKeyboard(actions, offsetX)
await this._waitForAction()
return
}

// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
const sliderSource = await getClickablePoint(src[0])
const sliderSource = await getClickablePoint(src)

// Drag start point
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 })
Expand All @@ -2173,6 +2180,18 @@ class Puppeteer extends Helper {
await this._waitForAction()
}

/**
* {{> setSliderValue }}
*/
async setSliderValue(locator, value) {
const els = await findSlider.call(this, await this.context, locator)
assertElementExists(els, locator, 'Slider Element')
const el = selectElement(els, locator, this)

await setSliderToValue(sliderActions.call(this, el), new Locator(locator), value)
await this._waitForAction()
}

/**
* {{> grabAttributeFromAll }}
*/
Expand Down Expand Up @@ -3148,6 +3167,33 @@ async function findClickable(matcher, locator) {
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
}

function sliderActions(el) {
return {
state: () => el.evaluate(readSliderState),
focus: () => el.focus(),
press: key => this.page.keyboard.press(key),
}
}

async function findSlider(matcher, locator) {
const matchedLocator = new Locator(locator)
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)

const literal = xpathLocator.literal(matchedLocator.value)

let els = await findElements.call(this, matcher, { xpath: Locator.slider.byLabel(literal) })
if (els.length) return els

try {
els = await findFields.call(this, matchedLocator.value)
if (els.length) return els
} catch (err) {
// not a field
}

return findElements.call(this, matcher, matchedLocator.value)
}

async function proceedSee(assertType, text, context, strict = false) {
let description
let allText
Expand Down
Loading