import { browser, expect, $ } from '@wdio/globals'
import { spyOn, mock, fn, unmock } from '@wdio/browser-runner'
import { html, render } from 'lit'
import isUrl from 'is-url'
import defaultExport, { namedExportValue } from 'someModule'
import namespacedModule from '@namespace/module'
import { someExport, namedExports, someFunction } from '@testing-library/user-event'
* a CJS module
*/
import stringWidth from 'string-width'
import { SimpleGreeting } from './components/LitComponent.ts'
const getQuestionFn = spyOn(SimpleGreeting.prototype, 'getQuestion')
mock('./components/constants.ts', async (mod) => {
return {
GREETING: mod.GREETING + ' Sir'
}
})
mock('string-width', () => ({
default: () => 123
}))
mock('graphql-request', () => ({
gql: fn(),
GraphQLClient: class GraphQLClientMock {
request = fn().mockResolvedValue({ result: 'Thanks for your answer!' })
}
}))
mock('@testing-library/user-event', async (mod) => {
return {
someExport: 'foobarloo',
namedExports: Object.keys(mod),
someFunction: fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValueOnce('third')
}
})
unmock('is-url')
mock('@namespace/module')
describe('Lit Component testing', () => {
it('recognises require files', () => {
expect(window.globalSetupScriptExecuted).toBe(true)
expect(window.mochaGlobalSetupExecuted).toBe(true)
})
it('should render component', async function () {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return
}
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
const innerElem = await $('simple-greeting').$('p')
expect(await innerElem.getText()).toBe('Hello Sir, WebdriverIO! How are you today?')
})
it('should render with mocked component function', async function () {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return
}
getQuestionFn.mockReturnValue('Does this work?')
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
const innerElem = await $('simple-greeting').$('p')
expect(await innerElem.getText()).toBe('Hello Sir, WebdriverIO! Does this work?')
})
it('does not stale on elements not found', async () => {
const start = Date.now()
await expect($('non-existing-element')).not.toBePresent()
await expect($('non-existing-element')).not.toBeDisplayed()
await expect(await $('non-existing-element')).not.toBePresent()
await expect(await $('non-existing-element')).not.toBeDisplayed()
expect(Date.now() - start).toBeLessThan(1500)
})
describe('shadow root piercing', function () {
it('should allow to pierce into closed shadow roots', async function () {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return this.skip()
}
render(
html`<closed-node>Hello, </closed-node>`,
document.body
)
await expect($('.findMe')).toHaveText('I am hidden!')
* Note: the `getText` command doesn't expose text from closed shadow roots
*/
const closedNode = $('closed-node')
await expect(closedNode).toHaveText('Hello,')
await expect(closedNode).toMatchInlineSnapshot(`
"<closed-node>Hello,
<template shadowrootmode="closed">
<style>section { color: blue; }</style>
<h2>Closed Node</h2>
<section>
<slot></slot>
<closed-node-nested>hidden
<template shadowrootmode="closed">
<style>.findMe { color: green; }</style>
<h2>Deep Closed Node</h2>
<div class="findMe">I am
<slot></slot>!</div>
</template>
</closed-node-nested>
</section>
</template>
</closed-node>"
`)
})
it('can fetch multiple elements within various closed shadow roots', async function () {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return this.skip()
}
render(
html`<closed-node>Hello, </closed-node>`,
document.body
)
const root = $('closed-node')
await expect(root.$('h2')).toHaveText('Closed Node')
expect(await root.$$('h2').map((h2) => h2.getText())).toEqual([
'Closed Node',
'Deep Closed Node'
])
})
})
describe('snapshot testing', () => {
beforeEach(() => {
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
})
it('of elements', async () => {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return
}
const elem = $('simple-greeting')
await expect(elem).toMatchSnapshot()
await expect(elem).toMatchInlineSnapshot(`
"<simple-greeting name="WebdriverIO">
<template shadowrootmode="open">
<style>:host { color: blue; }</style>
<div>
<p>Hello Sir, WebdriverIO! Does this work?</p>
<button>Good</button>
<hr />
<em></em>
<sub-elem>
<template shadowrootmode="open">
<style>.selectMeToo { color: blue; }</style>
<div>
<p class="selectMe">I am within another shadow root element</p>
<p class="selectMeToo">I am within another shadow root element as well</p>
</div>
</template>
</sub-elem>
</div>
</template>
</simple-greeting>"
`)
})
it('of objects', async () => {
const elem = $('simple-greeting')
await expect(elem.getCSSProperty('background-color')).toMatchSnapshot()
await expect(elem.getCSSProperty('background-color')).toMatchInlineSnapshot(`
{
"parsed": {
"alpha": 0,
"hex": "#000000",
"rgba": "rgba(0,0,0,0)",
"type": "color",
},
"property": "background-color",
"value": "rgba(0,0,0,0)",
}
`)
await expect({ foo: 'bar' }).toMatchSnapshot()
await expect({ foo: 'bar' }).toMatchInlineSnapshot(`
{
"foo": "bar",
}
`)
})
it('should be able to fetch elements that created without registering to the element registry', async () => {
* only run snapshot tests in non-Safari browsers as shadow dom piercing
* is not yet supported in Safari
*/
if (browser.capabilities.browserName?.toLowerCase() === 'safari') {
return
}
const shadowResult = await browser.execute(() => {
const shadowElement = document.createElement('div')
shadowElement.id = 'helloshadow'
document.body.appendChild(shadowElement)
const shadowCreated = shadowElement.attachShadow({ mode: 'open' })
shadowCreated.innerHTML = '<p id=\'shadowelement\'>Hello World</p>'
const shadowExists = document.querySelector('#helloshadow')?.shadowRoot
return shadowExists ? 'created' : 'failed'
})
expect(shadowResult).toBe('created')
const shadowRoot = await browser.execute(() =>
Boolean(document.querySelector('#helloshadow')?.shadowRoot)
)
expect(shadowRoot).toBe(true)
const shadowElement = await browser.execute(() =>
Boolean(document.querySelector('#helloshadow')?.shadowRoot?.querySelector('#shadowelement'))
)
expect(shadowElement).toBe(true)
await browser.$('#helloshadow').waitForDisplayed()
await browser.$('#shadowelement').waitForExist()
})
})
it('maps the driver response when the element is not interactable so that we shown an aligned message with the best information we can', async () => {
render(
html`<input style="display: none;" />`,
document.body
)
const err = await $('input').click().catch((err) => err)
expect(err.name).toBe('webdriverio(middleware): element did not become interactable')
expect(err.message).toBe('Element <input style="display: none;" /> did not become interactable')
})
it('intercepts "element not interactable" errors and waits for the element to be interactable', async () => {
render(
html`<input style="display: none;" />`,
document.body
)
setTimeout(() => {
document.querySelector('input').setAttribute('style', '')
}, 1000)
await $('input').click()
})
it('intercepts "element not interactable" errors and waits for the element in array to be interactable', async () => {
render(
html`<input style="display: none;" />`,
document.body
)
setTimeout(() => {
document.querySelector('input').setAttribute('style', '')
}, 1000)
await $$('input')[0].click()
})
it('should allow to auto mock dependencies', () => {
expect(defaultExport).toBe('barfoo')
expect(namedExportValue).toBe('foobar')
expect(namespacedModule).toBe('some value')
})
it('should allow to manual mock namespaces deps', async () => {
expect(someExport).toBe('foobarloo')
expect(namedExports).toEqual(['PointerEventsCheckLevel', 'default', 'userEvent'])
})
it('should allow to have different mock return values', () => {
expect(someFunction()).toBe('first')
expect(someFunction()).toBe('second')
expect(someFunction()).toBe('third')
expect(someFunction()).toBe(undefined)
})
it('should allow to mock CJS modules', () => {
expect(stringWidth('a')).toBe(123)
})
it('should allow to unmock', () => {
expect(isUrl).not.toBe('mocked value')
})
it('should have access to globals', () => {
expect(process.env.WDIO_PRESET).toBe('lit')
expect(window.WDIO_ENV_TEST).toBe('passed')
expect(window.TEST_COMMAND).toBe('serve')
})
it('should allow to manual mock dependencies', async function () {
* this fails in Safari as the click on the button is not recognised
*/
if (browser.capabilities.browserName.toLowerCase() === 'safari') {
return this.skip()
}
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
await $('simple-greeting').$('button').click()
await expect($('simple-greeting').$('em')).toHaveText('Thanks for your answer!')
})
it('should call execute method with the element', async function () {
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
const result = await $('simple-greeting').execute((elem, a, b, c) => {
return `${elem.outerHTML}, ${a}, ${b}, ${c}`
}, 1, 2, 3)
await expect(result).toEqual('<simple-greeting name="WebdriverIO"></simple-greeting>, 1, 2, 3')
})
it('should be able to chain shadow$ commands', async () => {
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
await expect($('simple-greeting').shadow$('sub-elem').shadow$('.selectMe'))
.toHaveText('I am within another shadow root element')
const pText = await $('simple-greeting')
.shadow$('sub-elem')
.shadow$$('p')
.map((elem) => elem.getText())
expect(pText).toEqual([
'I am within another shadow root element',
'I am within another shadow root element as well'
])
})
it('should be able to check if element found by shadow$ and shadow$$ is present', async () => {
render(
html`<simple-greeting name="WebdriverIO" />`,
document.body
)
await expect($('simple-greeting').shadow$('sub-elem').shadow$('.selectMe')).toBePresent()
await expect($('simple-greeting').shadow$('sub-elem').shadow$$('.selectMe')[0]).toBePresent()
})
it('should not stale process due to alert or prompt', async () => {
alert('test')
prompt('test')
confirm('test')
await expect(browser).toHaveTitle('WebdriverIO Browser Test')
})
describe('Selector Tests', () => {
const getHTMLOptions = { includeSelectorTag: false, prettify: false }
it('fetches element by content correctly', async () => {
render(
html`<div><div><div>Find me</div></div></div>`,
document.body
)
expect(await $('div=Find me').getHTML(false)).toBe('Find me')
expect(await $('div*=me').getHTML(false)).toBe('Find me')
})
it('fetches inner element by content correctly', async () => {
render(
html`<div><div><span>Find me</span></div></div>`,
document.body
)
expect(await $('div*=me').getHTML(false)).toBe('<span>Find me</span>')
})
it('fetches inner element by content correctly with class names', async () => {
render(
html`<div class="foo" id="#bar"><div><span>Find me</span></div></div>`,
document.body
)
expect(await $('div.foo*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('.foo*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('div#bar*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('#bar*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
})
const outerClassLists = ['foo', 'bar foo', 'foo bar baz', 'bar foo baz', 'bar baz foo']
const innerClassLists = ['foo-bar-baz', 'bar-foo-baz', 'bar-baz-foo']
for (const outerClassList of outerClassLists) {
for (const innerClassList of innerClassLists) {
it(`fetches element by content correctly with nested class names where the inner classlist is "${innerClassList}" and the outer classlist is "${outerClassList}"`, async () => {
render(
html`<div class="${outerClassList}"><div class="${innerClassList}"></div><div><div>Find me</div></div></div>`,
document.body
)
expect(await $('.foo*=Find').getHTML(getHTMLOptions)).toBe(`<div class="${innerClassList}"></div><div><div>Find me</div></div>`)
})
}
}
it('fetches element by content correctly with nested class names', async () => {
* <div class="foo" id="#bar">
* <div>
* <div class="foo" id="#bar">
* <div>
* <span>Find me</span>
* </div>
* </div>
* </div>
* </div>
*/
render(
html`
<div class="foo" id="#bar"><div><div class="foo" id="#bar"><div><span>Find me</span></div></div></div></div>`,
document.body
)
expect(await $('div.foo*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('.foo*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('div#bar*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('#bar*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
})
it('fetches inner element by content correctly with nested attribute selector', async () => {
* <div data-testid="foobar">
* <div>
* <div data-testid="foobar">
* <div>
* <span>Find me</span>
* </div>
* </div>
* </div>
* </div>
*/
render(
html`
<div data-testid="foobar"><div><div data-testid="foobar"><div><span>Find me</span></div></div></div></div>`,
document.body
)
expect(await $('[data-testid="foobar"]*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('[data-testid]*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('div[data-testid="foobar"]*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
expect(await $('div[data-testid]*=me').getHTML(getHTMLOptions)).toBe('<div><span>Find me</span></div>')
})
it('fetches the parent element by content correctly', async () => {
render(
html`<button><span>Click Me!</span></button>`,
document.body
)
expect(await $('span=Click Me!').getHTML(false)).toBe('Click Me!')
expect(await $('button=Click Me!').getHTML(false)).toBe('<span>Click Me!</span>')
const elem = $('button=Click Me!')
await expect(elem).toHaveText('Click Me!')
})
it('fetches the element with multiple text nodes by the content', async () => {
const container = document.createElement('p')
container.appendChild(document.createTextNode('Find'))
container.appendChild(document.createTextNode(' me'))
render(container, document.body)
const elem = await $('p=Find me')
await expect(elem).toHaveText('Find me')
})
it('fetches element by JS function', async () => {
expect((await $(() => document.body).getTagName()).toLowerCase()).toBe('body')
})
it('can save a screenshot', async () => {
expect((await browser.saveScreenshot('./screenshot.png')).type)
.toBe('Buffer')
})
it('can save a pdf', async () => {
* Safari does not support 'POST /session/<sessionId>/print' command
*/
if (browser.capabilities.browserName.toLowerCase() === 'safari') {
return
}
expect((await browser.savePDF('./screenshot.pdf')).type)
.toBe('Buffer')
})
it('supports custom command registration in before hook', async () => {
expect(await browser.someCustomCommand()).toBe('Hello World')
})
it('supports custom matchers added by services', async () => {
await expect('foo').toBeFoo()
let error
try {
await expect('bar').toBeFoo()
} catch (err) {
error = err
}
expect(error.message).toBe('expected bar to be foo')
})
it('should support nested element calls', async function () {
render(
html`<section>
<div class="first">
<ul>
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>
</div>
<div class="second">
<ul>
<li>2nd First</li>
<li>2nd Second</li>
<li>2nd Third</li>
<li>2nd Fourth</li>
<li>2nd Fifth</li>
</ul>
</div>
</section>`,
document.body
)
const first = $('.first').$$('li')
await expect(first).toBeElementsArrayOfSize(3)
await expect(first[0]).toHaveText('First')
await expect(first[1]).toHaveText('Second')
await expect(first[2]).toHaveText('Third')
const second = $('.second').$$('li')
await expect(second).toBeElementsArrayOfSize(5)
await expect(second[0]).toHaveText('2nd First')
await expect(second[1]).toHaveText('2nd Second')
await expect(second[2]).toHaveText('2nd Third')
await expect(second[3]).toHaveText('2nd Fourth')
await expect(second[4]).toHaveText('2nd Fifth')
const nestedElement = $('.second').$('li')
await expect(nestedElement).toHaveText('2nd First')
})
describe('a11y selectors', () => {
it('aria label is received from element content', async () => {
render(
html`<div><div><div>Find me</div></div></div>`,
document.body
)
expect(await $('aria/Find me').getHTML(false)).toBe('Find me')
})
it('images with an alt tag', async () => {
render(
html`<img alt="foo" src="Find me">`,
document.body
)
await expect($('aria/foo')).toHaveAttribute('src', 'Find me')
})
it('aria label is received by its title attribute', async () => {
render(
html`<img src="/you/got/it.png" alt="" title="Find me" />`,
document.body
)
await expect($('aria/Find me')).toHaveAttribute('src', '/you/got/it.png')
})
it('aria label is received by an input placeholder', async () => {
render(
html`
<input type="text" placeholder="Find me">
<textarea placeholder="Find me"></textarea>
`,
document.body
)
await expect($$('aria/Find me')).toBeElementsArrayOfSize(2)
})
it('aria label is received by an input aria-placeholder', async () => {
render(
html`
<input type="text" aria-placeholder="Find me">
<textarea aria-placeholder="Find me"></textarea>
`,
document.body
)
await expect($$('aria/Find me')).toBeElementsArrayOfSize(2)
})
* fails due to https://github.com/webdriverio/webdriverio/issues/8826
*/
it('input with a label', async () => {
render(
html`
<label for="search">Search</label>
<input id="search" type="text" value="Hello World!" />
`,
document.body
)
const elem = await $('aria/Search')
await expect(elem).toHaveValue('Hello World!')
})
it('multiple inputs with a label', async () => {
render(
html`
<label for="search-1">Search 1</label>
<input id="search-1" type="text" value="Hello World! 1" />
<label for="search-2">Search 2</label>
<input id="search-2" type="text" value="Hello World 2" />
`,
document.body
)
const elem1 = await $('aria/Search 1')
await expect(elem1).toHaveValue('Hello World! 1')
const elem2 = await $('aria/Search 2')
await expect(elem2).toHaveValue('Hello World 2')
})
it('textarea with a label', async () => {
render(
html`
<label for="search">Search</label>
<textarea id="search">Hello World!</textarea>
`,
document.body
)
const elem = await $('aria/Search')
await expect(elem).toHaveValue('Hello World!')
})
it('multiple textareas with a label', async () => {
render(
html`
<label for="search-1">Search 1</label>
<textarea id="search-1">Hello World! 1</textarea>
<label for="search-2">Search 2</label>
<textarea id="search-2">Hello World 2</textarea>
`,
document.body
)
const elem1 = await $('aria/Search 1')
await expect(elem1).toHaveValue('Hello World! 1')
const elem2 = await $('aria/Search 2')
await expect(elem2).toHaveValue('Hello World 2')
})
it('input with a label as parent', async () => {
render(
html`
<label>
Search
<input type="text" value="Hello World!" />
</label>
`,
document.body
)
const elem = await $('aria/Search')
await expect(elem).toHaveValue('Hello World!')
})
it('multiple inputs with a label as parent', async () => {
render(
html`
<label>
Search 1
<input type="text" value="Hello World! 1" />
</label>
<label>
Search 2
<input type="text" value="Hello World! 2" />
</label>
`,
document.body
)
const elem1 = await $('aria/Search 1')
await expect(elem1).toHaveValue('Hello World! 1')
const elem2 = await $('aria/Search 2')
await expect(elem2).toHaveValue('Hello World! 2')
})
it('textarea with a label as parent', async () => {
render(
html`
<label>
Search
<textarea>Hello World!</textarea>
</label>
`,
document.body
)
const elem = await $('aria/Search')
await expect(elem).toHaveValue('Hello World!')
})
it('multiple textareas with a label as parent', async () => {
render(
html`
<label>
Search 1
<textarea>Hello World! 1</textarea>
</label>
<label>
Search 2
<textarea>Hello World! 2</textarea>
</label>
`,
document.body
)
const elem1 = await $('aria/Search 1')
await expect(elem1).toHaveValue('Hello World! 1')
const elem2 = await $('aria/Search 2')
await expect(elem2).toHaveValue('Hello World! 2')
})
it('aria label is recevied by other element with aria-labelledBy', async () => {
render(
html`
<button aria-labelledby="ref-1">Click Me!</button>
<div id="ref-1">Some Button</div>
`,
document.body
)
const elem = await $('aria/Some Button')
await expect(elem).toHaveText('Click Me!')
})
it('aria label is recevied by other element with aria-describedby', async () => {
render(
html`
<button aria-describedby="ref-1">Click Me!</button>
<div id="ref-1">Some Button</div>
`,
document.body
)
const elem = await $('aria/Some Button')
await expect(elem).toHaveText('Click Me!')
})
it('element has direct aria label', async () => {
render(
html`<div>
<button aria-label="FindMe">Click Me!</button>
</div>`,
document.body
)
const elem = await $('aria/FindMe')
await expect(elem).toHaveText('Click Me!')
})
})
})
it('should support WASM', async () => {
const source = fetch('/browser-runner/wasm/add.wasm')
const wasmModule = await WebAssembly.instantiateStreaming(source)
expect(wasmModule.instance.exports.add(1, 2)).toBe(3)
})
it('should allow to re-fetch elements', async () => {
let i = 0
const stage = document.createElement('div')
stage.id = 'stage'
document.body.appendChild(stage)
setInterval(async () => {
const span = document.createElement('span')
span.textContent = 'Hello, world! ' + ++i
stage.appendChild(span)
}, 500)
await expect($('#stage').$$('span')[4]).toHaveText('Hello, world! 5')
})
it('can initiate web component elements with new keyword', () => {
class Foo extends HTMLElement { }
customElements.define('x-foo', Foo)
const a = new Foo()
expect(a.tagName).toBe('X-FOO')
})
it('connectedCallback should not fail if no original connectedCallback is defined', function () {
if (!browser.isBidi) {
return this.skip()
}
class Foo extends HTMLElement { }
customElements.define('y-foo', Foo)
const a = new Foo()
a.connectedCallback()
})
it('disConnectedCallback should not fail if no original disConnectedCallback is defined', function () {
if (!browser.isBidi) {
return this.skip()
}
class Foo extends HTMLElement { }
customElements.define('z-foo', Foo)
const a = new Foo()
a.disconnectedCallback()
})
})