feat(ct): vue3 rerender complete (#17069)

This commit is contained in:
sand4rt 2022-10-07 00:07:32 +02:00 committed by GitHub
parent 8b018f6b41
commit c889b2ad26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 180 additions and 61 deletions

View file

@ -50,11 +50,16 @@ export interface MountOptions<Props = Record<string, unknown>> {
interface MountResult<Props = Record<string, unknown>> extends Locator { interface MountResult<Props = Record<string, unknown>> extends Locator {
unmount(): Promise<void>; unmount(): Promise<void>;
rerender(options: { props: Props }): Promise<void> rerender(options: Omit<MountOptions<Props>, 'hooksConfig'>): Promise<void>
}
interface MountResultJsx extends Locator {
unmount(): Promise<void>;
rerender(props: JSX.Element): Promise<void>
} }
export interface ComponentFixtures { export interface ComponentFixtures {
mount(component: JSX.Element): Promise<MountResult>; mount(component: JSX.Element): Promise<MountResultJsx>;
mount(component: any, options?: MountOptions): Promise<MountResult>; mount(component: any, options?: MountOptions): Promise<MountResult>;
mount<Props>(component: any, options: MountOptions & { props: Props }): Promise<MountResult<Props>>; mount<Props>(component: any, options: MountOptions & { props: Props }): Promise<MountResult<Props>>;
} }

View file

@ -33,21 +33,20 @@ export function register(components) {
registry.set(name, value); registry.set(name, value);
} }
const allListeners = []; const allListeners = new Map();
/** /**
* @param {Component | string} child * @param {Component | string} child
* @returns {import('vue').VNode | string} * @returns {import('vue').VNode | string}
*/ */
function renderChild(child) { function createChild(child) {
return typeof child === 'string' ? child : render(child); return typeof child === 'string' ? child : createWrapper(child);
} }
/** /**
* @param {Component} component * @param {Component} component
* @returns {import('vue').VNode}
*/ */
function render(component) { function createComponent(component) {
if (typeof component === 'string') if (typeof component === 'string')
return component; return component;
@ -87,9 +86,9 @@ function render(component) {
if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') { if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') {
const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:')); const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:'));
const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default'; const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default';
slots[slot] = child.children.map(renderChild); slots[slot] = child.children.map(createChild);
} else { } else {
children.push(renderChild(child)); children.push(createChild(child));
} }
} }
@ -128,9 +127,29 @@ function render(component) {
lastArg = children; 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 // @ts-ignore
const wrapper = h(componentFunc, props, lastArg); const wrapper = h(Component, props, slots);
allListeners.push([wrapper, listeners]); allListeners.set(wrapper, listeners);
return wrapper; return wrapper;
} }
@ -156,10 +175,10 @@ function createDevTools() {
} }
const appKey = Symbol('appKey'); const appKey = Symbol('appKey');
const componentKey = Symbol('componentKey'); const wrapperKey = Symbol('wrapperKey');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
const wrapper = render(component); const wrapper = createWrapper(component);
const app = createApp({ const app = createApp({
render: () => wrapper render: () => wrapper
}); });
@ -169,7 +188,7 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
await hook({ app, hooksConfig }); await hook({ app, hooksConfig });
const instance = app.mount(rootElement); const instance = app.mount(rootElement);
rootElement[appKey] = app; rootElement[appKey] = app;
rootElement[componentKey] = wrapper; rootElement[wrapperKey] = wrapper;
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || []) for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ app, hooksConfig, instance }); await hook({ app, hooksConfig, instance });
@ -183,10 +202,18 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightRerender = async (rootElement, options) => { window.playwrightRerender = async (rootElement, options) => {
const component = rootElement[componentKey].component; const wrapper = rootElement[wrapperKey];
if (!component) if (!wrapper)
throw new Error('Component was not mounted'); throw new Error('Component was not mounted');
for (const [key, value] of Object.entries(options.props || {})) const { slots, listeners, props } = createComponent(options);
component.props[key] = value;
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();
}; };

View file

@ -163,6 +163,6 @@ window.playwrightRerender = async (element, options) => {
if (!component) if (!component)
throw new Error('Component was not mounted'); 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; component.$children[0][key] = value;
}; };

View file

