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 './matchers';
|
||||
export * from './viewports';
|
||||
export * from './generator';
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Page, TestInfo } from '@playwright/test';
|
||||
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* to be hydrated before proceeding with the test.
|
||||
*/
|
||||
export const goto = async (page: Page, url: string, options: any, testInfo: TestInfo, originalFn: typeof page.goto) => {
|
||||
const { mode, rtl, _testing } = testInfo.project.metadata;
|
||||
export const goto = async (
|
||||
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 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 formattedMode = urlToParams.get('ionic:mode') ?? mode;
|
||||
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
||||
const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing;
|
||||
const formattedRtl = urlToParams.get('rtl') ?? rtlString;
|
||||
const ionicTesting = urlToParams.get('ionic:_testing') ?? true;
|
||||
|
||||
/**
|
||||
* Pass through other custom query params
|
||||
@ -44,12 +73,10 @@ export const goto = async (page: Page, url: string, options: any, testInfo: Test
|
||||
description: formattedMode,
|
||||
});
|
||||
|
||||
if (rtl) {
|
||||
testInfo.annotations.push({
|
||||
type: 'rtl',
|
||||
description: 'true',
|
||||
type: 'direction',
|
||||
description: formattedRtl === 'true' ? 'rtl' : 'ltr',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await Promise.all([
|
||||
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Page, TestInfo } from '@playwright/test';
|
||||
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* 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 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()) {
|
||||
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 output = `
|
||||
<!DOCTYPE html>
|
||||
<html dir="${testInfo.project.metadata.rtl ? 'rtl' : 'ltr'}">
|
||||
<html dir="${direction}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
window.Ionic = {
|
||||
config: {
|
||||
mode: '${testInfo.project.metadata.mode}'
|
||||
mode: '${mode}'
|
||||
}
|
||||
}
|
||||
</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 { TestConfig } from './generator';
|
||||
import type { EventSpy } from './page/event-spy';
|
||||
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 {
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
goto: (
|
||||
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;
|
||||
goto: (url: string, options?: E2EPageOptions) => Promise<null | Response>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Assigns HTML markup to a page.
|
||||
* @param html - The HTML markup to assign to the page
|
||||
* @param options - Ionic config options or Playwright options for the page
|
||||
*/
|
||||
timeout?: number;
|
||||
setContent: (html: string, options?: E2EPageOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* See https://playwright.dev/docs/locators for more information.
|
||||
|
||||
@ -18,7 +18,13 @@ import {
|
||||
locator,
|
||||
} 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 &
|
||||
PlaywrightTestOptions &
|
||||
@ -43,8 +49,8 @@ export async function extendPageFixture(page: E2EPage, testInfo: TestInfo) {
|
||||
const originalLocator = page.locator.bind(page);
|
||||
|
||||
// Overridden Playwright methods
|
||||
page.goto = (url: string, options) => goToPage(page, url, options, testInfo, originalGoto);
|
||||
page.setContent = (html: string) => setContent(page, html, testInfo);
|
||||
page.goto = (url: string, options?: E2EPageOptions) => goToPage(page, url, testInfo, originalGoto, options);
|
||||
page.setContent = (html: string, options?: E2EPageOptions) => setContent(page, html, testInfo, options);
|
||||
page.locator = (selector: string, options?: LocatorOptions) => locator(page, originalLocator, selector, options);
|
||||
|
||||
// Custom Ionic methods
|
||||
|
||||
Reference in New Issue
Block a user