Skip to content

Commit

Permalink
[Web Components] Add Shadow DOM Support to CodeceptJS (codeceptjs#2276)
Browse files Browse the repository at this point in the history
* add shadow dom support to codeceptjs plugin WebdriverIO

* Update lib/helper/WebDriver.js

Co-authored-by: Kushang Gajjar <[email protected]>
Co-authored-by: Michael Bodnarchuk <[email protected]>
  • Loading branch information
3 people authored Mar 28, 2020
1 parent d775461 commit e1eaaa6
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .circleci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ docker-compose run --rm test-rest &&
docker-compose run --rm test-acceptance.webdriverio &&
docker-compose run --rm test-acceptance.nightmare &&
docker-compose run --rm test-acceptance.puppeteer &&
docker-compose run --rm test-acceptance.protractor &&
docker-compose run --rm test-acceptance.protractor
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ Thanks all to those who are and will have contributing to this awesome project!
<a href="https://github.com/lennym"><img src="https://avatars3.githubusercontent.com/u/117398?v=4" title="lennym" width="80" height="80"></a><a href="https://github.com/petehouston"><img src="https://avatars0.githubusercontent.com/u/9006720?v=4" title="petehouston" width="80" height="80"></a><a href="https://github.com/Holorium"><img src="https://avatars1.githubusercontent.com/u/10815542?v=4" title="Holorium" width="80" height="80"></a><a href="https://github.com/johnyb"><img src="https://avatars2.githubusercontent.com/u/86358?v=4" title="johnyb" width="80" height="80"></a><a href="https://github.com/jamesgeorge007"><img src="https://avatars2.githubusercontent.com/u/25279263?v=4" title="jamesgeorge007" width="80" height="80"></a><a href="https://github.com/jinjorge"><img src="https://avatars3.githubusercontent.com/u/2208083?v=4" title="jinjorge" width="80" height="80"></a>
<a href="https://github.com/galkin"><img src="https://avatars3.githubusercontent.com/u/5930544?v=4" title="galkin" width="80" height="80"></a><a href="https://github.com/radhey1851"><img src="https://avatars2.githubusercontent.com/u/22446528?v=4" title="radhey1851" width="80" height="80"></a>
<a href="https://github.com/nitschSB"><img src="https://avatars0.githubusercontent.com/u/39341455?v=4" title="nitschSB" width="80" height="80"></a><a href="https://github.com/abner"><img src="https://avatars1.githubusercontent.com/u/42773?v=4" title="abner" width="80" height="80"></a><a href="https://github.com/Akxe"><img src="https://avatars3.githubusercontent.com/u/2001798?v=4" title="Akxe" width="80" height="80"></a><a href="https://github.com/Kalostrinho"><img src="https://avatars0.githubusercontent.com/u/19229249?v=4" title="Kalostrinho" width="80" height="80"></a><a href="https://github.com/asselin"><img src="https://avatars2.githubusercontent.com/u/911250?v=4" title="asselin" width="80" height="80"></a><a href="https://github.com/xt1"><img src="https://avatars2.githubusercontent.com/u/3820037?v=4" title="xt1" width="80" height="80"></a>
<a href="https://github.com/gkushang"><img src="https://avatars0.githubusercontent.com/u/3663389?s=460&u=0f7dc8baaf29dc15fb2ec51398530c2e6f506f54&v=4" title="KushangGajjar" width="80" height="80"></a>

[//]: contributor-faces

Expand Down
10 changes: 6 additions & 4 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,21 @@ This repository demonstrates usage of:
* testing WYSIWYG editor
* GitLab CI

## [CodeceptJS Cucumber E2E Framework](https://github.com/gkushang/codeceptjs-e2e)
## [CodeceptJS Cucumber BDD E2E Framework](https://github.com/gkushang/codeceptjs-bdd)

This repository contains complete E2E framework for CodeceptJS with Cucumber and SauceLabs Integration
This repository contains complete BDD E2E framework for CodeceptJS with Cucumber and SauceLabs Integration

* CodecepJS-Cucumber E2E Framework
* CodeceptJS-Cucumber BDD Framework. Link to [docs](https://gkushang.github.io/)
* [CLI](https://gkushang.github.io/1-getting-started/setup-framework/) to quick start
* Saucelabs Integration
* Run Cross Browser tests in Parallel on SauceLabs with a simple command
* Run tests on `chrome:headless`
* Page Objects
* `Should.js` Assertion Library
* Soft Assertions
* Uses `wdio` service (selenium-standalone, sauce)
* Allure HTML Reports
* Uses shared Master configuration
* Uses shared configuration
* Sample example and feature files of GitHub Features

## [Amazon Tests v2](https://gitlab.com/thanhnguyendh/codeceptjs-wdio-services)
Expand Down
66 changes: 66 additions & 0 deletions docs/shadow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
permalink: /shadow
title: Locating Shadow Dom Elements
---

# Locating Shadow Dom Elements

Shadow DOM is one of the key browser features that make up web components. Web components are a really great way to build reusable elements, and are able to scale all the way up to complete web applications. Style encapsulation, the feature that gives shadow DOM it's power, has been a bit of a pain when it comes to E2E or UI testing. Things just got a little easier though, as CodeceptJS introduced built-in support for shadow DOM via locators of type `shadow`. Let's dig into what they're all about.

Generated HTML code may often look like this (ref: [Salesforce's Lighting Web Components](https://github.com/salesforce/lwc)):

```js
<body>
<my-app>
<recipe-hello>
<button>Click Me!</button>
</recipe-hello>
<recipe-hello-binding>
<ui-input>
<input type="text" class="input">
</ui-input>
</recipe-hello-binding>
</my-app>
</body>
```

This uses custom elements, `my-app`, `recipe-hello`, `recipe-hello-binding` and `ui-input`. It's quite common that clickable elements are not actual `a` or `button` elements but custom elements. This way `I.click('Click Me!');` won't work, as well as `fillField('.input', 'value)`. Finding a correct locator for such cases turns to be almost impossible until `shadow` element support is added to CodeceptJS.

## Locate Shadow Dom

For Web Components or [Salesforce's Lighting Web Components](https://github.com/salesforce/lwc) with Shadow DOM's, a special `shadow` locator is available. It allows to select an element by its shadow dom sequences and sequences are defined as an Array of `elements`. Elements defined in the array of `elements` must be in the ordered the shadow elements appear in the DOM.

```js
{ shadow: ['my-app', 'recipe-hello', 'button'] }
{ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }
```

In WebDriver, you can use shadow locators in any method where locator is required.

For example, to fill value in `input` field or to click the `Click Me!` button, in above HTML code:

```js
I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');
I.click({ shadow: ['my-app', 'recipe-hello', 'button'] });
```

## Example

```js
Feature('Shadow Dom Locators');

Scenario('should fill input field within shadow elements', I => {

// navigate to LWC webpage containing shadow dom
I.amOnPage('https://recipes.lwc.dev/');

// click Click Me! button
I.click({ shadow: ['my-app', 'recipe-hello', 'button'] });

// fill the input field
I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');

});


```
75 changes: 72 additions & 3 deletions lib/helper/WebDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
const ConnectionRefused = require('./errors/ConnectionRefused');
const Locator = require('../locator');

const SHADOW = 'shadow';
const webRoot = 'body';

/**
Expand Down Expand Up @@ -627,6 +628,62 @@ class WebDriver extends Helper {
this.$$ = this.browser.$$.bind(this.browser);
}

/**
* Check if locator is type of "Shadow"
*
* @param {object} locator
*/
_isShadowLocator(locator) {
return locator.type === SHADOW || locator[SHADOW];
}

/**
* Locate Element within the Shadow Dom
*
* @param {object} locator
*/
async _locateShadow(locator) {
const shadow = locator.value ? locator.value : locator[SHADOW];
const shadowSequence = [];
let elements;

if (!Array.isArray(shadow)) {
throw new Error(`Shadow '${shadow}' should be defined as an Array of elements.`);
}

// traverse through the Shadow locators in sequence
for (let index = 0; index < shadow.length; index++) {
const shadowElement = shadow[index];
shadowSequence.push(shadowElement);

if (!elements) {
elements = await (this.browser.$$(shadowElement));
} else if (Array.isArray(elements)) {
elements = await elements[0].shadow$$(shadowElement);
} else if (elements) {
elements = await elements.shadow$$(shadowElement);
}

if (!elements || !elements[0]) {
throw new Error(`Shadow Element '${shadowElement}' is not found. It is possible the element is incorrect or elements sequence is incorrect. Please verify the sequence '${shadowSequence.join('>')}' is correctly chained.`);
}
}

this.debugSection('Elements', `Found ${elements.length} '${SHADOW}' elements`);

return elements;
}

/**
* Smart Wait to locate an element
*
* @param {object} locator
*/
async _smartWait(locator) {
this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);
await this.defineTimeout({ implicit: this.options.smartWait });
}

/**
* Get elements by different locator types, including strict locator.
* Should be used in custom helpers:
Expand All @@ -641,6 +698,18 @@ class WebDriver extends Helper {
async _locate(locator, smartWait = false) {
if (require('../store').debugMode) smartWait = false;

// special locator type for Shadow DOM
if (this._isShadowLocator(locator)) {
if (!this.options.smartWait || !smartWait) {
const els = await this._locateShadow(locator);
return els;
}


const els = await this._locateShadow(locator);
return els;
}

// special locator type for React
if (locator.react) {
const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined);
Expand All @@ -652,9 +721,9 @@ class WebDriver extends Helper {
const els = await this.$$(withStrictLocator(locator));
return els;
}
this.debugSection(`SmartWait (${this.options.smartWait}ms)`, `Locating ${locator} in ${this.options.smartWait}`);

await this.defineTimeout({ implicit: this.options.smartWait });
await this._smartWait(locator);

const els = await this.$$(withStrictLocator(locator));
await this.defineTimeout({ implicit: 0 });
return els;
Expand Down Expand Up @@ -2301,7 +2370,6 @@ async function filterAsync(array, callback) {
return values;
}


async function findClickable(locator, locateFn) {
locator = new Locator(locator);
if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator, true);
Expand All @@ -2325,6 +2393,7 @@ async function findClickable(locator, locateFn) {

async function findFields(locator) {
locator = new Locator(locator);

if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true);
if (!locator.isFuzzy()) return this._locate(locator, true);

Expand Down
10 changes: 10 additions & 0 deletions test/acceptance/gherkin/shadow_dom.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@WebDriverIO @bdd
Feature: Web Components Shadow Dom Elements

In order to achieve my goals
As a persona
I want to be able to interact with a shadow dom

Scenario: Interacts with Shadow DOM elements in Web Componenets
Given I opened "https://recipes.lwc.dev" website
Then I should be able to fill the value in Hello Binding Shadow Input Element
8 changes: 8 additions & 0 deletions test/acceptance/gherkin/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ Then('I should see {string}', (str) => {
// From "gherkin/basic.feature" {"line":10,"column":5}
I.see(str);
});

Given('I opened {string} website', (website) => {
I.amOnPage(website);
});

Then('I should be able to fill the value in Hello Binding Shadow Input Element', (website) => {
I.fillField({ shadow: ['my-app', 'recipe-hello-binding', 'ui-input', 'input.input'] }, 'value');
});

0 comments on commit e1eaaa6

Please sign in to comment.