mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-06 22:29:44 +08:00
test(utils): add support for generators (#27241)
Issue number: N/A --------- This PR adds support for writing tests with generators.8febaddee9- Adds the underlying generator code necessary to create multiple variants of a single test - Note: This PR does not add support for dark mode generators. This will be added in a separate PR.5c498d8daf- Adds the type declarations on `page.goto` and `page.setContent` necessary to accept the config result provided by the `configs` generator function.df8c44b563- Updates the `goto` and `setContent` functionality to support the generator config with backwards compatibility for the legacy tests.
This commit is contained in:
81
core/src/utils/test/playwright/generator.ts
Normal file
81
core/src/utils/test/playwright/generator.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
export type Mode = 'ios' | 'md';
|
||||||
|
export type Direction = 'ltr' | 'rtl';
|
||||||
|
|
||||||
|
export type TitleFn = (title: string) => string;
|
||||||
|
export type ScreenshotFn = (fileName: string) => string;
|
||||||
|
|
||||||
|
export interface TestConfig {
|
||||||
|
mode: Mode;
|
||||||
|
direction: Direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestUtilities {
|
||||||
|
title: TitleFn;
|
||||||
|
screenshot: ScreenshotFn;
|
||||||
|
config: TestConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestConfigOption {
|
||||||
|
modes?: Mode[];
|
||||||
|
directions?: Direction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique test title based on a base title
|
||||||
|
* and a test config. Playwright uses test titles to generate
|
||||||
|
* test IDs for the test reports, so it's important that
|
||||||
|
* each test title is unique.
|
||||||
|
*/
|
||||||
|
const generateTitle = (title: string, config: TestConfig): string => {
|
||||||
|
const { mode, direction } = config;
|
||||||
|
|
||||||
|
return `${title} - ${mode}/${direction}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique filename based on a base filename
|
||||||
|
* and a test config.
|
||||||
|
*/
|
||||||
|
const generateScreenshotName = (fileName: string, config: TestConfig): string => {
|
||||||
|
const { mode, direction } = config;
|
||||||
|
|
||||||
|
return `${fileName}-${mode}-${direction}.png`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a config generate an array of test variants.
|
||||||
|
*/
|
||||||
|
export const configs = (testConfig: TestConfigOption = DEFAULT_TEST_CONFIG_OPTION): TestUtilities[] => {
|
||||||
|
const { modes, directions } = testConfig;
|
||||||
|
|
||||||
|
const configs: TestConfig[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If certain options are not provided,
|
||||||
|
* fall back to the defaults.
|
||||||
|
*/
|
||||||
|
const processedMode: Mode[] = modes ?? DEFAULT_MODES;
|
||||||
|
const processedDirection: Direction[] = directions ?? DEFAULT_DIRECTIONS;
|
||||||
|
|
||||||
|
processedMode.forEach((mode: Mode) => {
|
||||||
|
processedDirection.forEach((direction: Direction) => {
|
||||||
|
configs.push({ mode, direction });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs.map((config) => {
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
title: (title: string) => generateTitle(title, config),
|
||||||
|
screenshot: (fileName: string) => generateScreenshotName(fileName, config),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MODES: Mode[] = ['ios', 'md'];
|
||||||
|
const DEFAULT_DIRECTIONS: Direction[] = ['ltr', 'rtl'];
|
||||||
|
|
||||||
|
const DEFAULT_TEST_CONFIG_OPTION = {
|
||||||
|
modes: DEFAULT_MODES,
|
||||||
|
directions: DEFAULT_DIRECTIONS,
|
||||||
|
};
|
||||||
@ -5,3 +5,4 @@ export * from './page/utils';
|
|||||||
export * from './drag-element';
|
export * from './drag-element';
|
||||||
export * from './matchers';
|
export * from './matchers';
|
||||||
export * from './viewports';
|
export * from './viewports';
|
||||||
|
export * from './generator';
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Page, TestInfo } from '@playwright/test';
|
import type { Page, TestInfo } from '@playwright/test';
|
||||||
|
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an extended version of Playwright's
|
* This is an extended version of Playwright's
|
||||||
@ -7,8 +8,36 @@ import type { Page, TestInfo } from '@playwright/test';
|
|||||||
* automatically waits for the Stencil components
|
* automatically waits for the Stencil components
|
||||||
* to be hydrated before proceeding with the test.
|
* to be hydrated before proceeding with the test.
|
||||||
*/
|
*/
|
||||||
export const goto = async (page: Page, url: string, options: any, testInfo: TestInfo, originalFn: typeof page.goto) => {
|
export const goto = async (
|
||||||
const { mode, rtl, _testing } = testInfo.project.metadata;
|
page: Page,
|
||||||
|
url: string,
|
||||||
|
testInfo: TestInfo,
|
||||||
|
originalFn: typeof page.goto,
|
||||||
|
options?: E2EPageOptions
|
||||||
|
) => {
|
||||||
|
if (options === undefined && testInfo.project.metadata.mode === undefined) {
|
||||||
|
throw new Error(`
|
||||||
|
A config must be passed to page.goto to use a generator test:
|
||||||
|
|
||||||
|
configs().forEach(({ config, title }) => {
|
||||||
|
test(title('example test'), async ({ page }) => {
|
||||||
|
await page.goto('/src/components/button/test/basic', config);
|
||||||
|
});
|
||||||
|
});`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode: Mode;
|
||||||
|
let direction: Direction;
|
||||||
|
|
||||||
|
if (options == undefined) {
|
||||||
|
mode = testInfo.project.metadata.mode;
|
||||||
|
direction = testInfo.project.metadata.rtl ? 'rtl' : 'ltr';
|
||||||
|
} else {
|
||||||
|
mode = options.mode;
|
||||||
|
direction = options.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtlString = direction === 'rtl' ? 'true' : undefined;
|
||||||
|
|
||||||
const splitUrl = url.split('?');
|
const splitUrl = url.split('?');
|
||||||
const paramsString = splitUrl[1];
|
const paramsString = splitUrl[1];
|
||||||
@ -19,8 +48,8 @@ export const goto = async (page: Page, url: string, options: any, testInfo: Test
|
|||||||
*/
|
*/
|
||||||
const urlToParams = new URLSearchParams(paramsString);
|
const urlToParams = new URLSearchParams(paramsString);
|
||||||
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
|
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
|
||||||
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
const formattedRtl = urlToParams.get('rtl') ?? rtlString;
|
||||||
const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing;
|
const ionicTesting = urlToParams.get('ionic:_testing') ?? true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pass through other custom query params
|
* Pass through other custom query params
|
||||||
@ -44,12 +73,10 @@ export const goto = async (page: Page, url: string, options: any, testInfo: Test
|
|||||||
description: formattedMode,
|
description: formattedMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (rtl) {
|
testInfo.annotations.push({
|
||||||
testInfo.annotations.push({
|
type: 'direction',
|
||||||
type: 'rtl',
|
description: formattedRtl === 'true' ? 'rtl' : 'ltr',
|
||||||
description: 'true',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await Promise.all([
|
const result = await Promise.all([
|
||||||
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
|
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Page, TestInfo } from '@playwright/test';
|
import type { Page, TestInfo } from '@playwright/test';
|
||||||
|
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overwrites the default Playwright page.setContent method.
|
* Overwrites the default Playwright page.setContent method.
|
||||||
@ -8,18 +9,30 @@ import type { Page, TestInfo } from '@playwright/test';
|
|||||||
*
|
*
|
||||||
* @param page The Playwright page object.
|
* @param page The Playwright page object.
|
||||||
* @param html The HTML content to set on the page.
|
* @param html The HTML content to set on the page.
|
||||||
* @param testInfo The TestInfo associated with the current test run.
|
* @param testInfo The TestInfo associated with the current test run. (DEPRECATED)
|
||||||
|
* @param options The test config associated with the current test run.
|
||||||
*/
|
*/
|
||||||
export const setContent = async (page: Page, html: string, testInfo: TestInfo) => {
|
export const setContent = async (page: Page, html: string, testInfo: TestInfo, options?: E2EPageOptions) => {
|
||||||
if (page.isClosed()) {
|
if (page.isClosed()) {
|
||||||
throw new Error('setContent unavailable: page is already closed');
|
throw new Error('setContent unavailable: page is already closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mode: Mode;
|
||||||
|
let direction: Direction;
|
||||||
|
|
||||||
|
if (options == undefined) {
|
||||||
|
mode = testInfo.project.metadata.mode;
|
||||||
|
direction = testInfo.project.metadata.rtl ? 'rtl' : 'ltr';
|
||||||
|
} else {
|
||||||
|
mode = options.mode;
|
||||||
|
direction = options.direction;
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL;
|
const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL;
|
||||||
|
|
||||||
const output = `
|
const output = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="${testInfo.project.metadata.rtl ? 'rtl' : 'ltr'}">
|
<html dir="${direction}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||||
@ -30,7 +43,7 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo) =
|
|||||||
<script>
|
<script>
|
||||||
window.Ionic = {
|
window.Ionic = {
|
||||||
config: {
|
config: {
|
||||||
mode: '${testInfo.project.metadata.mode}'
|
mode: '${mode}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -59,6 +72,6 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`${baseUrl}#`);
|
await page.goto(`${baseUrl}#`, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,38 @@
|
|||||||
import type { Page, Response } from '@playwright/test';
|
import type { Page, Response } from '@playwright/test';
|
||||||
|
|
||||||
|
import type { TestConfig } from './generator';
|
||||||
import type { EventSpy } from './page/event-spy';
|
import type { EventSpy } from './page/event-spy';
|
||||||
import type { LocatorOptions, E2ELocator } from './page/utils/locator';
|
import type { LocatorOptions, E2ELocator } from './page/utils/locator';
|
||||||
|
|
||||||
|
export interface E2EPageOptions extends PageOptions, TestConfig {}
|
||||||
|
|
||||||
|
interface PageOptions {
|
||||||
|
/**
|
||||||
|
* Referer header value. If provided it will take preference over the referer header value set by
|
||||||
|
* [page.setExtraHTTPHeaders(headers)](https://playwright.dev/docs/api/class-page#page-set-extra-http-headers).
|
||||||
|
*/
|
||||||
|
referer?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be
|
||||||
|
* changed by using the
|
||||||
|
* [browserContext.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout),
|
||||||
|
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout),
|
||||||
|
* [page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout)
|
||||||
|
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When to consider operation succeeded, defaults to `load`. Events can be either:
|
||||||
|
* - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired.
|
||||||
|
* - `'load'` - consider operation to be finished when the `load` event is fired.
|
||||||
|
* - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms.
|
||||||
|
* - `'commit'` - consider operation to be finished when network response is received and the document started loading.
|
||||||
|
*/
|
||||||
|
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
||||||
|
}
|
||||||
|
|
||||||
export interface E2EPage extends Page {
|
export interface E2EPage extends Page {
|
||||||
/**
|
/**
|
||||||
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
|
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
|
||||||
@ -28,35 +58,15 @@ export interface E2EPage extends Page {
|
|||||||
* @param url URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the
|
* @param url URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the
|
||||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||||
*/
|
*/
|
||||||
goto: (
|
goto: (url: string, options?: E2EPageOptions) => Promise<null | Response>;
|
||||||
url: string,
|
|
||||||
options?: {
|
|
||||||
/**
|
|
||||||
* Referer header value. If provided it will take preference over the referer header value set by
|
|
||||||
* [page.setExtraHTTPHeaders(headers)](https://playwright.dev/docs/api/class-page#page-set-extra-http-headers).
|
|
||||||
*/
|
|
||||||
referer?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be
|
* Assigns HTML markup to a page.
|
||||||
* changed by using the
|
* @param html - The HTML markup to assign to the page
|
||||||
* [browserContext.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout),
|
* @param options - Ionic config options or Playwright options for the page
|
||||||
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout),
|
*/
|
||||||
* [page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout)
|
setContent: (html: string, options?: E2EPageOptions) => Promise<void>;
|
||||||
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
|
|
||||||
*/
|
|
||||||
timeout?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When to consider operation succeeded, defaults to `load`. Events can be either:
|
|
||||||
* - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired.
|
|
||||||
* - `'load'` - consider operation to be finished when the `load` event is fired.
|
|
||||||
* - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms.
|
|
||||||
* - `'commit'` - consider operation to be finished when network response is received and the document started loading.
|
|
||||||
*/
|
|
||||||
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
|
||||||
}
|
|
||||||
) => Promise<null | Response>;
|
|
||||||
/**
|
/**
|
||||||
* Find an element by selector.
|
* Find an element by selector.
|
||||||
* See https://playwright.dev/docs/locators for more information.
|
* See https://playwright.dev/docs/locators for more information.
|
||||||
|
|||||||
@ -18,7 +18,13 @@ import {
|
|||||||
locator,
|
locator,
|
||||||
} from './page/utils';
|
} from './page/utils';
|
||||||
import type { LocatorOptions } from './page/utils';
|
import type { LocatorOptions } from './page/utils';
|
||||||
import type { E2EPage, E2ESkip, BrowserNameOrCallback, SetIonViewportOptions } from './playwright-declarations';
|
import type {
|
||||||
|
E2EPage,
|
||||||
|
E2ESkip,
|
||||||
|
BrowserNameOrCallback,
|
||||||
|
SetIonViewportOptions,
|
||||||
|
E2EPageOptions,
|
||||||
|
} from './playwright-declarations';
|
||||||
|
|
||||||
type CustomTestArgs = PlaywrightTestArgs &
|
type CustomTestArgs = PlaywrightTestArgs &
|
||||||
PlaywrightTestOptions &
|
PlaywrightTestOptions &
|
||||||
@ -43,8 +49,8 @@ export async function extendPageFixture(page: E2EPage, testInfo: TestInfo) {
|
|||||||
const originalLocator = page.locator.bind(page);
|
const originalLocator = page.locator.bind(page);
|
||||||
|
|
||||||
// Overridden Playwright methods
|
// Overridden Playwright methods
|
||||||
page.goto = (url: string, options) => goToPage(page, url, options, testInfo, originalGoto);
|
page.goto = (url: string, options?: E2EPageOptions) => goToPage(page, url, testInfo, originalGoto, options);
|
||||||
page.setContent = (html: string) => setContent(page, html, testInfo);
|
page.setContent = (html: string, options?: E2EPageOptions) => setContent(page, html, testInfo, options);
|
||||||
page.locator = (selector: string, options?: LocatorOptions) => locator(page, originalLocator, selector, options);
|
page.locator = (selector: string, options?: LocatorOptions) => locator(page, originalLocator, selector, options);
|
||||||
|
|
||||||
// Custom Ionic methods
|
// Custom Ionic methods
|
||||||
|
|||||||
Reference in New Issue
Block a user