@ -60,7 +60,8 @@ export const fixtures: Fixtures<
await window.playwrightUnmount(rootElement); await window.playwrightUnmount(rootElement);
}); });
}, },
rerender: async (component: JsxComponent | string, options?: Omit<MountOptions, 'hooksConfig'>) => { rerender: async (options: JsxComponent | Omit<MountOptions, 'hooksConfig'>) => {
if (isJsxApi(options)) return await innerRerender(page, options);
await innerRerender(page, component, options); await innerRerender(page, component, options);
} }
}); });
@ -69,6 +70,10 @@ export const fixtures: Fixtures<
}, },
}; };
function isJsxApi(options: Record<string, unknown>): options is JsxComponent {
return options?.kind === 'jsx';
}
async function innerRerender(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> { async function innerRerender(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> {
const component = createComponent(jsxOrType, options); const component = createComponent(jsxOrType, options);
wrapFunctions(component, page, boundCallbacksForMount); wrapFunctions(component, page, boundCallbacksForMount);

View file

@ -40,6 +40,6 @@ declare global {
interface Window { interface Window {
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>; playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>;
playwrightUnmount(rootElement: Element): Promise<void>; playwrightUnmount(rootElement: Element): Promise<void>;
playwrightRerender(rootElement: Element, optionsOrComponent: Omit<MountOptions, 'hooksConfig'> | Component): Promise<void>; playwrightRerender(rootElement: Element, component: Component): Promise<void>;
} }
} }

View file

@ -17,10 +17,10 @@ test('renderer and keep the component instance intact', async ({ mount }) => {
const component = await mount(<Counter count={9001} />); const component = await mount(<Counter count={9001} />);
await expect(component.locator('#rerender-count')).toContainText('9001') await expect(component.locator('#rerender-count')).toContainText('9001')
await component.rerender({ props: { count: 1337 } }) await component.rerender(<Counter count={1337} />)
await expect(component.locator('#rerender-count')).toContainText('1337') await expect(component.locator('#rerender-count')).toContainText('1337')
await component.rerender({ props: { count: 42 } }) await component.rerender(<Counter count={42} />)
await expect(component.locator('#rerender-count')).toContainText('42') await expect(component.locator('#rerender-count')).toContainText('42')
await expect(component.locator('#remount-count')).toContainText('1') await expect(component.locator('#remount-count')).toContainText('1')

View file

@ -1,7 +1,9 @@
<template> <template>
<div> <div @click="$emit('submit', 'hello')">
<span id="remount-count">{{ remountCount }}</span> <div id="props">{{ count }}</div>
<span id="rerender-count">{{ count }}</span> <div id="remount-count">{{ remountCount }}</div>
<slot name="main" />
<slot />
</div> </div>
</template> </template>
@ -13,7 +15,7 @@ let remountCount = 0
defineProps({ defineProps({
count: { count: {
type: Number, type: Number,
required: true required: false
} }
}) })
remountCount++ remountCount++

View file

@ -13,15 +13,39 @@ test('render props', async ({ mount }) => {
await expect(component).toContainText('Submit') 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 count={9001} />); const component = await mount(<Counter count={9001} />)
await expect(component.locator('#rerender-count')).toContainText('9001') await expect(component.locator('#props')).toContainText('9001')
await component.rerender({ props: { count: 1337 } }) await component.rerender(<Counter count={1337} />)
await expect(component.locator('#rerender-count')).toContainText('1337') await expect(component).not.toContainText('9001')
await expect(component.locator('#props')).toContainText('1337')
await component.rerender({ props: { count: 42 } }) await expect(component.locator('#remount-count')).toContainText('1')
await expect(component.locator('#rerender-count')).toContainText('42') })
test('renderer updates event listeners without remounting', async ({ mount }) => {
const component = await mount(<Counter />)
const messages = []
await component.rerender(<Counter v-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>Default Slot</Counter>)
await expect(component).toContainText('Default Slot')
await component.rerender(<Counter>
<template v-slot:main>Test Slot</template>
</Counter>)
await expect(component).not.toContainText('Default Slot')
await expect(component).toContainText('Test Slot')
await expect(component.locator('#remount-count')).toContainText('1') await expect(component.locator('#remount-count')).toContainText('1')
}) })

View file

@ -18,23 +18,51 @@ test('render props', async ({ mount }) => {
await expect(component).toContainText('Submit') 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, { const component = await mount(Counter, {
props: { props: { count: 9001 }
count: 9001 })
} await expect(component.locator('#props')).toContainText('9001')
});
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') props: { count: 1337 }
})
await component.rerender({ props: { count: 42 } }) await expect(component).not.toContainText('9001')
await expect(component.locator('#rerender-count')).toContainText('42') await expect(component.locator('#props')).toContainText('1337')
await expect(component.locator('#remount-count')).toContainText('1') 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 }) => { test('emit an submit event when the button is clicked', async ({ mount }) => {
const messages = [] const messages = []

View file

@ -19,19 +19,47 @@ test('render props', async ({ mount }) => {
await expect(component).toContainText('Submit') 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<{ count: number }>(Counter, { const component = await mount(Counter, {
props: { props: { count: 9001 }
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 component.rerender({ props: { count: 1337 } }) await expect(component.locator('#remount-count')).toContainText('1')
await expect(component.locator('#rerender-count')).toContainText('1337') })
await component.rerender({ props: { count: 42 } }) test('renderer updates slots without remounting', async ({ mount }) => {
await expect(component.locator('#rerender-count')).toContainText('42') 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') await expect(component.locator('#remount-count')).toContainText('1')
}) })

View file

@ -16,10 +16,10 @@ test('renderer and keep the component instance intact', async ({ mount }) => {
const component = await mount(<Counter count={9001} />) const component = await mount(<Counter count={9001} />)
await expect(component.locator('#rerender-count')).toContainText('9001') await expect(component.locator('#rerender-count')).toContainText('9001')
await component.rerender({ props: { count: 1337 } }) await component.rerender(<Counter count={1337} />)
await expect(component.locator('#rerender-count')).toContainText('1337') await expect(component.locator('#rerender-count')).toContainText('1337')
await component.rerender({ props: { count: 42 } }) await component.rerender(<Counter count={42} />)
await expect(component.locator('#rerender-count')).toContainText('42') await expect(component.locator('#rerender-count')).toContainText('42')
await expect(component.locator('#remount-count')).toContainText('1') await expect(component.locator('#remount-count')).toContainText('1')