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);