diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts index b5e7295c16..c24dfcdfec 100644 --- a/packages/playwright-ct-vue/index.d.ts +++ b/packages/playwright-ct-vue/index.d.ts @@ -50,11 +50,16 @@ export interface MountOptions> { interface MountResult> extends Locator { unmount(): Promise; - rerender(options: { props: Props }): Promise + rerender(options: Omit, 'hooksConfig'>): Promise +} + +interface MountResultJsx extends Locator { + unmount(): Promise; + rerender(props: JSX.Element): Promise } export interface ComponentFixtures { - mount(component: JSX.Element): Promise; + mount(component: JSX.Element): Promise; mount(component: any, options?: MountOptions): Promise; mount(component: any, options: MountOptions & { props: Props }): Promise>; } diff --git a/packages/playwright-ct-vue/registerSource.mjs b/packages/playwright-ct-vue/registerSource.mjs index 5f388308b8..fc70131cba 100644 --- a/packages/playwright-ct-vue/registerSource.mjs +++ b/packages/playwright-ct-vue/registerSource.mjs @@ -33,21 +33,20 @@ export function register(components) { registry.set(name, value); } -const allListeners = []; +const allListeners = new Map(); /** * @param {Component | string} child * @returns {import('vue').VNode | string} */ -function renderChild(child) { - return typeof child === 'string' ? child : render(child); +function createChild(child) { + return typeof child === 'string' ? child : createWrapper(child); } /** * @param {Component} component - * @returns {import('vue').VNode} */ -function render(component) { +function createComponent(component) { if (typeof component === 'string') return component; @@ -87,9 +86,9 @@ function render(component) { if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') { const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:')); const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default'; - slots[slot] = child.children.map(renderChild); + slots[slot] = child.children.map(createChild); } else { - children.push(renderChild(child)); + children.push(createChild(child)); } } @@ -128,9 +127,29 @@ function render(component) { lastArg = children; } + return { Component: componentFunc, props, slots: lastArg, listeners }; +} + +function wrapFunctions(slots) { + const slotsWithRenderFunctions = {}; + if (!Array.isArray(slots)) { + for (const [key, value] of Object.entries(slots || {})) + slotsWithRenderFunctions[key] = () => [value]; + } else if (slots?.length) { + slots['default'] = () => slots; + } + return slotsWithRenderFunctions; +} + +/** + * @param {Component} component + * @returns {import('vue').VNode | string} + */ +function createWrapper(component) { + const { Component, props, slots, listeners } = createComponent(component); // @ts-ignore - const wrapper = h(componentFunc, props, lastArg); - allListeners.push([wrapper, listeners]); + const wrapper = h(Component, props, slots); + allListeners.set(wrapper, listeners); return wrapper; } @@ -156,10 +175,10 @@ function createDevTools() { } const appKey = Symbol('appKey'); -const componentKey = Symbol('componentKey'); +const wrapperKey = Symbol('wrapperKey'); window.playwrightMount = async (component, rootElement, hooksConfig) => { - const wrapper = render(component); + const wrapper = createWrapper(component); const app = createApp({ render: () => wrapper }); @@ -169,7 +188,7 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => { await hook({ app, hooksConfig }); const instance = app.mount(rootElement); rootElement[appKey] = app; - rootElement[componentKey] = wrapper; + rootElement[wrapperKey] = wrapper; for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || []) await hook({ app, hooksConfig, instance }); @@ -183,10 +202,18 @@ window.playwrightUnmount = async rootElement => { }; window.playwrightRerender = async (rootElement, options) => { - const component = rootElement[componentKey].component; - if (!component) + const wrapper = rootElement[wrapperKey]; + if (!wrapper) throw new Error('Component was not mounted'); - for (const [key, value] of Object.entries(options.props || {})) - component.props[key] = value; + const { slots, listeners, props } = createComponent(options); + + wrapper.component.slots = wrapFunctions(slots); + allListeners.set(wrapper, listeners); + + for (const [key, value] of Object.entries(props)) + wrapper.component.props[key] = value; + + if (!Object.keys(props).length) + wrapper.component.update(); }; diff --git a/packages/playwright-ct-vue2/registerSource.mjs b/packages/playwright-ct-vue2/registerSource.mjs index eb22de82e6..70a8946399 100644 --- a/packages/playwright-ct-vue2/registerSource.mjs +++ b/packages/playwright-ct-vue2/registerSource.mjs @@ -163,6 +163,6 @@ window.playwrightRerender = async (element, options) => { if (!component) throw new Error('Component was not mounted'); - for (const [key, value] of Object.entries(/** @type {any} */(options).props)) + for (const [key, value] of Object.entries(/** @type {any} */(options).props || /** @type {any} */(options).options.props)) component.$children[0][key] = value; }; diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index d4a14c8fe9..0583e033e0 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -60,7 +60,8 @@ export const fixtures: Fixtures< await window.playwrightUnmount(rootElement); }); }, - rerender: async (component: JsxComponent | string, options?: Omit) => { + rerender: async (options: JsxComponent | Omit) => { + if (isJsxApi(options)) return await innerRerender(page, options); await innerRerender(page, component, options); } }); @@ -69,6 +70,10 @@ export const fixtures: Fixtures< }, }; +function isJsxApi(options: Record): options is JsxComponent { + return options?.kind === 'jsx'; +} + async function innerRerender(page: Page, jsxOrType: JsxComponent | string, options: Omit = {}): Promise { const component = createComponent(jsxOrType, options); wrapFunctions(component, page, boundCallbacksForMount); diff --git a/packages/playwright-test/types/component.d.ts b/packages/playwright-test/types/component.d.ts index 5fc210c814..3d4a805ef8 100644 --- a/packages/playwright-test/types/component.d.ts +++ b/packages/playwright-test/types/component.d.ts @@ -40,6 +40,6 @@ declare global { interface Window { playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise; playwrightUnmount(rootElement: Element): Promise; - playwrightRerender(rootElement: Element, optionsOrComponent: Omit | Component): Promise; + playwrightRerender(rootElement: Element, component: Component): Promise; } } 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 313092b1b6..b75f7956ae 100644 --- a/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue-cli/src/notation-jsx.spec.tsx @@ -16,11 +16,11 @@ 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 component.rerender() await expect(component.locator('#rerender-count')).toContainText('1337') - - await component.rerender({ props: { count: 42 } }) + + await component.rerender() await expect(component.locator('#rerender-count')).toContainText('42') await expect(component.locator('#remount-count')).toContainText('1') diff --git a/tests/components/ct-vue-vite/src/components/Counter.vue b/tests/components/ct-vue-vite/src/components/Counter.vue index 6e211660b7..a2c6f4c82e 100644 --- a/tests/components/ct-vue-vite/src/components/Counter.vue +++ b/tests/components/ct-vue-vite/src/components/Counter.vue @@ -1,7 +1,9 @@ @@ -13,7 +15,7 @@ let remountCount = 0 defineProps({ count: { type: Number, - required: true + required: false } }) remountCount++ 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 bfdae6120b..6c1bf931f0 100644 --- a/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue-vite/src/notation-jsx.spec.tsx @@ -13,15 +13,39 @@ test('render props', async ({ mount }) => { await expect(component).toContainText('Submit') }) -test('renderer and keep the component instance intact', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('#rerender-count')).toContainText('9001') +test('renderer updates props without remounting', async ({ mount }) => { + const component = await mount() + await expect(component.locator('#props')).toContainText('9001') - await component.rerender({ props: { count: 1337 } }) - await expect(component.locator('#rerender-count')).toContainText('1337') + await component.rerender() + await expect(component).not.toContainText('9001') + await expect(component.locator('#props')).toContainText('1337') - await component.rerender({ props: { count: 42 } }) - await expect(component.locator('#rerender-count')).toContainText('42') + await expect(component.locator('#remount-count')).toContainText('1') +}) + +test('renderer updates event listeners without remounting', async ({ mount }) => { + const component = await mount() + + const messages = [] + await component.rerender( { + messages.push(count) + }} />) + await component.click(); + expect(messages).toEqual(['hello']) + + await expect(component.locator('#remount-count')).toContainText('1') +}) + +test('renderer updates slots without remounting', async ({ mount }) => { + const component = await mount(Default Slot) + await expect(component).toContainText('Default Slot') + + await component.rerender( + + ) + await expect(component).not.toContainText('Default Slot') + await expect(component).toContainText('Test Slot') await expect(component.locator('#remount-count')).toContainText('1') }) 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 0128e6b66e..f044eefe66 100644 --- a/tests/components/ct-vue-vite/src/notation-vue.spec.js +++ b/tests/components/ct-vue-vite/src/notation-vue.spec.js @@ -18,23 +18,51 @@ test('render props', async ({ mount }) => { await expect(component).toContainText('Submit') }) -test('renderer and keep the component instance intact', async ({ mount }) => { + +test('renderer updates props without remounting', async ({ mount }) => { const component = await mount(Counter, { - props: { - count: 9001 - } - }); - await expect(component.locator('#rerender-count')).toContainText('9001') + props: { count: 9001 } + }) + await expect(component.locator('#props')).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') + await component.rerender({ + props: { count: 1337 } + }) + await expect(component).not.toContainText('9001') + await expect(component.locator('#props')).toContainText('1337') await expect(component.locator('#remount-count')).toContainText('1') }) +test('renderer updates event listeners without remounting', async ({ mount }) => { + const component = await mount(Counter) + + const messages = [] + await component.rerender({ + on: { + submit: count => messages.push(count) + } + }) + await component.click(); + expect(messages).toEqual(['hello']) + + await expect(component.locator('#remount-count')).toContainText('1') +}) + +test('renderer updates slots without remounting', async ({ mount }) => { + const component = await mount(Counter, { + slots: { default: 'Default Slot' } + }) + await expect(component).toContainText('Default Slot') + + await component.rerender({ + slots: { main: 'Test Slot' } + }) + await expect(component).not.toContainText('Default Slot') + await expect(component).toContainText('Test Slot') + + await expect(component.locator('#remount-count')).toContainText('1') +}) test('emit an submit event when the button is clicked', async ({ mount }) => { const messages = [] 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 31631e9811..356f1b5a32 100644 --- a/tests/components/ct-vue-vite/src/notation-vue.spec.ts +++ b/tests/components/ct-vue-vite/src/notation-vue.spec.ts @@ -19,19 +19,47 @@ test('render props', async ({ mount }) => { await expect(component).toContainText('Submit') }) -test('renderer and keep the component instance intact', async ({ mount }) => { - const component = await mount<{ count: number }>(Counter, { - props: { - count: 9001 +test('renderer updates props without remounting', async ({ mount }) => { + const component = await mount(Counter, { + props: { count: 9001 } + }) + await expect(component.locator('#props')).toContainText('9001') + + await component.rerender({ + props: { count: 1337 } + }) + await expect(component).not.toContainText('9001') + await expect(component.locator('#props')).toContainText('1337') + + await expect(component.locator('#remount-count')).toContainText('1') +}) + +test('renderer updates event listeners without remounting', async ({ mount }) => { + const component = await mount(Counter) + + const messages = [] + await component.rerender({ + on: { + submit: data => messages.push(data) } - }); - await expect(component.locator('#rerender-count')).toContainText('9001') + }) + await component.click(); + expect(messages).toEqual(['hello']) + + await expect(component.locator('#remount-count')).toContainText('1') +}) - await component.rerender({ props: { count: 1337 } }) - await expect(component.locator('#rerender-count')).toContainText('1337') +test('renderer updates slots without remounting', async ({ mount }) => { + const component = await mount(Counter, { + slots: { default: 'Default Slot' } + }) + await expect(component).toContainText('Default Slot') - await component.rerender({ props: { count: 42 } }) - await expect(component.locator('#rerender-count')).toContainText('42') + await component.rerender({ + slots: { main: 'Test Slot' } + }) + await expect(component).not.toContainText('Default Slot') + await expect(component).toContainText('Test Slot') await expect(component.locator('#remount-count')).toContainText('1') }) 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 a5fe871669..737edc6cff 100644 --- a/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx +++ b/tests/components/ct-vue2-cli/src/notation-jsx.spec.tsx @@ -15,11 +15,11 @@ 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 component.rerender() await expect(component.locator('#rerender-count')).toContainText('1337') - - await component.rerender({ props: { count: 42 } }) + + await component.rerender() await expect(component.locator('#rerender-count')).toContainText('42') await expect(component.locator('#remount-count')).toContainText('1')