feat(ct): vue2 rerender complete (#17929)
Partial fix for https://github.com/microsoft/playwright/issues/15919
This commit is contained in:
parent
410b4447c8
commit
da9276bd03
8
packages/playwright-ct-vue/index.d.ts
vendored
8
packages/playwright-ct-vue/index.d.ts
vendored
|
|
@ -42,10 +42,10 @@ type JsonObject = { [Key in string]?: JsonValue };
|
||||||
type Slot = string | string[];
|
type Slot = string | string[];
|
||||||
|
|
||||||
export interface MountOptions<Props = Record<string, unknown>> {
|
export interface MountOptions<Props = Record<string, unknown>> {
|
||||||
props?: Props,
|
props?: Props;
|
||||||
slots?: Record<string, Slot> & { default?: Slot };
|
slots?: Record<string, Slot> & { default?: Slot };
|
||||||
on?: Record<string, Function>,
|
on?: Record<string, Function>;
|
||||||
hooksConfig?: JsonObject,
|
hooksConfig?: JsonObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MountResult<Props = Record<string, unknown>> extends Locator {
|
interface MountResult<Props = Record<string, unknown>> extends Locator {
|
||||||
|
|
@ -55,7 +55,7 @@ interface MountResult<Props = Record<string, unknown>> extends Locator {
|
||||||
|
|
||||||
interface MountResultJsx extends Locator {
|
interface MountResultJsx extends Locator {
|
||||||
unmount(): Promise<void>;
|
unmount(): Promise<void>;
|
||||||
rerender(props: JSX.Element): Promise<void>
|
rerender(component: JSX.Element): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentFixtures {
|
export interface ComponentFixtures {
|
||||||
|
|
|
||||||
9
packages/playwright-ct-vue2/index.d.ts
vendored
9
packages/playwright-ct-vue2/index.d.ts
vendored
|
|
@ -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(component: 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>>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,16 @@ export function register(components) {
|
||||||
/**
|
/**
|
||||||
* @param {Component | string} child
|
* @param {Component | string} child
|
||||||
* @param {import('vue').CreateElement} h
|
* @param {import('vue').CreateElement} h
|
||||||
* @returns {import('vue').VNode | string}
|
|
||||||
*/
|
*/
|
||||||
function renderChild(child, h) {
|
function createChild(child, h) {
|
||||||
return typeof child === 'string' ? child : render(child, h);
|
return typeof child === 'string' ? child : createWrapper(child, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Component} component
|
* @param {Component} component
|
||||||
* @param {import('vue').CreateElement} h
|
* @param {import('vue').CreateElement} h
|
||||||
* @returns {import('vue').VNode}
|
|
||||||
*/
|
*/
|
||||||
function render(component, h) {
|
function createComponent(component, h) {
|
||||||
/**
|
/**
|
||||||
* @type {import('vue').Component | string | undefined}
|
* @type {import('vue').Component | string | undefined}
|
||||||
*/
|
*/
|
||||||
|
|
@ -87,9 +85,9 @@ function render(component, h) {
|
||||||
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';
|
||||||
nodeData.scopedSlots[slot] = () => child.children.map(c => renderChild(c, h));
|
nodeData.scopedSlots[slot] = () => child.children.map(c => createChild(c, h));
|
||||||
} else {
|
} else {
|
||||||
children.push(renderChild(child, h));
|
children.push(createChild(child, h));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +108,7 @@ function render(component, h) {
|
||||||
// Vue test util syntax.
|
// Vue test util syntax.
|
||||||
const options = component.options || {};
|
const options = component.options || {};
|
||||||
for (const [key, value] of Object.entries(options.slots || {})) {
|
for (const [key, value] of Object.entries(options.slots || {})) {
|
||||||
const list = (Array.isArray(value) ? value : [value]).map(v => renderChild(v, h));
|
const list = (Array.isArray(value) ? value : [value]).map(v => createChild(v, h));
|
||||||
if (key === 'default')
|
if (key === 'default')
|
||||||
children.push(...list);
|
children.push(...list);
|
||||||
else
|
else
|
||||||
|
|
@ -130,18 +128,33 @@ function render(component, h) {
|
||||||
lastArg = children;
|
lastArg = children;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = h(componentFunc, nodeData, lastArg);
|
return { Component: componentFunc, nodeData, slots: lastArg };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Component} component
|
||||||
|
* @param {import('vue').CreateElement} h
|
||||||
|
* @returns {import('vue').VNode}
|
||||||
|
*/
|
||||||
|
function createWrapper(component, h) {
|
||||||
|
const { Component, nodeData, slots } = createComponent(component, h);
|
||||||
|
const wrapper = h(Component, nodeData, slots);
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceKey = Symbol('instanceKey');
|
const instanceKey = Symbol('instanceKey');
|
||||||
|
const wrapperKey = Symbol('wrapperKey');
|
||||||
|
|
||||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||||
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
|
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
|
||||||
await hook({ hooksConfig });
|
await hook({ hooksConfig });
|
||||||
|
|
||||||
const instance = new Vue({
|
const instance = new Vue({
|
||||||
render: h => render(component, h),
|
render: h => {
|
||||||
|
const wrapper = createWrapper(component, h);
|
||||||
|
/** @type {any} */ (rootElement)[wrapperKey] = wrapper;
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
}).$mount();
|
}).$mount();
|
||||||
rootElement.appendChild(instance.$el);
|
rootElement.appendChild(instance.$el);
|
||||||
/** @type {any} */ (rootElement)[instanceKey] = instance;
|
/** @type {any} */ (rootElement)[instanceKey] = instance;
|
||||||
|
|
@ -159,10 +172,30 @@ window.playwrightUnmount = async rootElement => {
|
||||||
};
|
};
|
||||||
|
|
||||||
window.playwrightRerender = async (element, options) => {
|
window.playwrightRerender = async (element, options) => {
|
||||||
const component = /** @type {any} */(element)[instanceKey];
|
const wrapper = /** @type {any} */(element)[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(/** @type {any} */(options).props || /** @type {any} */(options).options.props))
|
const component = wrapper.componentInstance;
|
||||||
component.$children[0][key] = value;
|
const { nodeData, slots } = createComponent(options, component.$createElement);
|
||||||
|
|
||||||
|
|
||||||
|
for (const [name] of Object.entries(component.$listeners || {})) {
|
||||||
|
component.$off(name);
|
||||||
|
delete component.$listeners[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(nodeData.on || {})) {
|
||||||
|
component.$on(name, value);
|
||||||
|
component.$listeners[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(component.$scopedSlots, nodeData.scopedSlots);
|
||||||
|
component.$slots.default = slots;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(nodeData.props || {}))
|
||||||
|
component[key] = value;
|
||||||
|
|
||||||
|
if (!Object.keys(nodeData.props || {}).length)
|
||||||
|
component.$forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<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>
|
||||||
</div>
|
<slot name="main" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,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(<Counter 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(<Counter 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 messages = []
|
||||||
|
const component = await mount(<Counter />)
|
||||||
|
|
||||||
|
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')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,53 @@ 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')
|
})
|
||||||
|
|
||||||
|
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 component.rerender({
|
||||||
|
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 expect(component.locator('#remount-count')).toContainText('1')
|
await expect(component.locator('#remount-count')).toContainText('1')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue