feat(ct): allow configuring apps per test (#15551)

This commit is contained in:
Pavel Feldman 2022-07-12 08:37:33 -08:00 committed by GitHub
parent f76fb3e08a
commit f3d3231b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 320 additions and 33 deletions

View File

@ -7,3 +7,5 @@
!registerSource.mjs
!index.d.ts
!index.js
!hooks.d.ts
!hooks.mjs

18
packages/playwright-ct-react/hooks.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export declare function beforeMount(callback: (params: { hooksConfig: any }) => Promise<void>): void;
export declare function afterMount(callback: (params: { hooksConfig: any }) => Promise<void>): void;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const __pw_hooks_before_mount = [];
const __pw_hooks_after_mount = [];
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
export const beforeMount = callback => {
__pw_hooks_before_mount.push(callback);
};
export const afterMount = callback => {
__pw_hooks_after_mount.push(callback);
};

View File

@ -35,7 +35,7 @@ export type PlaywrightTestConfig = Omit<BasePlaywrightTestConfig, 'use'> & {
};
interface ComponentFixtures {
mount(component: JSX.Element): Promise<Locator>;
mount(component: JSX.Element, options?: { hooksConfig?: any }): Promise<Locator>;
}
export const test: TestType<

View File

@ -19,6 +19,10 @@
"./register": {
"types": "./register.d.ts",
"default": "./register.mjs"
},
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"dependencies": {

View File

@ -68,6 +68,12 @@ function render(component) {
}));
}
window.playwrightMount = async component => {
ReactDOM.render(render(component), document.getElementById('root'));
window.playwrightMount = async (component, rootElement, hooksConfig) => {
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
await hook({ hooksConfig });
ReactDOM.render(render(component), rootElement);
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ hooksConfig });
};

View File

@ -7,3 +7,5 @@
!registerSource.mjs
!index.d.ts
!index.js
!hooks.d.ts
!hooks.mjs

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export declare function beforeMount(callback: (params: { hooksConfig: any }) => Promise<void>): void;
export declare function afterMount(callback: (params: { hooksConfig: any }) => Promise<void>): void;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const __pw_hooks_before_mount = [];
const __pw_hooks_after_mount = [];
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
export const beforeMount = callback => {
__pw_hooks_before_mount.push(callback);
};
export const afterMount = callback => {
__pw_hooks_after_mount.push(callback);
};

View File

@ -39,6 +39,7 @@ interface ComponentFixtures {
props?: Props,
slots?: { [key: string]: any },
on?: { [key: string]: Function },
hooksConfig: any,
}): Promise<Locator>;
}

View File

@ -19,6 +19,10 @@
"./register": {
"types": "./register.d.ts",
"default": "./register.mjs"
},
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"dependencies": {

View File

@ -32,7 +32,7 @@ export function register(components) {
registry.set(name, value);
}
window.playwrightMount = async (component, rootElement) => {
window.playwrightMount = async (component, rootElement, hooksConfig) => {
let componentCtor = registry.get(component.type);
if (!componentCtor) {
// Lookup by shorthand.
@ -50,10 +50,18 @@ window.playwrightMount = async (component, rootElement) => {
if (component.kind !== 'object')
throw new Error('JSX mount notation is not supported');
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
await hook({ hooksConfig });
const wrapper = new componentCtor({
target: rootElement,
props: component.options?.props,
});
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ hooksConfig });
for (const [key, listener] of Object.entries(component.options?.on || {}))
wrapper.$on(key, event => listener(event.detail));
};

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { App } from 'vue';
import { App, ComponentPublicInstance } from 'vue';
export declare function onApp(callback: (app: App, appConfig: any) => Promise<void>): void;
export declare function beforeMount(callback: (params: { app: App, hooksConfig: any }) => Promise<void>): void;
export declare function afterMount(callback: (params: { app: App, hooksConfig: any, instance: ComponentPublicInstance }) => Promise<void>): void;

View File

@ -14,9 +14,16 @@
* limitations under the License.
*/
const __pw_hooks_on_app = [];
window.__pw_hooks_on_app = __pw_hooks_on_app;
const __pw_hooks_before_mount = [];
const __pw_hooks_after_mount = [];
export const onApp = callback => {
__pw_hooks_on_app.push(callback);
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
export const beforeMount = callback => {
__pw_hooks_before_mount.push(callback);
};
export const afterMount = callback => {
__pw_hooks_after_mount.push(callback);
};

View File

@ -40,7 +40,7 @@ interface ComponentFixtures {
props?: Props,
slots?: { [key: string]: any },
on?: { [key: string]: Function },
appConfig?: any,
hooksConfig?: any,
}): Promise<Locator>;
}

View File

@ -155,14 +155,15 @@ function createDevTools() {
};
}
window.playwrightMount = async (component, rootElement) => {
window.playwrightMount = async (component, rootElement, hooksConfig) => {
const app = createApp({
render: () => render(component)
});
setDevtoolsHook(createDevTools(), {});
if (component.kind === 'object') {
for (const onAppCallback of /** @type {any} */(window).__pw_hooks_on_app || [])
await onAppCallback(app, /** @type {any} */(component.options)?.appConfig)
}
app.mount(rootElement);
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
await hook({ app, hooksConfig });
const instance = app.mount(rootElement);
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ app, hooksConfig, instance });
};

