diff --git a/core/src/components/datetime/test/minmax/datetime.e2e.ts b/core/src/components/datetime/test/minmax/datetime.e2e.ts
new file mode 100644
index 0000000000..2b66f462c1
--- /dev/null
+++ b/core/src/components/datetime/test/minmax/datetime.e2e.ts
@@ -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(`
+
+
+
+ `);
+
+ 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();
+ });
+});
diff --git a/core/src/components/datetime/utils/comparison.ts b/core/src/components/datetime/utils/comparison.ts
index 22a6fff16f..2e64fb5776 100644
--- a/core/src/components/datetime/utils/comparison.ts
+++ b/core/src/components/datetime/utils/comparison.ts
@@ -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.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.day! > compareParts.day!)
+ baseParts.day &&
+ baseParts.day > compareParts.day!)
);
};
diff --git a/core/src/components/datetime/utils/state.ts b/core/src/components/datetime/utils/state.ts
index 4519f6aaa7..238f4000d1 100644
--- a/core/src/components/datetime/utils/state.ts
+++ b/core/src/components/datetime/utils/state.ts
@@ -140,7 +140,10 @@ export const isMonthDisabled = (
* previous navigation button is disabled.
*/
export const isPrevMonthDisabled = (refParts: DatetimeParts, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
- const prevMonth = getPreviousMonth(refParts);
+ const prevMonth = {
+ ...getPreviousMonth(refParts),
+ day: null,
+ };
return isMonthDisabled(prevMonth, {
minParts,
maxParts,
@@ -152,7 +155,10 @@ export const isPrevMonthDisabled = (refParts: DatetimeParts, minParts?: Datetime
* determine if the next navigation button is disabled.
*/
export const isNextMonthDisabled = (refParts: DatetimeParts, maxParts?: DatetimeParts) => {
- const nextMonth = getNextMonth(refParts);
+ const nextMonth = {
+ ...getNextMonth(refParts),
+ day: null,
+ };
return isMonthDisabled(nextMonth, {
maxParts,
});
diff --git a/core/src/utils/test/playwright/page/utils/goto.ts b/core/src/utils/test/playwright/page/utils/goto.ts
index 873f1461cc..e0270935d9 100644
--- a/core/src/utils/test/playwright/page/utils/goto.ts
+++ b/core/src/utils/test/playwright/page/utils/goto.ts
@@ -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}`;
+ testInfo.annotations.push({
+ type: 'mode',
+ description: formattedMode,
+ });
+
+ if (rtl) {
+ testInfo.annotations.push({
+ type: 'rtl',
+ description: 'true',
+ });
+ }
+
const result = await Promise.all([
page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }),
originalFn(formattedUrl),
diff --git a/core/src/utils/test/playwright/page/utils/index.ts b/core/src/utils/test/playwright/page/utils/index.ts
index cccfbd8093..4bef8cfc41 100644
--- a/core/src/utils/test/playwright/page/utils/index.ts
+++ b/core/src/utils/test/playwright/page/utils/index.ts
@@ -3,3 +3,4 @@ export * from './goto';
export * from './get-snapshot-settings';
export * from './set-ion-viewport';
export * from './spy-on-event';
+export * from './set-content';
diff --git a/core/src/utils/test/playwright/page/utils/set-content.ts b/core/src/utils/test/playwright/page/utils/set-content.ts
new file mode 100644
index 0000000000..a021946bb5
--- /dev/null
+++ b/core/src/utils/test/playwright/page/utils/set-content.ts
@@ -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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+ ${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 });
+};
diff --git a/core/src/utils/test/playwright/playwright-declarations.ts b/core/src/utils/test/playwright/playwright-declarations.ts
index 70a0f154d4..11a93886d0 100644
--- a/core/src/utils/test/playwright/playwright-declarations.ts
+++ b/core/src/utils/test/playwright/playwright-declarations.ts
@@ -43,12 +43,6 @@ export interface E2EPage extends Page {
* we need to wait until the changes have been applied to the DOM.
*/
waitForChanges: (timeoutMs?: number) => Promise;
- /**
- * 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;
-
/**
* Creates a new EventSpy and listens
* on the window for an event.
diff --git a/core/src/utils/test/playwright/playwright-page.ts b/core/src/utils/test/playwright/playwright-page.ts
index 870cb7fe99..97a7f17200 100644
--- a/core/src/utils/test/playwright/playwright-page.ts
+++ b/core/src/utils/test/playwright/playwright-page.ts
@@ -8,7 +8,14 @@ import type {
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 {
+ getSnapshotSettings,
+ goto as goToPage,
+ setContent,
+ setIonViewport,
+ spyOnEvent,
+ waitForChanges,
+} from './page/utils';
import type { E2EPage } from './playwright-declarations';
type CustomTestArgs = PlaywrightTestArgs &
@@ -28,6 +35,7 @@ export const test = base.extend({
// Overridden Playwright methods
page.goto = (url: string) => goToPage(page, url, testInfo, originalGoto);
+ page.setContent = (html: string) => setContent(page, html);
// Custom Ionic methods
page.getSnapshotSettings = () => getSnapshotSettings(page, testInfo);
page.setIonViewport = () => setIonViewport(page);