mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
fix(datetime): arrow navigation respects min/max values (#25182)
Resolves #25073
This commit is contained in:
50
core/src/components/datetime/test/minmax/datetime.e2e.ts
Normal file
50
core/src/components/datetime/test/minmax/datetime.e2e.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('datetime: minmax', () => {
|
||||||
|
test('calendar arrow navigation should respect min/max values', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/ionic-team/ionic-framework/issues/25073',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-datetime min="2022-04-22" max="2022-05-21" value="2022-04-22T10:00:00"></ion-datetime>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const observer = new MutationObserver((mutationRecords) => {
|
||||||
|
if (mutationRecords) {
|
||||||
|
window.dispatchEvent(new CustomEvent('datetimeMonthDidChange'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initDatetimeChangeEvent = () => {
|
||||||
|
observer.observe(document.querySelector('ion-datetime').shadowRoot.querySelector('.calendar-body'), {
|
||||||
|
subtree: true,
|
||||||
|
childList: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
|
||||||
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
|
const prevButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(1)');
|
||||||
|
const nextButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(2)');
|
||||||
|
|
||||||
|
expect(nextButton).toBeEnabled();
|
||||||
|
expect(prevButton).toBeDisabled();
|
||||||
|
|
||||||
|
await page.evaluate('initDatetimeChangeEvent()');
|
||||||
|
|
||||||
|
const monthDidChangeSpy = await page.spyOnEvent('datetimeMonthDidChange');
|
||||||
|
|
||||||
|
await nextButton.click();
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
await monthDidChangeSpy.next();
|
||||||
|
|
||||||
|
expect(nextButton).toBeDisabled();
|
||||||
|
expect(prevButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
@ -18,7 +18,8 @@ export const isBefore = (baseParts: DatetimeParts, compareParts: DatetimeParts)
|
|||||||
(baseParts.year === compareParts.year && baseParts.month < compareParts.month) ||
|
(baseParts.year === compareParts.year && baseParts.month < compareParts.month) ||
|
||||||
(baseParts.year === compareParts.year &&
|
(baseParts.year === compareParts.year &&
|
||||||
baseParts.month === compareParts.month &&
|
baseParts.month === compareParts.month &&
|
||||||
baseParts.day! < compareParts.day!)
|
baseParts.day &&
|
||||||
|
baseParts.day < compareParts.day!)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export const isAfter = (baseParts: DatetimeParts, compareParts: DatetimeParts) =
|
|||||||
(baseParts.year === compareParts.year && baseParts.month > compareParts.month) ||
|
(baseParts.year === compareParts.year && baseParts.month > compareParts.month) ||
|
||||||
(baseParts.year === compareParts.year &&
|
(baseParts.year === compareParts.year &&
|
||||||
baseParts.month === compareParts.month &&
|
baseParts.month === compareParts.month &&
|
||||||
baseParts.day! > compareParts.day!)
|
baseParts.day &&
|
||||||
|
baseParts.day > compareParts.day!)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -140,7 +140,10 @@ export const isMonthDisabled = (
|
|||||||
* previous navigation button is disabled.
|
* previous navigation button is disabled.
|
||||||
*/
|
*/
|
||||||
export const isPrevMonthDisabled = (refParts: DatetimeParts, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
|
export const isPrevMonthDisabled = (refParts: DatetimeParts, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
|
||||||
const prevMonth = getPreviousMonth(refParts);
|
const prevMonth = {
|
||||||
|
...getPreviousMonth(refParts),
|
||||||
|
day: null,
|
||||||
|
};
|
||||||
return isMonthDisabled(prevMonth, {
|
return isMonthDisabled(prevMonth, {
|
||||||
minParts,
|
minParts,
|
||||||
maxParts,
|
maxParts,
|
||||||
@ -152,7 +155,10 @@ export const isPrevMonthDisabled = (refParts: DatetimeParts, minParts?: Datetime
|
|||||||
* determine if the next navigation button is disabled.
|
* determine if the next navigation button is disabled.
|
||||||
*/
|
*/
|
||||||
export const isNextMonthDisabled = (refParts: DatetimeParts, maxParts?: DatetimeParts) => {
|
export const isNextMonthDisabled = (refParts: DatetimeParts, maxParts?: DatetimeParts) => {
|
||||||
const nextMonth = getNextMonth(refParts);
|
const nextMonth = {
|
||||||
|
...getNextMonth(refParts),
|
||||||
|
day: null,
|
||||||
|
};
|
||||||
return isMonthDisabled(nextMonth, {
|
return isMonthDisabled(nextMonth, {
|
||||||
maxParts,
|
maxParts,
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,18 @@ export const goto = async (page: Page, url: string, testInfo: TestInfo, original
|
|||||||
|
|
||||||
const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
|
const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
|
||||||
|
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'mode',
|
||||||
|
description: formattedMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rtl) {
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'rtl',
|
||||||
|
description: 'true',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await Promise.all([
|
const result = await Promise.all([
|
||||||
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
|
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
|
||||||
originalFn(formattedUrl),
|
originalFn(formattedUrl),
|
||||||
|
@ -3,3 +3,4 @@ export * from './goto';
|
|||||||
export * from './get-snapshot-settings';
|
export * from './get-snapshot-settings';
|
||||||
export * from './set-ion-viewport';
|
export * from './set-ion-viewport';
|
||||||
export * from './spy-on-event';
|
export * from './spy-on-event';
|
||||||
|
export * from './set-content';
|
||||||
|
59
core/src/utils/test/playwright/page/utils/set-content.ts
Normal file
59
core/src/utils/test/playwright/page/utils/set-content.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the default Playwright page.setContent method.
|
||||||
|
*
|
||||||
|
* Navigates to a blank page, sets the content, and waits for the
|
||||||
|
* Stencil components to be hydrated before proceeding with the test.
|
||||||
|
*
|
||||||
|
* @param page The Playwright page object.
|
||||||
|
* @param html The HTML content to set on the page.
|
||||||
|
*/
|
||||||
|
export const setContent = async (page: Page, html: string) => {
|
||||||
|
if (page.isClosed()) {
|
||||||
|
throw new Error('setContent unavailable: page is already closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL;
|
||||||
|
|
||||||
|
const output = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||||
|
<link href="${baseUrl}/css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="${baseUrl}/scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="${baseUrl}/scripts/testing/scripts.js"></script>
|
||||||
|
<script type="module" src="${baseUrl}/dist/ionic/ionic.esm.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${html}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (baseUrl) {
|
||||||
|
await page.route(baseUrl, (route) => {
|
||||||
|
if (route.request().url() === `${baseUrl}/`) {
|
||||||
|
/**
|
||||||
|
* Intercepts the empty page request and returns the
|
||||||
|
* HTML content that was passed in.
|
||||||
|
*/
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: output,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Allow all other requests to pass through
|
||||||
|
route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}#`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 });
|
||||||
|
};
|
@ -43,12 +43,6 @@ export interface E2EPage extends Page {
|
|||||||
* we need to wait until the changes have been applied to the DOM.
|
* we need to wait until the changes have been applied to the DOM.
|
||||||
*/
|
*/
|
||||||
waitForChanges: (timeoutMs?: number) => Promise<void>;
|
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
|
* Creates a new EventSpy and listens
|
||||||
* on the window for an event.
|
* on the window for an event.
|
||||||
|
@ -8,7 +8,14 @@ import type {
|
|||||||
import { test as base } from '@playwright/test';
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
import { initPageEvents } from './page/event-spy';
|
import { initPageEvents } from './page/event-spy';
|
||||||
import { getSnapshotSettings, goto as goToPage, setIonViewport, spyOnEvent, waitForChanges } from './page/utils';
|
import {
|
||||||
|
getSnapshotSettings,
|
||||||
|
goto as goToPage,
|
||||||
|
setContent,
|
||||||
|
setIonViewport,
|
||||||
|
spyOnEvent,
|
||||||
|
waitForChanges,
|
||||||
|
} from './page/utils';
|
||||||
import type { E2EPage } from './playwright-declarations';
|
import type { E2EPage } from './playwright-declarations';
|
||||||
|
|
||||||
type CustomTestArgs = PlaywrightTestArgs &
|
type CustomTestArgs = PlaywrightTestArgs &
|
||||||
@ -28,6 +35,7 @@ export const test = base.extend<CustomFixtures>({
|
|||||||
|
|
||||||
// Overridden Playwright methods
|
// Overridden Playwright methods
|
||||||
page.goto = (url: string) => goToPage(page, url, testInfo, originalGoto);
|
page.goto = (url: string) => goToPage(page, url, testInfo, originalGoto);
|
||||||
|
page.setContent = (html: string) => setContent(page, html);
|
||||||
// Custom Ionic methods
|
// Custom Ionic methods
|
||||||
page.getSnapshotSettings = () => getSnapshotSettings(page, testInfo);
|
page.getSnapshotSettings = () => getSnapshotSettings(page, testInfo);
|
||||||
page.setIonViewport = () => setIonViewport(page);
|
page.setIonViewport = () => setIonViewport(page);
|
||||||
|
Reference in New Issue
Block a user