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:
Liam DeBeasi
2023-04-26 14:54:22 -04:00
committed by GitHub
parent 4826a3d9f5
commit ea4c24a8af
6 changed files with 183 additions and 45 deletions

View 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,
};

View File

@ -5,3 +5,4 @@ export * from './page/utils';
export * from './drag-element';
export * from './matchers';
export * from './viewports';
export * from './generator';

View File

@ -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 }),

View File

@ -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);
}
};

View File

@ -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.

View File

@ -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