feat(ct): vue3 rerender complete (#17069)
This commit is contained in:
Родитель
8b018f6b41
Коммит
c889b2ad26
|
@ -50,11 +50,16 @@ export interface MountOptions<Props = Record<string, unknown>> {
|
|||
|
||||
interface MountResult<Props = Record<string, unknown>> extends Locator {
|
||||
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 {
|
||||
mount(component: JSX.Element): Promise<MountResult>;
|
||||
mount(component: JSX.Element): Promise<MountResultJsx>;
|
||||
mount(component: any, options?: MountOptions): Promise<MountResult>;
|
||||
mount<Props>(component: any, options: MountOptions & { props: Props }): Promise<MountResult<Props>>;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -60,7 +60,8 @@ export const fixtures: Fixtures<
|
|||
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);
|
||||
}
|
||||
});
|
||||
|
@ -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> {
|
||||
const component = createComponent(jsxOrType, options);
|
||||
wrapFunctions(component, page, boundCallbacksForMount);
|
||||
|
|
|
@ -40,6 +40,6 @@ declare global {
|
|||
interface Window {
|
||||
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>;
|
||||
playwrightUnmount(rootElement: Element): Promise<void>;
|
||||
playwrightRerender(rootElement: Element, optionsOrComponent: Omit<MountOptions, 'hooksConfig'> | Component): Promise<void>;
|
||||
playwrightRerender(rootElement: Element, component: Component): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ test('render props', async ({ mount }) => {
|
|||
test('renderer and keep the component instance intact', async ({ mount }) => {
|
||||
const component = await mount(<Counter count={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 component.rerender({ props: { count: 42 } })
|
||||
|
||||
await component.rerender(<Counter count={42} />)
|
||||
await expect(component.locator('#rerender-count')).toContainText('42')
|
||||
|
||||
await expect(component.locator('#remount-count')).toContainText('1')
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<span id="remount-count">{{ remountCount }}</span>
|
||||
<span id="rerender-count">{{ count }}</span>
|
||||
<div @click="$emit('submit', 'hello')">
|
||||
<div id="props">{{ count }}</div>
|
||||
<div id="remount-count">{{ remountCount }}</div>
|
||||
<slot name="main" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -13,7 +15,7 @@ let remountCount = 0
|
|||
defineProps({
|
||||
count: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
}
|
||||
})
|
||||
remountCount++
|
||||
|
|
|
@ -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(<Counter count={9001} />);
|
||||
await expect(component.locator('#rerender-count')).toContainText('9001')
|
||||
test('renderer updates props without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter 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(<Counter count={1337} />)
|
||||
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(<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')
|
||||
})
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -15,11 +15,11 @@ test('render props', async ({ mount }) => {
|
|||
test('renderer and keep the component instance intact', async ({ mount }) => {
|
||||
const component = await mount(<Counter count={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 component.rerender({ props: { count: 42 } })
|
||||
|
||||
await component.rerender(<Counter count={42} />)
|
||||
await expect(component.locator('#rerender-count')).toContainText('42')
|
||||
|
||||
await expect(component.locator('#remount-count')).toContainText('1')
|
||||
|
|
Загрузка…
Ссылка в новой задаче