diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 41231c394b..dcab9afe04 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -278,6 +278,13 @@ export class InjectedScript { return []; if (body === 'return-empty') return []; + if (body === 'component') { + if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) + return []; + // Usually, we return the mounted component that is a single child. + // However, when mounting fragments, return the root instead. + return [root.childElementCount === 1 ? root.firstElementChild! : root as Element]; + } throw new Error(`Internal error, unknown control selector ${body}`); } }; @@ -302,16 +309,6 @@ export class InjectedScript { return { queryAll }; } - private _createLayoutEngine(name: LayoutSelectorName): SelectorEngineV2 { - const queryAll = (root: SelectorRoot, body: ParsedSelector) => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) - return []; - const has = !!this.querySelector(body, root, false); - return has ? [root as Element] : []; - }; - return { queryAll }; - } - extend(source: string, params: any): any { const constrFunction = globalThis.eval(` (() => { diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index 17ab6c3cde..33711783de 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -124,8 +124,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: await window.playwrightMount(component, rootElement, hooksConfig); - // When mounting fragments, return selector pointing to the root element. - return rootElement.childNodes.length > 1 ? '#root' : '#root > *'; + return '#root >> control=component'; }, { component, hooksConfig: options.hooksConfig }); return selector; } diff --git a/tests/components/ct-react-vite/src/components/EmptyFragment.tsx b/tests/components/ct-react-vite/src/components/EmptyFragment.tsx new file mode 100644 index 0000000000..64cb1177a3 --- /dev/null +++ b/tests/components/ct-react-vite/src/components/EmptyFragment.tsx @@ -0,0 +1,3 @@ +export default function EmptyFragment() { + return <>{[]}; +} diff --git a/tests/components/ct-react-vite/src/tests.spec.tsx b/tests/components/ct-react-vite/src/tests.spec.tsx index 99b46d5780..7cb5d6bc2e 100644 --- a/tests/components/ct-react-vite/src/tests.spec.tsx +++ b/tests/components/ct-react-vite/src/tests.spec.tsx @@ -4,6 +4,7 @@ import DefaultChildren from './components/DefaultChildren'; import MultipleChildren from './components/MultipleChildren'; import MultiRoot from './components/MultiRoot'; import Counter from './components/Counter'; +import EmptyFragment from './components/EmptyFragment'; test.use({ viewport: { width: 500, height: 500 } }); @@ -27,8 +28,8 @@ test('renderer updates callbacks without remounting', async ({ mount }) => { const component = await mount() const messages: string[] = [] - await component.rerender( { - messages.push(message) + await component.rerender( { + messages.push(message) }} />) await component.click(); expect(messages).toEqual(['hello']) @@ -117,4 +118,11 @@ test('unmount a multi root component', async ({ mount, page }) => { await component.unmount() await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') -}) +}); + +test('get textContent of the empty fragment', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-react/src/components/EmptyFragment.tsx b/tests/components/ct-react/src/components/EmptyFragment.tsx new file mode 100644 index 0000000000..64cb1177a3 --- /dev/null +++ b/tests/components/ct-react/src/components/EmptyFragment.tsx @@ -0,0 +1,3 @@ +export default function EmptyFragment() { + return <>{[]}; +} diff --git a/tests/components/ct-react/src/tests.spec.tsx b/tests/components/ct-react/src/tests.spec.tsx index 9b5bc34158..64e2727c52 100644 --- a/tests/components/ct-react/src/tests.spec.tsx +++ b/tests/components/ct-react/src/tests.spec.tsx @@ -7,6 +7,7 @@ import DefaultChildren from './components/DefaultChildren'; import MultipleChildren from './components/MultipleChildren'; import MultiRoot from './components/MultiRoot'; import Counter from './components/Counter'; +import EmptyFragment from './components/EmptyFragment'; test.use({ viewport: { width: 500, height: 500 } }); @@ -30,8 +31,8 @@ test('renderer updates callbacks without remounting', async ({ mount }) => { const component = await mount() const messages: string[] = [] - await component.rerender( { - messages.push(message) + await component.rerender( { + messages.push(message) }} />) await component.click(); expect(messages).toEqual(['hello']) @@ -127,6 +128,13 @@ test('render delayed data', async ({ mount }) => { await expect(component).toHaveText('complete'); }); +test('get textContent of the empty fragment', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); + const testWithServer = test.extend(serverFixtures); testWithServer('components routing should go through context', async ({ mount, context, server }) => { server.setRoute('/hello', (req, res) => { diff --git a/tests/components/ct-solid/src/components/EmptyFragment.tsx b/tests/components/ct-solid/src/components/EmptyFragment.tsx new file mode 100644 index 0000000000..64cb1177a3 --- /dev/null +++ b/tests/components/ct-solid/src/components/EmptyFragment.tsx @@ -0,0 +1,3 @@ +export default function EmptyFragment() { + return <>{[]}; +} diff --git a/tests/components/ct-solid/src/tests.spec.tsx b/tests/components/ct-solid/src/tests.spec.tsx index e16720aa6c..24dc0f67cc 100644 --- a/tests/components/ct-solid/src/tests.spec.tsx +++ b/tests/components/ct-solid/src/tests.spec.tsx @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-solid' import Button from './components/Button'; import DefaultChildren from './components/DefaultChildren'; import MultiRoot from './components/MultiRoot'; +import EmptyFragment from './components/EmptyFragment'; test.use({ viewport: { width: 500, height: 500 } }); @@ -52,3 +53,10 @@ test('unmount a multi root component', async ({ mount, page }) => { await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') }); + +test('get textContent of the empty fragment', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-svelte-vite/src/components/Empty.svelte b/tests/components/ct-svelte-vite/src/components/Empty.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/components/ct-svelte-vite/src/tests.spec.ts b/tests/components/ct-svelte-vite/src/tests.spec.ts index be24cb8e9c..915c2dc205 100644 --- a/tests/components/ct-svelte-vite/src/tests.spec.ts +++ b/tests/components/ct-svelte-vite/src/tests.spec.ts @@ -18,6 +18,7 @@ import { test, expect } from '@playwright/experimental-ct-svelte'; import Button from './components/Button.svelte'; import DefaultSlot from './components/DefaultSlot.svelte'; import MultiRoot from './components/MultiRoot.svelte'; +import Empty from './components/Empty.svelte'; test.use({ viewport: { width: 500, height: 500 } }); @@ -83,4 +84,11 @@ test('unmount a multi root component', async ({ mount, page }) => { await component.unmount() await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') -}) +}); + +test('get textContent of the empty component', async ({ mount }) => { + const component = await mount(Empty); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-svelte/src/components/Empty.svelte b/tests/components/ct-svelte/src/components/Empty.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/components/ct-svelte/src/tests.spec.ts b/tests/components/ct-svelte/src/tests.spec.ts index be24cb8e9c..915c2dc205 100644 --- a/tests/components/ct-svelte/src/tests.spec.ts +++ b/tests/components/ct-svelte/src/tests.spec.ts @@ -18,6 +18,7 @@ import { test, expect } from '@playwright/experimental-ct-svelte'; import Button from './components/Button.svelte'; import DefaultSlot from './components/DefaultSlot.svelte'; import MultiRoot from './components/MultiRoot.svelte'; +import Empty from './components/Empty.svelte'; test.use({ viewport: { width: 500, height: 500 } }); @@ -83,4 +84,11 @@ test('unmount a multi root component', async ({ mount, page }) => { await component.unmount() await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') -}) +}); + +test('get textContent of the empty component', async ({ mount }) => { + const component = await mount(Empty); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue-cli/src/components/EmptyTemplate.vue b/tests/components/ct-vue-cli/src/components/EmptyTemplate.vue new file mode 100644 index 0000000000..ba5341392f --- /dev/null +++ b/tests/components/ct-vue-cli/src/components/EmptyTemplate.vue @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx b/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx index fda81dad5e..1c275378eb 100644 --- a/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx @@ -4,6 +4,7 @@ import Counter from './components/Counter.vue' import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' import MultiRoot from './components/MultiRoot.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -15,10 +16,10 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount(); await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -92,4 +93,11 @@ test('unmount a multi root component', async ({ mount, page }) => { await component.unmount() await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') -}) +}); + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue-cli/src/notation-vue.spec.ts b/tests/components/ct-vue-cli/src/notation-vue.spec.ts index a620165a5a..52b8c31a22 100644 --- a/tests/components/ct-vue-cli/src/notation-vue.spec.ts +++ b/tests/components/ct-vue-cli/src/notation-vue.spec.ts @@ -6,6 +6,7 @@ import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' import MultiRoot from './components/MultiRoot.vue' import Component from './components/Component.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -20,15 +21,15 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount<{ count: number }>(Counter, { - props: { + props: { count: 9001 } }); await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -117,3 +118,10 @@ test('unmount a multi root component', async ({ mount, page }) => { await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') }) + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(EmptyTemplate); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue-vite/src/components/EmptyTemplate.vue b/tests/components/ct-vue-vite/src/components/EmptyTemplate.vue new file mode 100644 index 0000000000..ba5341392f --- /dev/null +++ b/tests/components/ct-vue-vite/src/components/EmptyTemplate.vue @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx b/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx index c0316b4a7c..bfdae6120b 100644 --- a/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx @@ -4,6 +4,7 @@ import Counter from './components/Counter.vue' import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' import MultiRoot from './components/MultiRoot.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -15,10 +16,10 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount(); await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -96,3 +97,10 @@ test('unmount a multi root component', async ({ mount, page }) => { await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') }) + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue-vite/src/notation-vue.spec.js b/tests/components/ct-vue-vite/src/notation-vue.spec.js index 8b54ba3769..0128e6b66e 100644 --- a/tests/components/ct-vue-vite/src/notation-vue.spec.js +++ b/tests/components/ct-vue-vite/src/notation-vue.spec.js @@ -5,6 +5,7 @@ import DefaultSlot from './components/DefaultSlot.vue' import MultiRoot from './components/MultiRoot.vue' import NamedSlots from './components/NamedSlots.vue' import Component from './components/Component.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -19,15 +20,15 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount(Counter, { - props: { + props: { count: 9001 } }); await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -108,4 +109,11 @@ test('unmount a multi root component', async ({ mount, page }) => { await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') -}) +}); + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(EmptyTemplate); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue-vite/src/notation-vue.spec.ts b/tests/components/ct-vue-vite/src/notation-vue.spec.ts index 5177bdbb09..31631e9811 100644 --- a/tests/components/ct-vue-vite/src/notation-vue.spec.ts +++ b/tests/components/ct-vue-vite/src/notation-vue.spec.ts @@ -6,6 +6,7 @@ import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' import MultiRoot from './components/MultiRoot.vue' import Component from './components/Component.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -20,15 +21,15 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount<{ count: number }>(Counter, { - props: { + props: { count: 9001 } }); await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -120,3 +121,10 @@ test('unmount a multi root component', async ({ mount, page }) => { await expect(page.locator('#root')).not.toContainText('root 1') await expect(page.locator('#root')).not.toContainText('root 2') }) + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(EmptyTemplate); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue2-cli/src/components/EmptyTemplate.vue b/tests/components/ct-vue2-cli/src/components/EmptyTemplate.vue new file mode 100644 index 0000000000..ba5341392f --- /dev/null +++ b/tests/components/ct-vue2-cli/src/components/EmptyTemplate.vue @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx b/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx index e37a8232a9..ba3be167cf 100644 --- a/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx @@ -3,6 +3,7 @@ import Button from './components/Button.vue' import Counter from './components/Counter.vue' import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -14,10 +15,10 @@ test('render props', async ({ mount }) => { test('renderer and keep the component instance intact', async ({ mount }) => { const component = await mount() await expect(component.locator('#rerender-count')).toContainText('9001') - + await component.rerender({ props: { count: 1337 } }) await expect(component.locator('#rerender-count')).toContainText('1337') - + await component.rerender({ props: { count: 42 } }) await expect(component.locator('#rerender-count')).toContainText('42') @@ -83,3 +84,10 @@ test('run hooks', async ({ page, mount }) => { }) expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount el: HTMLButtonElement']) }) + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +}); diff --git a/tests/components/ct-vue2-cli/src/notation-vue.spec.ts b/tests/components/ct-vue2-cli/src/notation-vue.spec.ts index 255c955db1..a67685f415 100644 --- a/tests/components/ct-vue2-cli/src/notation-vue.spec.ts +++ b/tests/components/ct-vue2-cli/src/notation-vue.spec.ts @@ -4,6 +4,7 @@ import Counter from './components/Counter.vue' import DefaultSlot from './components/DefaultSlot.vue' import NamedSlots from './components/NamedSlots.vue' import Component from './components/Component.vue' +import EmptyTemplate from './components/EmptyTemplate.vue' test.use({ viewport: { width: 500, height: 500 } }) @@ -106,3 +107,10 @@ test('unmount', async ({ page, mount }) => { await component.unmount(); await expect(page.locator('#root')).not.toContainText('Submit'); }); + +test('get textContent of the empty template', async ({ mount }) => { + const component = await mount(EmptyTemplate); + expect(await component.allTextContents()).toEqual(['']); + expect(await component.textContent()).toBe(''); + await expect(component).toHaveText(''); +});