diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 2a8880d63972..a4154864fb81 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -5,44 +5,6 @@ import type { BrowserRPC } from '../client' // this file should not import anything directly, only types -function convertElementToXPath(element: Element) { - if (!element || !(element instanceof Element)) { - throw new Error( - `Expected DOM element to be an instance of Element, received ${typeof element}`, - ) - } - - return getPathTo(element) -} - -function getPathTo(element: Element): string { - if (element.id !== '') { - return `id("${element.id}")` - } - - if (!element.parentNode || element === document.documentElement) { - return element.tagName - } - - let ix = 0 - const siblings = element.parentNode.childNodes - for (let i = 0; i < siblings.length; i++) { - const sibling = siblings[i] - if (sibling === element) { - return `${getPathTo(element.parentNode as Element)}/${element.tagName}[${ - ix + 1 - }]` - } - if ( - sibling.nodeType === 1 - && (sibling as Element).tagName === element.tagName - ) { - ix++ - } - } - return 'invalid xpath' -} - // @ts-expect-error not typed global const state = (): WorkerGlobalState => __vitest_worker__ // @ts-expect-error not typed global @@ -60,37 +22,99 @@ function triggerCommand(command: string, ...args: any[]) { const provider = runner().provider +function convertElementToCssSelector(element: Element) { + if (!element || !(element instanceof Element)) { + throw new Error( + `Expected DOM element to be an instance of Element, received ${typeof element}`, + ) + } + + return getUniqueCssSelector(element) +} + +function getUniqueCssSelector(el: Element) { + const path = [] + let parent: null | ParentNode + let hasShadowRoot = false + // eslint-disable-next-line no-cond-assign + while (parent = getParent(el)) { + if ((parent as Element).shadowRoot) { + hasShadowRoot = true + } + + const tag = el.tagName + if (el.id) { + path.push(`#${el.id}`) + } + else if (!el.nextElementSibling && !el.previousElementSibling) { + path.push(tag) + } + else { + let index = 0 + let sameTagSiblings = 0 + let elementIndex = 0 + + for (const sibling of parent.children) { + index++ + if (sibling.tagName === tag) { + sameTagSiblings++ + } + if (sibling === el) { + elementIndex = index + } + } + + if (sameTagSiblings > 1) { + path.push(`${tag}:nth-child(${elementIndex})`) + } + else { + path.push(tag) + } + } + el = parent as Element + }; + return `${provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`.toLowerCase() +} + +function getParent(el: Element) { + const parent = el.parentNode + if (parent instanceof ShadowRoot) { + return parent.host + } + return parent +} + export const userEvent: UserEvent = { // TODO: actually setup userEvent with config options setup() { return userEvent }, click(element: Element, options: UserEventClickOptions = {}) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_click', xpath, options) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_click', css, options) }, dblClick(element: Element, options: UserEventClickOptions = {}) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_dblClick', xpath, options) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_dblClick', css, options) }, tripleClick(element: Element, options: UserEventClickOptions = {}) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_tripleClick', xpath, options) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_tripleClick', css, options) }, selectOptions(element, value) { const values = provider === 'webdriverio' ? getWebdriverioSelectOptions(element, value) : getSimpleSelectOptions(element, value) - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_selectOptions', xpath, values) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_selectOptions', css, values) }, type(element: Element, text: string, options: UserEventTypeOptions = {}) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_type', xpath, text, options) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_type', css, text, options) }, clear(element: Element) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_clear', xpath) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_clear', css) }, tab(options: UserEventTabOptions = {}) { return triggerCommand('__vitest_tab', options) @@ -99,23 +123,23 @@ export const userEvent: UserEvent = { return triggerCommand('__vitest_keyboard', text) }, hover(element: Element) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_hover', xpath) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_hover', css) }, unhover(element: Element) { - const xpath = convertElementToXPath(element.ownerDocument.body) - return triggerCommand('__vitest_hover', xpath) + const css = convertElementToCssSelector(element.ownerDocument.body) + return triggerCommand('__vitest_hover', css) }, // non userEvent events, but still useful fill(element: Element, text: string, options) { - const xpath = convertElementToXPath(element) - return triggerCommand('__vitest_fill', xpath, text, options) + const css = convertElementToCssSelector(element) + return triggerCommand('__vitest_fill', css, text, options) }, dragAndDrop(source: Element, target: Element, options = {}) { - const sourceXpath = convertElementToXPath(source) - const targetXpath = convertElementToXPath(target) - return triggerCommand('__vitest_dragAndDrop', sourceXpath, targetXpath, options) + const sourceCss = convertElementToCssSelector(source) + const targetCss = convertElementToCssSelector(target) + return triggerCommand('__vitest_dragAndDrop', sourceCss, targetCss, options) }, } @@ -137,7 +161,7 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[] if (typeof optionValue !== 'string') { const index = options.indexOf(optionValue as HTMLOptionElement) if (index === -1) { - throw new Error(`The element ${convertElementToXPath(optionValue)} was not found in the "select" options.`) + throw new Error(`The element ${convertElementToCssSelector(optionValue)} was not found in the "select" options.`) } return [{ index }] @@ -162,7 +186,7 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[] function getSimpleSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) { return (Array.isArray(value) ? value : [value]).map((v) => { if (typeof v !== 'string') { - return { element: convertElementToXPath(v) } + return { element: convertElementToCssSelector(v) } } return v }) @@ -220,7 +244,7 @@ export const page: BrowserPage = { return triggerCommand('__vitest_screenshot', name, { ...options, element: options.element - ? convertElementToXPath(options.element) + ? convertElementToCssSelector(options.element) : undefined, }) }, diff --git a/packages/browser/src/node/commands/clear.ts b/packages/browser/src/node/commands/clear.ts index 96bfd4c657df..a06f3b4a3f4a 100644 --- a/packages/browser/src/node/commands/clear.ts +++ b/packages/browser/src/node/commands/clear.ts @@ -5,19 +5,18 @@ import type { UserEventCommand } from './utils' export const clear: UserEventCommand = async ( context, - xpath, + selector, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context - const element = iframe.locator(`xpath=${xpath}`) + const element = iframe.locator(`css=${selector}`) await element.clear({ timeout: 1000, }) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - const element = await browser.$(markedXpath) + const element = await browser.$(selector) await element.clearValue() } else { diff --git a/packages/browser/src/node/commands/click.ts b/packages/browser/src/node/commands/click.ts index d01c8f2768bc..b939513bdc2e 100644 --- a/packages/browser/src/node/commands/click.ts +++ b/packages/browser/src/node/commands/click.ts @@ -5,21 +5,20 @@ import type { UserEventCommand } from './utils' export const click: UserEventCommand = async ( context, - xpath, + selector, options = {}, ) => { const provider = context.provider if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe - await tester.locator(`xpath=${xpath}`).click({ + await tester.locator(`css=${selector}`).click({ timeout: 1000, ...options, }) } else if (provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - await browser.$(markedXpath).click(options as any) + await browser.$(selector).click(options as any) } else { throw new TypeError(`Provider "${provider.name}" doesn't support click command`) @@ -28,18 +27,17 @@ export const click: UserEventCommand = async ( export const dblClick: UserEventCommand = async ( context, - xpath, + selector, options = {}, ) => { const provider = context.provider if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe - await tester.locator(`xpath=${xpath}`).dblclick(options) + await tester.locator(`css=${selector}`).dblclick(options) } else if (provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - await browser.$(markedXpath).doubleClick() + await browser.$(selector).doubleClick() } else { throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`) @@ -48,13 +46,13 @@ export const dblClick: UserEventCommand = async ( export const tripleClick: UserEventCommand = async ( context, - xpath, + selector, options = {}, ) => { const provider = context.provider if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe - await tester.locator(`xpath=${xpath}`).click({ + await tester.locator(`css=${selector}`).click({ timeout: 1000, ...options, clickCount: 3, @@ -62,11 +60,10 @@ export const tripleClick: UserEventCommand = async ( } else if (provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` await browser .action('pointer', { parameters: { pointerType: 'mouse' } }) // move the pointer over the button - .move({ origin: await browser.$(markedXpath) }) + .move({ origin: await browser.$(selector) }) // simulate 3 clicks .down() .up() diff --git a/packages/browser/src/node/commands/dragAndDrop.ts b/packages/browser/src/node/commands/dragAndDrop.ts index 03d6740d5dc5..61af689ff270 100644 --- a/packages/browser/src/node/commands/dragAndDrop.ts +++ b/packages/browser/src/node/commands/dragAndDrop.ts @@ -12,8 +12,8 @@ export const dragAndDrop: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const frame = await context.frame() await frame.dragAndDrop( - `xpath=${source}`, - `xpath=${target}`, + `css=${source}`, + `css=${target}`, { timeout: 1000, ...options, @@ -21,10 +21,8 @@ export const dragAndDrop: UserEventCommand = async ( ) } else if (context.provider instanceof WebdriverBrowserProvider) { - const sourceXpath = `//${source}` - const targetXpath = `//${target}` - const $source = context.browser.$(sourceXpath) - const $target = context.browser.$(targetXpath) + const $source = context.browser.$(source) + const $target = context.browser.$(target) const duration = (options as any)?.duration ?? 10 // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 diff --git a/packages/browser/src/node/commands/fill.ts b/packages/browser/src/node/commands/fill.ts index 866c2165aa14..11468afdb963 100644 --- a/packages/browser/src/node/commands/fill.ts +++ b/packages/browser/src/node/commands/fill.ts @@ -5,19 +5,18 @@ import type { UserEventCommand } from './utils' export const fill: UserEventCommand = async ( context, - xpath, + selector, text, options = {}, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context - const element = iframe.locator(`xpath=${xpath}`) + const element = iframe.locator(`css=${selector}`) await element.fill(text, { timeout: 1000, ...options }) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - await browser.$(markedXpath).setValue(text) + await browser.$(selector).setValue(text) } else { throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`) diff --git a/packages/browser/src/node/commands/hover.ts b/packages/browser/src/node/commands/hover.ts index 043c9e21c5fa..fa82faa435e1 100644 --- a/packages/browser/src/node/commands/hover.ts +++ b/packages/browser/src/node/commands/hover.ts @@ -5,19 +5,18 @@ import type { UserEventCommand } from './utils' export const hover: UserEventCommand = async ( context, - xpath, + selector, options = {}, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { - await context.iframe.locator(`xpath=${xpath}`).hover({ + await context.iframe.locator(`css=${selector}`).hover({ timeout: 1000, ...options, }) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - await browser.$(markedXpath).moveTo(options) + await browser.$(selector).moveTo(options) } else { throw new TypeError(`Provider "${context.provider.name}" does not support hover`) diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts index 211e12f13f92..d81b0932b7d9 100644 --- a/packages/browser/src/node/commands/screenshot.ts +++ b/packages/browser/src/node/commands/screenshot.ts @@ -27,9 +27,13 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async ( if (context.provider instanceof PlaywrightBrowserProvider) { if (options.element) { - const { element: elementXpath, ...config } = options - const element = context.iframe.locator(`xpath=${elementXpath}`) - const buffer = await element.screenshot({ ...config, path: savePath }) + const { element: css, ...config } = options + const element = context.iframe.locator(`css=${css}`) + const buffer = await element.screenshot({ + timeout: 1000, + ...config, + path: savePath, + }) return returnResult(options, path, buffer) } @@ -48,8 +52,7 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async ( return returnResult(options, path, buffer) } - const xpath = `//${options.element}` - const element = await page.$(xpath) + const element = await page.$(`${options.element}`) const buffer = await element.saveScreenshot(savePath) return returnResult(options, path, buffer) } diff --git a/packages/browser/src/node/commands/select.ts b/packages/browser/src/node/commands/select.ts index 14adf30d99bf..93607197961f 100644 --- a/packages/browser/src/node/commands/select.ts +++ b/packages/browser/src/node/commands/select.ts @@ -6,20 +6,20 @@ import type { UserEventCommand } from './utils' export const selectOptions: UserEventCommand = async ( context, - xpath, + selector, userValues, options = {}, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { const value = userValues as any as (string | { element: string })[] const { iframe } = context - const selectElement = iframe.locator(`xpath=${xpath}`) + const selectElement = iframe.locator(`css=${selector}`) const values = await Promise.all(value.map(async (v) => { if (typeof v === 'string') { return v } - const elementHandler = await iframe.locator(`xpath=${v.element}`).elementHandle() + const elementHandler = await iframe.locator(`css=${v.element}`).elementHandle() if (!elementHandler) { throw new Error(`Element not found: ${v.element}`) } @@ -38,11 +38,10 @@ export const selectOptions: UserEventCommand = async return } - const markedXpath = `//${xpath}` const browser = context.browser if (values.length === 1 && 'index' in values[0]) { - const selectElement = browser.$(markedXpath) + const selectElement = browser.$(selector) await selectElement.selectByIndex(values[0].index) } else { diff --git a/packages/browser/src/node/commands/type.ts b/packages/browser/src/node/commands/type.ts index e94850943673..ff79fb7e9c34 100644 --- a/packages/browser/src/node/commands/type.ts +++ b/packages/browser/src/node/commands/type.ts @@ -6,7 +6,7 @@ import { keyboardImplementation } from './keyboard' export const type: UserEventCommand = async ( context, - xpath, + selector, text, options = {}, ) => { @@ -14,7 +14,7 @@ export const type: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context - const element = iframe.locator(`xpath=${xpath}`) + const element = iframe.locator(`css=${selector}`) if (!skipClick) { await element.focus() @@ -30,8 +30,7 @@ export const type: UserEventCommand = async ( } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser - const markedXpath = `//${xpath}` - const element = browser.$(markedXpath) + const element = browser.$(selector) if (!skipClick && !await element.isFocused()) { await element.click() diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 396841a0ccdb..5065cc3498bd 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -23,8 +23,8 @@ describe('running browser tests', async () => { console.error(stderr) }) - expect(browserResultJson.testResults).toHaveLength(19) - expect(passedTests).toHaveLength(17) + expect(browserResultJson.testResults).toHaveLength(18) + expect(passedTests).toHaveLength(16) expect(failedTests).toHaveLength(2) expect(stderr).not.toContain('has been externalized for browser compatibility') diff --git a/test/browser/test/click.test.ts b/test/browser/test/click.test.ts deleted file mode 100644 index 6e72ddf41fcc..000000000000 --- a/test/browser/test/click.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { userEvent } from '@vitest/browser/context' -import { expect, test, vi } from 'vitest' - -test('clicks on an element', async () => { - const button = document.createElement('button') - button.textContent = 'Hello World' - const fn = vi.fn() - button.onclick = () => { - fn() - } - document.body.appendChild(button) - - await userEvent.click(button) - expect(fn).toHaveBeenCalled() -}) diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts index c906600ed28c..6014cef11de9 100644 --- a/test/browser/test/dom.test.ts +++ b/test/browser/test/dom.test.ts @@ -37,6 +37,43 @@ describe('dom related activity', () => { ) expect(base64).toBeTypeOf('string') }) + + test('shadow dom screenshot', async () => { + const wrapper = createWrapper() + const div = createNode() + wrapper.appendChild(div) + + const shadow = div.attachShadow({ mode: 'open' }) + const shadowDiv = createNode() + shadow.appendChild(shadowDiv) + + const screenshotPath = await page.screenshot({ + element: shadowDiv, + }) + expect(screenshotPath).toMatch( + /__screenshots__\/dom.test.ts\/dom-related-activity-shadow-dom-screenshot-1.png/, + ) + }) + + test('svg screenshot', async () => { + const wrapper = createWrapper() + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('width', '100') + svg.setAttribute('height', '100') + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect.setAttribute('width', '100') + rect.setAttribute('height', '100') + rect.setAttribute('fill', 'red') + svg.appendChild(rect) + wrapper.appendChild(svg) + + const screenshotPath = await page.screenshot({ + element: svg, + }) + expect(screenshotPath).toMatch( + /__screenshots__\/dom.test.ts\/dom-related-activity-svg-screenshot-1.png/, + ) + }) }) function createWrapper() { diff --git a/test/browser/test/userEvent.test.ts b/test/browser/test/userEvent.test.ts index b02b64ebf07b..d71fd29b78d4 100644 --- a/test/browser/test/userEvent.test.ts +++ b/test/browser/test/userEvent.test.ts @@ -37,6 +37,37 @@ describe('userEvent.click', () => { expect(onClick).not.toHaveBeenCalled() }) + + test('click inside shadow dom', async () => { + const shadowRoot = createShadowRoot() + const button = document.createElement('button') + button.textContent = 'Click me' + shadowRoot.appendChild(button) + + const onClick = vi.fn() + button.addEventListener('click', onClick) + + await userEvent.click(button) + + expect(onClick).toHaveBeenCalled() + }) + + test('clicks inside svg', async () => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + circle.setAttribute('cx', '50') + circle.setAttribute('cy', '50') + circle.setAttribute('r', '40') + svg.appendChild(circle) + document.body.appendChild(svg) + + const onClick = vi.fn() + circle.addEventListener('click', onClick) + + await userEvent.click(circle) + + expect(onClick).toHaveBeenCalled() + }) }) describe('userEvent.dblClick', () => { @@ -154,6 +185,79 @@ describe('userEvent.hover, userEvent.unhover', () => { expect(pointerEntered).toBe(false) expect(mouseEntered).toBe(false) }) + + test('hover works with shadow root', async () => { + const shadowRoot = createShadowRoot() + const target = document.createElement('div') + target.style.width = '100px' + target.style.height = '100px' + + let mouseEntered = false + let pointerEntered = false + target.addEventListener('mouseover', () => { + mouseEntered = true + }) + target.addEventListener('pointerenter', () => { + pointerEntered = true + }) + target.addEventListener('pointerleave', () => { + pointerEntered = false + }) + target.addEventListener('mouseout', () => { + mouseEntered = false + }) + + shadowRoot.appendChild(target) + + await userEvent.hover(target) + + expect(pointerEntered).toBe(true) + expect(mouseEntered).toBe(true) + + await userEvent.unhover(target) + + expect(pointerEntered).toBe(false) + expect(mouseEntered).toBe(false) + }) + + test('hover works with svg', async () => { + const target = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + circle.setAttribute('cx', '50') + circle.setAttribute('cy', '50') + circle.setAttribute('r', '40') + target.appendChild(circle) + document.body.appendChild(target) + target.style.width = '100px' + target.style.height = '100px' + + let mouseEntered = false + let pointerEntered = false + target.addEventListener('mouseover', () => { + mouseEntered = true + }) + target.addEventListener('pointerenter', () => { + pointerEntered = true + }) + target.addEventListener('pointerleave', () => { + pointerEntered = false + }) + target.addEventListener('mouseout', () => { + mouseEntered = false + }) + + document.body.appendChild(target) + + await userEvent.hover(target) + + expect(pointerEntered).toBe(true) + expect(mouseEntered).toBe(true) + + await userEvent.unhover(target) + + expect(pointerEntered).toBe(false) + expect(mouseEntered).toBe(false) + }) }) const inputLike = [ @@ -161,19 +265,16 @@ const inputLike = [ const input = document.createElement('input') input.type = 'text' input.placeholder = 'Type here' - document.body.appendChild(input) return input }, () => { const input = document.createElement('textarea') input.placeholder = 'Type here' - document.body.appendChild(input) return input }, () => { const contentEditable = document.createElement('div') contentEditable.contentEditable = 'true' - document.body.appendChild(contentEditable) return contentEditable }, ] @@ -301,6 +402,21 @@ describe.each(inputLike)('userEvent.type', (getElement) => { ]) }) + test('types into a shadow root input', async () => { + const shadowRoot = createShadowRoot() + const { input, keydown, value } = createTextInput(shadowRoot) + + await userEvent.type(input, 'Hello') + expect(value()).toBe('Hello') + expect(keydown).toEqual([ + 'H', + 'e', + 'l', + 'l', + 'o', + ]) + }) + // strangly enough, original userEvent doesn't support this, // but we can implement it test.skipIf(server.provider === 'preview')('selectall works correctly', async () => { @@ -314,7 +430,7 @@ describe.each(inputLike)('userEvent.type', (getElement) => { expect(input.value).toBe('') }) - function createTextInput() { + function createTextInput(root: Node = document.body) { const input = getElement() const keydown: string[] = [] const keyup: string[] = [] @@ -324,7 +440,7 @@ describe.each(inputLike)('userEvent.type', (getElement) => { input.addEventListener('keyup', (event: KeyboardEvent) => { keyup.push(event.key) }) - document.body.appendChild(input) + root.appendChild(input) return { input, keydown, @@ -362,6 +478,7 @@ describe('userEvent.tab', () => { describe.each(inputLike)('userEvent.fill', async (getInput) => { test('correctly fills the input value', async () => { const input = getInput() + document.body.appendChild(input) function value() { if ('value' in input) { return input.value @@ -375,6 +492,21 @@ describe.each(inputLike)('userEvent.fill', async (getInput) => { await userEvent.fill(input, 'Another Value') expect(value()).toBe('Another Value') }) + + test('fill input in shadow root', async () => { + const input = getInput() + const shadowRoot = createShadowRoot() + shadowRoot.appendChild(input) + function value() { + if ('value' in input) { + return input.value + } + return input.textContent + } + + await userEvent.fill(input, 'Hello') + expect(value()).toBe('Hello') + }) }) describe('userEvent.keyboard', async () => { @@ -600,3 +732,10 @@ describe.each([ }) }) }) + +function createShadowRoot() { + const div = document.createElement('div') + const shadowRoot = div.attachShadow({ mode: 'open' }) + document.body.appendChild(div) + return shadowRoot +}