fix(datetime): time picker display matches dynamically set value (#25010)

Resolves #24967
This commit is contained in:
Sean Perkins
2022-04-15 12:29:37 -04:00
committed by GitHub
parent b00159e1dd
commit 11493a086a
161 changed files with 650 additions and 416 deletions

View File

@@ -1,156 +0,0 @@
import type {
Page,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
Response,
TestInfo,
} from '@playwright/test';
import { test as base } from '@playwright/test';
import { EventSpy, initPageEvents, addE2EListener } from './eventSpy';
export type IonicPage = Page & {
goto: (url: string) => Promise<null | Response>;
setIonViewport: () => Promise<void>;
getSnapshotSettings: () => string;
spyOnEvent: (eventName: string) => Promise<EventSpy>;
_e2eEventsIds: number;
_e2eEvents: Map<number, any>;
};
type CustomTestArgs = PlaywrightTestArgs &
PlaywrightTestOptions &
PlaywrightWorkerArgs &
PlaywrightWorkerOptions & {
page: IonicPage;
};
type CustomFixtures = {
page: IonicPage;
};
export const test = base.extend<CustomFixtures>({
page: async ({ page }: CustomTestArgs, use: (r: IonicPage) => Promise<void>, testInfo: TestInfo) => {
const oldGoTo = page.goto.bind(page);
/**
* This is an extended version of Playwright's
* page.goto method. In addition to performing
* the normal page.goto work, this code also
* automatically waits for the Stencil components
* to be hydrated before proceeding with the test.
*/
page.goto = async (url: string) => {
const { mode, rtl, _testing } = testInfo.project.metadata;
const splitUrl = url.split('?');
const paramsString = splitUrl[1];
/**
* This allows developers to force a
* certain mode or LTR/RTL config per 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 formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
const results = await Promise.all([
page.waitForFunction(() => (window as any).testAppLoaded === true),
oldGoTo(formattedUrl),
]);
return results[1];
};
/**
* This provides metadata that can be used to
* create a unique screenshot URL.
* For example, we need to be able to differentiate
* between iOS in LTR mode and iOS in RTL mode.
*/
page.getSnapshotSettings = () => {
const url = page.url();
const splitUrl = url.split('?');
const paramsString = splitUrl[1];
const { mode, rtl } = testInfo.project.metadata;
/**
* Account for custom settings when overriding
* the mode/rtl setting. Fall back to the
* project metadata if nothing was found. This
* will happen if you call page.getSnapshotSettings
* before page.goto.
*/
const urlToParams = new URLSearchParams(paramsString);
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
const formattedRtl = urlToParams.get('rtl') ?? rtl;
/**
* If encoded in the search params, the rtl value
* can be `'true'` instead of `true`.
*/
const rtlString = formattedRtl === true || formattedRtl === 'true' ? 'rtl' : 'ltr';
return `${formattedMode}-${rtlString}`;
};
/**
* Taking fullpage screenshots in Playwright
* does not work with ion-content by default.
* The reason is that full page screenshots do not
* expand any scrollable container on the page. Instead,
* they render the full scrollable content of the document itself.
* To work around this, we increase the size of the document
* so the full scrollable content inside of ion-content
* can be captured in a screenshot.
*/
page.setIonViewport = async () => {
const currentViewport = page.viewportSize();
const pixelAmountRenderedOffscreen = await page.evaluate(() => {
const content = document.querySelector('ion-content');
if (content) {
const innerScroll = content.shadowRoot!.querySelector('.inner-scroll')!;
return innerScroll.scrollHeight - content.clientHeight;
}
return 0;
});
const width = currentViewport?.width ?? 640;
const height = (currentViewport?.height ?? 480) + pixelAmountRenderedOffscreen;
await page.setViewportSize({
width,
height,
});
};
/**
* Creates a new EventSpy and listens
* on the window for an event.
* The test will timeout if the event
* never fires.
*
* Usage:
* const ionChange = await page.spyOnEvent('ionChange');
* ...
* await ionChange.next();
*/
page.spyOnEvent = async (eventName: string): Promise<EventSpy> => {
const spy = new EventSpy(eventName);
await addE2EListener(page, eventName, (ev: Event) => spy.push(ev));
return spy;
};
initPageEvents(page);
await use(page);
},
});

View File

@@ -1,2 +1,3 @@
export * from './fixtures';
export * from './eventSpy';
export * from './playwright-page';
export * from './playwright-declarations';
export * from './page/event-spy';

View File

@@ -1,4 +1,4 @@
import type { IonicPage } from './fixtures';
import type { E2EPage } from '../playwright-declarations';
/**
* The EventSpy class allows
@@ -65,7 +65,7 @@ export class EventSpy {
* respond to an event listener created within
* the page itself.
*/
export const initPageEvents = async (page: IonicPage) => {
export const initPageEvents = async (page: E2EPage) => {
page._e2eEventsIds = 0;
page._e2eEvents = new Map();
@@ -82,7 +82,7 @@ export const initPageEvents = async (page: IonicPage) => {
* page context to updates the _e2eEvents map
* when an event is fired.
*/
export const addE2EListener = async (page: IonicPage, eventName: string, callback: (ev: any) => void) => {
export const addE2EListener = async (page: E2EPage, eventName: string, callback: (ev: any) => void) => {
const id = page._e2eEventsIds++;
page._e2eEvents.set(id, {
eventName,

View File

@@ -0,0 +1,33 @@
import type { Page, TestInfo } from '@playwright/test';
/**
* This provides metadata that can be used to
* create a unique screenshot URL.
* For example, we need to be able to differentiate
* between iOS in LTR mode and iOS in RTL mode.
*/
export const getSnapshotSettings = (page: Page, testInfo: TestInfo) => {
const url = page.url();
const splitUrl = url.split('?');
const paramsString = splitUrl[1];
const { mode, rtl } = testInfo.project.metadata;
/**
* Account for custom settings when overriding
* the mode/rtl setting. Fall back to the
* project metadata if nothing was found. This
* will happen if you call page.getSnapshotSettings
* before page.goto.
*/
const urlToParams = new URLSearchParams(paramsString);
const formattedMode = urlToParams.get('ionic:mode') ?? mode;
const formattedRtl = urlToParams.get('rtl') ?? rtl;
/**
* If encoded in the search params, the rtl value
* can be `'true'` instead of `true`.
*/
const rtlString = formattedRtl === true || formattedRtl === 'true' ? 'rtl' : 'ltr';
return `${formattedMode}-${rtlString}`;
};

View File

@@ -0,0 +1,33 @@
import type { Page, TestInfo } from '@playwright/test';
/**
* This is an extended version of Playwright's
* page.goto method. In addition to performing
* the normal page.goto work, this code also
* automatically waits for the Stencil components
* to be hydrated before proceeding with the test.
*/
export const goto = async (page: Page, url: string, testInfo: TestInfo, originalFn: typeof page.goto) => {
const { mode, rtl, _testing } = testInfo.project.metadata;
const splitUrl = url.split('?');
const paramsString = splitUrl[1];
/**
* This allows developers to force a
* certain mode or LTR/RTL config per 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 formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
const result = await Promise.all([
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
originalFn(formattedUrl),
]);
return result[1];
};

View File

@@ -0,0 +1,5 @@
export * from './wait-for-changes';
export * from './goto';
export * from './get-snapshot-settings';
export * from './set-ion-viewport';
export * from './spy-on-event';

View File

@@ -0,0 +1,33 @@
import type { Page } from '@playwright/test';
/**
* Taking fullpage screenshots in Playwright
* does not work with ion-content by default.
* The reason is that full page screenshots do not
* expand any scrollable container on the page. Instead,
* they render the full scrollable content of the document itself.
* To work around this, we increase the size of the document
* so the full scrollable content inside of ion-content
* can be captured in a screenshot.
*
*/
export const setIonViewport = async (page: Page) => {
const currentViewport = page.viewportSize();
const pixelAmountRenderedOffscreen = await page.evaluate(() => {
const content = document.querySelector('ion-content');
if (content) {
const innerScroll = content.shadowRoot!.querySelector('.inner-scroll')!;
return innerScroll.scrollHeight - content.clientHeight;
}
return 0;
});
const width = currentViewport?.width ?? 640;
const height = (currentViewport?.height ?? 480) + pixelAmountRenderedOffscreen;
await page.setViewportSize({
width,
height,
});
};

View File

@@ -0,0 +1,10 @@
import type { E2EPage } from '../../playwright-declarations';
import { addE2EListener, EventSpy } from '../event-spy';
export const spyOnEvent = async (page: E2EPage, eventName: string): Promise<EventSpy> => {
const spy = new EventSpy(eventName);
await addE2EListener(page, eventName, (ev: Event) => spy.push(ev));
return spy;
};

View File

@@ -0,0 +1,57 @@
import type { Page } from '@playwright/test';
import type { HostElement } from '@stencil/core/internal';
/**
* Waits for a combined threshold of a Stencil web component to be re-hydrated in the next repaint + 100ms.
* Used for testing changes to a web component that does not modify CSS classes or introduce new DOM nodes.
*
* Original source: https://github.com/ionic-team/stencil/blob/main/src/testing/puppeteer/puppeteer-page.ts#L298-L363
*/
export const waitForChanges = async (page: Page, timeoutMs = 100) => {
try {
if (page.isClosed()) {
/**
* If the page is already closed, we can skip the long execution of this method
* and return early.
*/
return;
}
await page.evaluate(() => {
// BROWSER CONTEXT
return new Promise<void>((resolve) => {
// Wait for the next repaint to happen
requestAnimationFrame(() => {
const promiseChain: Promise<any>[] = [];
const waitComponentOnReady = (elm: Element | ShadowRoot, promises: Promise<any>[]) => {
if ('shadowRoot' in elm && elm.shadowRoot instanceof ShadowRoot) {
waitComponentOnReady(elm.shadowRoot, promises);
}
const children = elm.children;
const len = children.length;
for (let i = 0; i < len; i++) {
const childElm = children[i];
const childStencilElm = childElm as HostElement;
if (childElm.tagName.includes('-') && typeof childStencilElm.componentOnReady === 'function') {
promises.push(childStencilElm.componentOnReady());
}
waitComponentOnReady(childElm, promises);
}
};
waitComponentOnReady(document.documentElement, promiseChain);
Promise.all(promiseChain)
.then(() => resolve())
.catch(() => resolve());
});
});
});
if (page.isClosed()) {
return;
}
await page.waitForTimeout(timeoutMs);
} catch (e) {
console.error(e);
}
};

View File

@@ -0,0 +1,66 @@
import type { Page, Response } from '@playwright/test';
import type { EventSpy } from './page/event-spy';
export interface E2EPage extends Page {
/**
* Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
* last redirect.
*
* The method will throw an error if:
* - there's an SSL error (e.g. in case of self-signed certificates).
* - target URL is invalid.
* - the `timeout` is exceeded during navigation.
* - the remote server does not respond or is unreachable.
* - the main resource failed to load.
*
* The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 "Not
* Found" and 500 "Internal Server Error". The status code for such responses can be retrieved by calling
* [response.status()](https://playwright.dev/docs/api/class-response#response-status).
*
* > NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
* `about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
* > NOTE: Headless mode doesn't support navigation to a PDF document. See the
* [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
*
* Shortcut for main frame's [frame.goto(url[, options])](https://playwright.dev/docs/api/class-frame#frame-goto)
* @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) => Promise<null | Response>;
/**
* Increases the size of the page viewport to match the `ion-content` contents.
* Use this method when taking full-screen screenshots.
*/
setIonViewport: () => Promise<void>;
/**
* This provides metadata that can be used to create a unique screenshot URL.
* For example, we need to be able to differentiate between iOS in LTR mode and iOS in RTL mode.
*/
getSnapshotSettings: () => string;
/**
* After changes have been made to a component, such as an update to a property or attribute,
* we need to wait until the changes have been applied to the DOM.
*/
waitForChanges: (timeoutMs?: number) => Promise<void>;
/**
* Listens on the window for a specific event to be dispatched.
* Will wait a maximum of 5 seconds for the event to be dispatched.
*/
waitForCustomEvent: (eventName: string) => Promise<Page>;
/**
* Creates a new EventSpy and listens
* on the window for an event.
* The test will timeout if the event
* never fires.
*
* Usage:
* const ionChange = await page.spyOnEvent('ionChange');
* ...
* await ionChange.next();
*/
spyOnEvent: (eventName: string) => Promise<EventSpy>;
_e2eEventsIds: number;
_e2eEvents: Map<number, any>;
}

View File

@@ -0,0 +1,48 @@
import type {
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
TestInfo,
} from '@playwright/test';
import { test as base } from '@playwright/test';
import { initPageEvents } from './page/event-spy';
import {
getSnapshotSettings,
goto as goToPage,
setIonViewport,
spyOnEvent,
waitForChanges,
} from './page/utils';
import type { E2EPage } from './playwright-declarations';
type CustomTestArgs = PlaywrightTestArgs &
PlaywrightTestOptions &
PlaywrightWorkerArgs &
PlaywrightWorkerOptions & {
page: E2EPage;
};
type CustomFixtures = {
page: E2EPage;
};
export const test = base.extend<CustomFixtures>({
page: async ({ page }: CustomTestArgs, use: (r: E2EPage) => Promise<void>, testInfo: TestInfo) => {
const originalGoto = page.goto.bind(page);
// Overridden Playwright methods
page.goto = (url: string) => goToPage(page, url, testInfo, originalGoto);
// Custom Ionic methods
page.getSnapshotSettings = () => getSnapshotSettings(page, testInfo);
page.setIonViewport = () => setIonViewport(page);
page.waitForChanges = (timeoutMs?: number) => waitForChanges(page, timeoutMs);
page.spyOnEvent = (eventName: string) => spyOnEvent(page, eventName);
// Custom event behavior
initPageEvents(page);
await use(page);
},
});