mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(datetime): time picker display matches dynamically set value (#25010)
Resolves #24967
This commit is contained in:
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './fixtures';
|
||||
export * from './eventSpy';
|
||||
export * from './playwright-page';
|
||||
export * from './playwright-declarations';
|
||||
export * from './page/event-spy';
|
||||
|
||||
@@ -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,
|
||||
@@ -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}`;
|
||||
};
|
||||
33
core/src/utils/test/playwright/page/utils/goto.ts
Normal file
33
core/src/utils/test/playwright/page/utils/goto.ts
Normal 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];
|
||||
};
|
||||
5
core/src/utils/test/playwright/page/utils/index.ts
Normal file
5
core/src/utils/test/playwright/page/utils/index.ts
Normal 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';
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
10
core/src/utils/test/playwright/page/utils/spy-on-event.ts
Normal file
10
core/src/utils/test/playwright/page/utils/spy-on-event.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
66
core/src/utils/test/playwright/playwright-declarations.ts
Normal file
66
core/src/utils/test/playwright/playwright-declarations.ts
Normal 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>;
|
||||
}
|
||||
48
core/src/utils/test/playwright/playwright-page.ts
Normal file
48
core/src/utils/test/playwright/playwright-page.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user