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('');
+});