fix(ct): support empty fragments (#17475)
Currently, we ues `#root` vs `#root > *` selector for component roots depending on the number of root children. This heuristic detects fragments that render multiple elements inside the root. However, this does not work with empty fragments that do not render anything. The fix is to make the `#root >> control=component` selector that would dynamically detect the root. This supports empty fragments and also allows for dynamic updates of the fragments.
This commit is contained in:
parent
d431958603
commit
f17d345ac9
|
|
@ -278,6 +278,13 @@ export class InjectedScript {
|
||||||
return [];
|
return [];
|
||||||
if (body === 'return-empty')
|
if (body === 'return-empty')
|
||||||
return [];
|
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}`);
|
throw new Error(`Internal error, unknown control selector ${body}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -302,16 +309,6 @@ export class InjectedScript {
|
||||||
return { queryAll };
|
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 {
|
extend(source: string, params: any): any {
|
||||||
const constrFunction = globalThis.eval(`
|
const constrFunction = globalThis.eval(`
|
||||||
(() => {
|
(() => {
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
||||||
|
|
||||||
await window.playwrightMount(component, rootElement, hooksConfig);
|
await window.playwrightMount(component, rootElement, hooksConfig);
|
||||||
|
|
||||||
// When mounting fragments, return selector pointing to the root element.
|
return '#root >> control=component';
|
||||||
return rootElement.childNodes.length > 1 ? '#root' : '#root > *';
|
|
||||||
}, { component, hooksConfig: options.hooksConfig });
|
}, { component, hooksConfig: options.hooksConfig });
|
||||||
return selector;
|
return selector;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function EmptyFragment() {
|
||||||
|
return <>{[]}</>;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import DefaultChildren from './components/DefaultChildren';
|
||||||
import MultipleChildren from './components/MultipleChildren';
|
import MultipleChildren from './components/MultipleChildren';
|
||||||
import MultiRoot from './components/MultiRoot';
|
import MultiRoot from './components/MultiRoot';
|
||||||
import Counter from './components/Counter';
|
import Counter from './components/Counter';
|
||||||
|
import EmptyFragment from './components/EmptyFragment';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
|
|
@ -117,4 +118,11 @@ test('unmount a multi root component', async ({ mount, page }) => {
|
||||||
await component.unmount()
|
await component.unmount()
|
||||||
await expect(page.locator('#root')).not.toContainText('root 1')
|
await expect(page.locator('#root')).not.toContainText('root 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
await expect(page.locator('#root')).not.toContainText('root 2')
|
||||||
})
|
});
|
||||||
|
|
||||||
|
test('get textContent of the empty fragment', async ({ mount }) => {
|
||||||
|
const component = await mount(<EmptyFragment />);
|
||||||
|
expect(await component.allTextContents()).toEqual(['']);
|
||||||
|
expect(await component.textContent()).toBe('');
|
||||||
|
await expect(component).toHaveText('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function EmptyFragment() {
|
||||||
|
return <>{[]}</>;
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import DefaultChildren from './components/DefaultChildren';
|
||||||
import MultipleChildren from './components/MultipleChildren';
|
import MultipleChildren from './components/MultipleChildren';
|
||||||
import MultiRoot from './components/MultiRoot';
|
import MultiRoot from './components/MultiRoot';
|
||||||
import Counter from './components/Counter';
|
import Counter from './components/Counter';
|
||||||
|
import EmptyFragment from './components/EmptyFragment';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
|
|
@ -127,6 +128,13 @@ test('render delayed data', async ({ mount }) => {
|
||||||
await expect(component).toHaveText('complete');
|
await expect(component).toHaveText('complete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('get textContent of the empty fragment', async ({ mount }) => {
|
||||||
|
const component = await mount(<EmptyFragment />);
|
||||||
|
expect(await component.allTextContents()).toEqual(['']);
|
||||||
|
expect(await component.textContent()).toBe('');
|
||||||
|
await expect(component).toHaveText('');
|
||||||
|
});
|
||||||
|
|
||||||
const testWithServer = test.extend(serverFixtures);
|
const testWithServer = test.extend(serverFixtures);
|
||||||
testWithServer('components routing should go through context', async ({ mount, context, server }) => {
|
testWithServer('components routing should go through context', async ({ mount, context, server }) => {
|
||||||
server.setRoute('/hello', (req, res) => {
|
server.setRoute('/hello', (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function EmptyFragment() {
|
||||||
|
return <>{[]}</>;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-solid'
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import DefaultChildren from './components/DefaultChildren';
|
import DefaultChildren from './components/DefaultChildren';
|
||||||
import MultiRoot from './components/MultiRoot';
|
import MultiRoot from './components/MultiRoot';
|
||||||
|
import EmptyFragment from './components/EmptyFragment';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
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 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
await expect(page.locator('#root')).not.toContainText('root 2')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('get textContent of the empty fragment', async ({ mount }) => {
|
||||||
|
const component = await mount(<EmptyFragment />);
|
||||||
|
expect(await component.allTextContents()).toEqual(['']);
|
||||||
|
expect(await component.textContent()).toBe('');
|
||||||
|
await expect(component).toHaveText('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { test, expect } from '@playwright/experimental-ct-svelte';
|
||||||
import Button from './components/Button.svelte';
|
import Button from './components/Button.svelte';
|
||||||
import DefaultSlot from './components/DefaultSlot.svelte';
|
import DefaultSlot from './components/DefaultSlot.svelte';
|
||||||
import MultiRoot from './components/MultiRoot.svelte';
|
import MultiRoot from './components/MultiRoot.svelte';
|
||||||
|
import Empty from './components/Empty.svelte';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
|
|
@ -83,4 +84,11 @@ test('unmount a multi root component', async ({ mount, page }) => {
|
||||||
await component.unmount()
|
await component.unmount()
|
||||||
await expect(page.locator('#root')).not.toContainText('root 1')
|
await expect(page.locator('#root')).not.toContainText('root 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { test, expect } from '@playwright/experimental-ct-svelte';
|
||||||
import Button from './components/Button.svelte';
|
import Button from './components/Button.svelte';
|
||||||
import DefaultSlot from './components/DefaultSlot.svelte';
|
import DefaultSlot from './components/DefaultSlot.svelte';
|
||||||
import MultiRoot from './components/MultiRoot.svelte';
|
import MultiRoot from './components/MultiRoot.svelte';
|
||||||
|
import Empty from './components/Empty.svelte';
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } });
|
test.use({ viewport: { width: 500, height: 500 } });
|
||||||
|
|
||||||
|
|
@ -83,4 +84,11 @@ test('unmount a multi root component', async ({ mount, page }) => {
|
||||||
await component.unmount()
|
await component.unmount()
|
||||||
await expect(page.locator('#root')).not.toContainText('root 1')
|
await expect(page.locator('#root')).not.toContainText('root 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
@ -4,6 +4,7 @@ import Counter from './components/Counter.vue'
|
||||||
import DefaultSlot from './components/DefaultSlot.vue'
|
import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import MultiRoot from './components/MultiRoot.vue'
|
import MultiRoot from './components/MultiRoot.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -92,4 +93,11 @@ test('unmount a multi root component', async ({ mount, page }) => {
|
||||||
await component.unmount()
|
await component.unmount()
|
||||||
await expect(page.locator('#root')).not.toContainText('root 1')
|
await expect(page.locator('#root')).not.toContainText('root 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import MultiRoot from './components/MultiRoot.vue'
|
import MultiRoot from './components/MultiRoot.vue'
|
||||||
import Component from './components/Component.vue'
|
import Component from './components/Component.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -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 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
@ -4,6 +4,7 @@ import Counter from './components/Counter.vue'
|
||||||
import DefaultSlot from './components/DefaultSlot.vue'
|
import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import MultiRoot from './components/MultiRoot.vue'
|
import MultiRoot from './components/MultiRoot.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -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 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import MultiRoot from './components/MultiRoot.vue'
|
import MultiRoot from './components/MultiRoot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import Component from './components/Component.vue'
|
import Component from './components/Component.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -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 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import MultiRoot from './components/MultiRoot.vue'
|
import MultiRoot from './components/MultiRoot.vue'
|
||||||
import Component from './components/Component.vue'
|
import Component from './components/Component.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -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 1')
|
||||||
await expect(page.locator('#root')).not.toContainText('root 2')
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
@ -3,6 +3,7 @@ import Button from './components/Button.vue'
|
||||||
import Counter from './components/Counter.vue'
|
import Counter from './components/Counter.vue'
|
||||||
import DefaultSlot from './components/DefaultSlot.vue'
|
import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -83,3 +84,10 @@ test('run hooks', async ({ page, mount }) => {
|
||||||
})
|
})
|
||||||
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount el: HTMLButtonElement'])
|
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount el: HTMLButtonElement'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Counter from './components/Counter.vue'
|
||||||
import DefaultSlot from './components/DefaultSlot.vue'
|
import DefaultSlot from './components/DefaultSlot.vue'
|
||||||
import NamedSlots from './components/NamedSlots.vue'
|
import NamedSlots from './components/NamedSlots.vue'
|
||||||
import Component from './components/Component.vue'
|
import Component from './components/Component.vue'
|
||||||
|
import EmptyTemplate from './components/EmptyTemplate.vue'
|
||||||
|
|
||||||
test.use({ viewport: { width: 500, height: 500 } })
|
test.use({ viewport: { width: 500, height: 500 } })
|
||||||
|
|
||||||
|
|
@ -106,3 +107,10 @@ test('unmount', async ({ page, mount }) => {
|
||||||
await component.unmount();
|
await component.unmount();
|
||||||
await expect(page.locator('#root')).not.toContainText('Submit');
|
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('');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue