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``, 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``, 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`Hello, `, 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(` "Hello, " `) }) 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`Hello, `, 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``, 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(` " " `) }) 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 = '

Hello World

' 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``, 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 did not become interactable') }) it('intercepts "element not interactable" errors and waits for the element to be interactable', async () => { render( html``, 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``, 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``, 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``, 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(', 1, 2, 3') }) it('should be able to chain shadow$ commands', async () => { render( html``, 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``, 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`
Find me
`, 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`
Find me
`, document.body ) expect(await $('div*=me').getHTML(false)).toBe('Find me') }) it('fetches inner element by content correctly with class names', async () => { render( html`
Find me
`, document.body ) expect(await $('div.foo*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('.foo*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('div#bar*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('#bar*=me').getHTML(getHTMLOptions)).toBe('
Find me
') }) 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`
Find me
`, document.body ) expect(await $('.foo*=Find').getHTML(getHTMLOptions)).toBe(`
Find me
`) }) } } it('fetches element by content correctly with nested class names', async () => { /** *
*
*
*
* Find me *
*
*
*
*/ render( html`
Find me
`, document.body ) expect(await $('div.foo*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('.foo*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('div#bar*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('#bar*=me').getHTML(getHTMLOptions)).toBe('
Find me
') }) it('fetches inner element by content correctly with nested attribute selector', async () => { /** *
*
*
*
* Find me *
*
*
*
*/ render( html`
Find me
`, document.body ) expect(await $('[data-testid="foobar"]*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('[data-testid]*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('div[data-testid="foobar"]*=me').getHTML(getHTMLOptions)).toBe('
Find me
') expect(await $('div[data-testid]*=me').getHTML(getHTMLOptions)).toBe('
Find me
') }) it('fetches the parent element by content correctly', async () => { render( html``, document.body ) expect(await $('span=Click Me!').getHTML(false)).toBe('Click Me!') expect(await $('button=Click Me!').getHTML(false)).toBe('Click Me!') 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//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`
  • First
  • Second
  • Third
  • 2nd First
  • 2nd Second
  • 2nd Third
  • 2nd Fourth
  • 2nd Fifth
`, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2B render( html`
Find me
`, document.body ) expect(await $('aria/Find me').getHTML(false)).toBe('Find me') }) it('images with an alt tag', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html`foo`, document.body ) await expect($('aria/foo')).toHaveAttribute('src', 'Find me') }) it('aria label is received by its title attribute', async () => { // https://www.a11yproject.com/posts/title-attributes/ render( html``, document.body ) await expect($('aria/Find me')).toHaveAttribute('src', '/you/got/it.png') }) it('aria label is received by an input placeholder', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, document.body ) await expect($$('aria/Find me')).toBeElementsArrayOfSize(2) }) it('aria label is received by an input aria-placeholder', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, document.body ) const elem = await $('aria/Search') await expect(elem).toHaveValue('Hello World!') }) it('multiple inputs with a label', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, document.body ) const elem = await $('aria/Search') await expect(elem).toHaveValue('Hello World!') }) it('multiple textareas with a label', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, document.body ) const elem = await $('aria/Search') await expect(elem).toHaveValue('Hello World!') }) it('multiple inputs with a label as parent', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, document.body ) const elem = await $('aria/Search') await expect(elem).toHaveValue('Hello World!') }) it('multiple textareas with a label as parent', async () => { // https://www.w3.org/TR/accname-1.1/#step2D render( html` `, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2B render( html`
Some Button
`, 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 () => { // https://www.w3.org/TR/accname-1.1/#step2B render( html`
Some Button
`, document.body ) const elem = await $('aria/Some Button') await expect(elem).toHaveText('Click Me!') }) it('element has direct aria label', async () => { // https://www.w3.org/TR/accname-1.1/#step2C render( html`
`, 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 () { // only in bidi the customElementWrapper is not available 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 () { // only in bidi the customElementWrapper is not available if (!browser.isBidi) { return this.skip() } class Foo extends HTMLElement { } customElements.define('z-foo', Foo) const a = new Foo() a.disconnectedCallback() }) })