diff --git a/core/src/utils/test/playwright/generator.ts b/core/src/utils/test/playwright/generator.ts new file mode 100644 index 0000000000..e2cdd940f9 --- /dev/null +++ b/core/src/utils/test/playwright/generator.ts @@ -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, +}; diff --git a/core/src/utils/test/playwright/index.ts b/core/src/utils/test/playwright/index.ts index 6aeb5d7f23..ce51b37126 100644 --- a/core/src/utils/test/playwright/index.ts +++ b/core/src/utils/test/playwright/index.ts @@ -5,3 +5,4 @@ export * from './page/utils'; export * from './drag-element'; export * from './matchers'; export * from './viewports'; +export * from './generator'; diff --git a/core/src/utils/test/playwright/page/utils/goto.ts b/core/src/utils/test/playwright/page/utils/goto.ts index 9f5e84dbcc..3dcf71ac85 100644 --- a/core/src/utils/test/playwright/page/utils/goto.ts +++ b/core/src/utils/test/playwright/page/utils/goto.ts @@ -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', - }); - } + testInfo.annotations.push({ + type: 'direction', + description: formattedRtl === 'true' ? 'rtl' : 'ltr', + }); const result = await Promise.all([ page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }), diff --git a/core/src/utils/test/playwright/page/utils/set-content.ts b/core/src/utils/test/playwright/page/utils/set-content.ts index aaa905cf47..3087789cdf 100644 --- a/core/src/utils/test/playwright/page/utils/set-content.ts +++ b/core/src/utils/test/playwright/page/utils/set-content.ts @@ -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 = ` - +
@@ -30,7 +43,7 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo) = @@ -59,6 +72,6 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo) = } }); - await page.goto(`${baseUrl}#`); + await page.goto(`${baseUrl}#`, options); } }; diff --git a/core/src/utils/test/playwright/playwright-declarations.ts b/core/src/utils/test/playwright/playwright-declarations.ts index 74fd3a8a17..74d4ff93e7 100644 --- a/core/src/utils/test/playwright/playwright-declarations.ts +++ b/core/src/utils/test/playwright/playwright-declarations.ts @@ -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