View File

@ -7,3 +7,5 @@
!registerSource.mjs
!index.d.ts
!index.js
!hooks.d.ts
!hooks.js

20
packages/playwright-ct-vue2/hooks.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CombinedVueInstance, Vue } from 'vue/types/vue';
export declare function beforeMount(callback: (params: { hooksConfig: any }) => Promise<void>): void;
export declare function afterMount(callback: (params: { hooksConfig: any, instance: CombinedVueInstance<Vue, object, object, object, Record<never, any>> }) => Promise<void>): void;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const __pw_hooks_before_mount = [];
const __pw_hooks_after_mount = [];
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
export const beforeMount = callback => {
__pw_hooks_before_mount.push(callback);
};
export const afterMount = callback => {
__pw_hooks_after_mount.push(callback);
};

View File

@ -40,6 +40,7 @@ interface ComponentFixtures {
props?: Props,
slots?: { [key: string]: any },
on?: { [key: string]: Function },
hooksConfig: any,
}): Promise<Locator>;
}

View File

@ -19,6 +19,10 @@
"./register": {
"types": "./register.d.ts",
"default": "./register.mjs"
},
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"dependencies": {

View File

@ -134,9 +134,17 @@ function render(component, h) {
return wrapper;
}
window.playwrightMount = async (component, rootElement) => {
const mounted = new Vue({
window.playwrightMount = async (component, rootElement, hooksConfig) => {
const config = hooksConfig || /** @type {any} */(component).options?.hooksConfig;
for (const hook of /** @type {any} */(window).__pw_hooks_before_mount || [])
await hook({ hooksConfig });
const instance = new Vue({
render: h => render(component, h),
}).$mount();
rootElement.appendChild(mounted.$el);
rootElement.appendChild(instance.$el);
for (const hook of /** @type {any} */(window).__pw_hooks_after_mount || [])
await hook({ hooksConfig, instance });
};

View File

@ -88,7 +88,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
// WebKit does not wait for deferred scripts.
await page.waitForFunction(() => !!window.playwrightMount);
const selector = await page.evaluate(async ({ component }) => {
const selector = await page.evaluate(async ({ component, hooksConfig }) => {
const unwrapFunctions = (object: any) => {
for (const [key, value] of Object.entries(object)) {
if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) {
@ -110,11 +110,11 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
document.body.appendChild(rootElement);
}
await window.playwrightMount(component, rootElement);
await window.playwrightMount(component, rootElement, hooksConfig);
// When mounting fragments, return selector pointing to the root element.
return rootElement.childNodes.length > 1 ? '#root' : '#root > *';
}, { component });
}, { component, hooksConfig: options.hooksConfig });
return selector;
}

View File

@ -25,6 +25,7 @@ export type ObjectComponentOptions = {
props?: { [key: string]: any },
slots?: { [key: string]: any },
on?: { [key: string]: Function },
hooksConfig?: any,
};
export type ObjectComponent = {
@ -37,6 +38,6 @@ export type Component = JsxComponent | ObjectComponent;
declare global {
interface Window {
playwrightMount(component: Component, rootElement: Element): Promise<void>;
playwrightMount(component: Component, rootElement: Element, hooksConfig: any): Promise<void>;
}
}

View File

@ -1 +1,13 @@
//@ts-check
import '../src/index.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
beforeMount(async ({ hooksConfig }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
});
afterMount(async ({}) => {
console.log(`After mount`);
});

View File

@ -7,3 +7,14 @@ test('should work', async ({ mount }) => {
const component = await mount(<App></App>);
await expect(component).toContainText('Hello Vite + React!');
});
test('should configure app', async ({ page, mount }) => {
const messages: string[] = [];
page.on('console', m => messages.push(m.text()));
await mount(<App></App>, {
hooksConfig: {
route: 'A'
}
});
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']);
});

View File

@ -0,0 +1,11 @@
//@ts-check
import { beforeMount, afterMount } from '@playwright/experimental-ct-svelte/hooks';
beforeMount(async ({ hooksConfig }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
});
afterMount(async ({}) => {
console.log(`After mount`);
});

View File

@ -33,3 +33,17 @@ test('should work', async ({ mount }) => {
await component.click();
expect(values).toEqual([{ count: 1 }]);
});
test('should configure app', async ({ page, mount }) => {
const messages: string[] = [];
page.on('console', m => messages.push(m.text()));
await mount(Counter, {
props: {
units: 's',
},
hooksConfig: {
route: 'A'
}
});
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount']);
});

View File

@ -1,5 +1,10 @@
import { onApp } from '@playwright/experimental-ct-vue/hooks';
//@ts-check
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue/hooks';
onApp(async (app, addConfig) => {
console.log(`App ${!!app} configured with config: ${JSON.stringify(addConfig)}`);
beforeMount(async ({ app, hooksConfig }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}, app: ${!!app}`);
});
afterMount(async ({ instance }) => {
console.log(`After mount el: ${instance.$el.constructor.name}`);
});

View File

@ -60,3 +60,12 @@ test('slot should emit events', async ({ mount }) => {
await component.locator('text=Main Content').click();
expect(clickFired).toBeTruthy();
})
test('should run hooks', async ({ page, mount }) => {
const messages = []
page.on('console', m => messages.push(m.text()))
await mount(<Button title='Submit'></Button>, {
hooksConfig: { route: 'A' }
})
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}, app: true', 'After mount el: HTMLButtonElement'])
})

View File

@ -67,16 +67,14 @@ test('optionless should work', async ({ mount }) => {
await expect(component).toContainText('test')
})
test('should configure app', async ({ page, mount }) => {
test('should run hooks', async ({ page, mount }) => {
const messages = []
page.on('console', m => messages.push(m.text()))
const component = await mount(Button, {
await mount(Button, {
props: {
title: 'Submit'
},
appConfig: {
route: 'A'
}
hooksConfig: { route: 'A' }
})
expect(messages).toEqual(['App true configured with config: {\"route\":\"A\"}'])
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}, app: true', 'After mount el: HTMLButtonElement'])
})

View File

@ -0,0 +1,11 @@
//@ts-check
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue2/hooks';
beforeMount(async ({ hooksConfig }) => {
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
});
afterMount(async ({ instance }) => {
console.log(`After mount el: ${instance.$el.constructor.name}`);
});

View File

@ -60,3 +60,12 @@ test('slot should emit events', async ({ mount }) => {
await component.locator('text=Main Content').click();
expect(clickFired).toBeTruthy();
})
test('should run hooks', async ({ page, mount }) => {
const messages = []
page.on('console', m => messages.push(m.text()))
await mount(<Button title='Submit'></Button>, {
hooksConfig: { route: 'A' }
})
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount el: HTMLButtonElement'])
})

View File

@ -60,3 +60,15 @@ test('named slots should work', async ({ mount }) => {
await expect(component).toContainText('Main Content')
await expect(component).toContainText('Footer')
})
test('should run hooks', async ({ page, mount }) => {
const messages = []
page.on('console', m => messages.push(m.text()))
await mount(Button, {
props: {
title: 'Submit'
},
hooksConfig: { route: 'A' }
})
expect(messages).toEqual(['Before mount: {\"route\":\"A\"}', 'After mount el: HTMLButtonElement'])
})