diff --git a/tests/e2e/default/reports/sidebar-filter.wdio-spec.js b/tests/e2e/default/reports/sidebar-filter.wdio-spec.js index ad00de036e3..0dc9e89c489 100644 --- a/tests/e2e/default/reports/sidebar-filter.wdio-spec.js +++ b/tests/e2e/default/reports/sidebar-filter.wdio-spec.js @@ -197,8 +197,39 @@ describe('Reports Sidebar Filter', () => { await reportsPage.openSidebarFilter(); await reportsPage.openSidebarFilterDateAccordion(); + + // 1. Max Date parity - verify future dates are disabled + await $('#fromDateFilter').click(); + await reportsPage.waitForNepaliDatePickerDisplayed(); + + // Future dates in the current month should be disabled + const disableCells = await reportsPage.getDisabledNepaliDateCells(); + expect(disableCells.length).to.be.greaterThan(0); + + // 2. Escape Dismissal - press Escape and verify picker is dismissed + await browser.keys(['Escape']); + const picker = reportsPage.getNepaliDatePicker(); + expect(await picker.isDisplayed()).to.be.false; + + // 3. Dismiss by clicking outside (e.g. the accordion header) + await $('#fromDateFilter').click(); + expect(await picker.isDisplayed()).to.be.true; + const accordionHeader = await $('#date-filter-accordion mat-expansion-panel-header'); + await accordionHeader.click(); + expect(await picker.isDisplayed()).to.be.false; + + // 4. Select From and To dates await reportsPage.setSidebarFilterBikFromDate(); await reportsPage.setSidebarFilterBikToDate(); + + // 5. Reopen active selection verification + await $('#toDateFilter').click(); + // Verify that the active selected date is highlighted correctly + expect(await reportsPage.isNepaliDatePickerActiveCellDisplayed()).to.be.true; + + // Dismiss it + await browser.keys(['Escape']); + await commonPage.waitForPageLoaded(); expect(await reportsPage.leftPanelSelectors.allReports().length).to.equal(2); diff --git a/tests/page-objects/default/common/common.wdio.page.js b/tests/page-objects/default/common/common.wdio.page.js index db3bb30e709..365b6c533cb 100644 --- a/tests/page-objects/default/common/common.wdio.page.js +++ b/tests/page-objects/default/common/common.wdio.page.js @@ -178,7 +178,7 @@ const waitForLoaders = async (timeout = 5000) => { }); }; -const waitForAngularLoaded = async (timeout = 40000) => { +const waitForAngularLoaded = async (timeout = 60000) => { await hamburgerMenuSelectors.hamburgerMenu().waitForDisplayed({ timeout }); }; diff --git a/tests/page-objects/default/reports/reports.wdio.page.js b/tests/page-objects/default/reports/reports.wdio.page.js index 17d706cd2c8..a8aff48e7d3 100644 --- a/tests/page-objects/default/reports/reports.wdio.page.js +++ b/tests/page-objects/default/reports/reports.wdio.page.js @@ -237,6 +237,24 @@ const filterByDate = async (startDate, endDate) => { await $('#freetext').click(); // blur the datepicker }; +const clickSidebarFilterFromDate = async () => { + const el = sidebarFilterSelectors.fromDate(); + await el.waitForClickable(); + await el.click(); +}; + +const clickSidebarFilterToDate = async () => { + const el = sidebarFilterSelectors.toDate(); + await el.waitForClickable(); + await el.click(); +}; + +const clickSidebarFilterDateAccordionHeader = async () => { + const el = sidebarFilterSelectors.dateAccordionHeader(); + await el.waitForClickable(); + await el.click(); +}; + const openSidebarFilter = async () => { if (!await sidebarFilterSelectors.resetBtn().isDisplayed()) { await sidebarFilterSelectors.openBtn().click(); @@ -488,7 +506,34 @@ const waitForReportsLoaded = async (timeout) => { ); }; +const getNepaliDatePicker = () => $('.nepali-date-picker'); +const getNepaliDatePickerOverlay = () => $('.nepali-date-picker-overlay'); + +const waitForNepaliDatePickerDisplayed = async () => { + const picker = getNepaliDatePicker(); + await browser.waitUntil(async () => { + return await picker.isDisplayed(); + }, { timeout: 5000, timeoutMsg: 'Nepali date picker not displayed' }); + return picker; +}; + +const getDisabledNepaliDateCells = async () => { + const picker = getNepaliDatePicker(); + return await picker.$$('table tbody td.current-month-date.disable'); +}; + +const isNepaliDatePickerActiveCellDisplayed = async () => { + const picker = getNepaliDatePicker(); + const activeCell = picker.$('table tbody td.current-month-date.active'); + return await activeCell.isDisplayed(); +}; + module.exports = { + getNepaliDatePicker, + getNepaliDatePickerOverlay, + waitForNepaliDatePickerDisplayed, + getDisabledNepaliDateCells, + isNepaliDatePickerActiveCellDisplayed, leftPanelSelectors, rightPanelSelectors, deleteDialogSelectors, @@ -503,6 +548,9 @@ module.exports = { setSidebarFilterToDate, setSidebarFilterBikFromDate, setSidebarFilterBikToDate, + clickSidebarFilterFromDate, + clickSidebarFilterToDate, + clickSidebarFilterDateAccordionHeader, filterByForm, filterByFacility, filterByStatus, diff --git a/webapp/tests/karma/js/enketo/widgets/bikram-sambat-datepicker.spec.ts b/webapp/tests/karma/js/enketo/widgets/bikram-sambat-datepicker.spec.ts index 18309b9aa26..50943aeca5a 100644 --- a/webapp/tests/karma/js/enketo/widgets/bikram-sambat-datepicker.spec.ts +++ b/webapp/tests/karma/js/enketo/widgets/bikram-sambat-datepicker.spec.ts @@ -277,4 +277,71 @@ describe('Enketo: Bikram Sambat Datepicker Widget', () => { expect($('.nepali-date-picker').is(':visible')).to.be.true; }); + + it('highlights the correct active day corresponding to the input value when reopened', async () => { + await initWidget(); + dayInput().val('१५'); + monthInput().val('३'); + yearInput().val('२०८१'); + + // Click to open calendar + $('.calendar-btn').click(); + + // Verify the correct day (15) is highlighted with the "active" class + let activeCell = $('.nepali-date-picker table tbody td.current-month-date.active'); + expect(activeCell.text()).to.equal('१५'); + + // Close and change day to 20 + $('.nepali-date-picker-overlay').click(); + dayInput().val('२०').trigger('change'); + + // Reopen + $('.calendar-btn').click(); + + // Verify the new day (20) is highlighted with the "active" class + activeCell = $('.nepali-date-picker table tbody td.current-month-date.active'); + expect(activeCell.text()).to.equal('२०'); + }); + + it('updates the position of the calendar popup on window scroll when positioned as anchored', () => { + const { setupNepaliDatePicker } = require('../../../../../src/js/enketo/widgets/bikram-sambat-picker-shared'); + + // Set up a test input and trigger in the DOM + const html = ` +
+ + +
+ `; + $('body').append(html); + + const hiddenInput = $('#scroll-hidden-input'); + + // Call setupNepaliDatePicker with position: 'anchored' + setupNepaliDatePicker(hiddenInput, { + position: 'anchored', + closeOnDateSelect: false + }); + + // Open the picker + hiddenInput.click(); + + const picker = $('.nepali-date-picker'); + expect(picker).to.have.lengthOf(1); + + // Set initial style + picker.css({ top: '100px', left: '50px' }); + + // Simulate scroll event + const event = new Event('scroll'); + window.dispatchEvent(event); + + // Position should be updated/recalculated + expect(picker.css('top')).to.not.equal('100px'); + + // Clean up + hiddenInput.remove(); + $('#scroll-test-wrapper').remove(); + $('.nepali-date-picker').remove(); + }); }); diff --git a/webapp/tests/karma/ts/components/filters/date-filter.component.spec.ts b/webapp/tests/karma/ts/components/filters/date-filter.component.spec.ts index 5ad7c327000..a801b37727c 100644 --- a/webapp/tests/karma/ts/components/filters/date-filter.component.spec.ts +++ b/webapp/tests/karma/ts/components/filters/date-filter.component.spec.ts @@ -16,6 +16,7 @@ import { Selectors } from '@mm-selectors/index'; import { ResponsiveService } from '@mm-services/responsive.service'; import { LanguageService } from '@mm-services/language.service'; import { FormatDateService } from '@mm-services/format-date.service'; +import { toBik_dev } from 'bikram-sambat'; describe('Date Filter Component', () => { let component: DateFilterComponent; @@ -195,7 +196,8 @@ describe('Date Filter Component', () => { expect(nepaliDatePickerStub.callCount).to.equal(1); const maxDateVal = nepaliDatePickerStub.args[0][0].maxDate; - expect(maxDateVal).to.match(/^[०-९-]+$/); + const expectedMaxDate = toBik_dev(moment().clone().locale('en').format('YYYY-MM-DD')); + expect(maxDateVal).to.equal(expectedMaxDate); const hiddenInput = $('#bikram-sambat-test-wrapper .nepali-datepicker-input'); expect(hiddenInput).to.have.lengthOf(1); @@ -254,7 +256,7 @@ describe('Date Filter Component', () => { expect(setFilter.callCount).to.equal(1); const appliedDate = setFilter.args[0][0].date.from; - expect(moment(appliedDate).format('YYYY-MM-DD')).to.equal('2024-07-24'); + expect(moment(appliedDate).format('YYYY-MM-DD HH:mm:ss.SSS')).to.equal('2024-07-24 00:00:00.000'); setFilter.restore(); }); @@ -300,6 +302,133 @@ describe('Date Filter Component', () => { expect($('.nepali-date-picker-overlay')).to.have.lengthOf(0); }); + it('should maintain independent picker elements for From and To components and leave each other unchanged', () => { + // Create From component + const fixtureFrom = TestBed.createComponent(DateFilterComponent); + const compFrom = fixtureFrom.componentInstance; + compFrom.isStartDate = true; + compFrom.fieldId = 'from-test-field'; + + const htmlFrom = `
`; + document.body.insertAdjacentHTML('afterbegin', htmlFrom); + fixtureFrom.detectChanges(); + + // Create To component + const fixtureTo = TestBed.createComponent(DateFilterComponent); + const compTo = fixtureTo.componentInstance; + compTo.isStartDate = false; + compTo.fieldId = 'to-test-field'; + + const htmlTo = `
`; + document.body.insertAdjacentHTML('afterbegin', htmlTo); + fixtureTo.detectChanges(); + + // Verify each appended its own hidden input + const fromHiddenInput = $('#from-test-wrapper .nepali-datepicker-input'); + const toHiddenInput = $('#to-test-wrapper .nepali-datepicker-input'); + expect(fromHiddenInput).to.have.lengthOf(1); + expect(toHiddenInput).to.have.lengthOf(1); + + // Simulate date selection on From component + const setFilter = sinon.stub(GlobalActions.prototype, 'setFilter'); + const eventFrom = $.Event('dateSelect'); + (eventFrom as any).datePickerData = { bsYear: 2081, bsMonth: 4, bsDate: 9 }; + fromHiddenInput.trigger(eventFrom); + + // Verify From component's selection was registered + expect(setFilter.callCount).to.equal(1); + expect(setFilter.firstCall.args[0].date.from).to.not.be.undefined; + expect(setFilter.firstCall.args[0].date.to).to.be.undefined; + + // Clear call history + setFilter.resetHistory(); + + // Simulate date selection on To component + const eventTo = $.Event('dateSelect'); + (eventTo as any).datePickerData = { bsYear: 2081, bsMonth: 4, bsDate: 20 }; + toHiddenInput.trigger(eventTo); + + // Verify To component's selection was registered independently + expect(setFilter.callCount).to.equal(1); + expect(setFilter.firstCall.args[0].date.to).to.not.be.undefined; + expect(setFilter.firstCall.args[0].date.from).to.be.undefined; + + setFilter.restore(); + + // Clean up DOM + compFrom.ngOnDestroy(); + compTo.ngOnDestroy(); + $('#from-test-wrapper').remove(); + $('#to-test-wrapper').remove(); + }); + + it('clearing/resetting the date filter should set the date to undefined', () => { + component.ngAfterViewInit(); + const setFilter = sinon.stub(GlobalActions.prototype, 'setFilter'); + component.dateRange = { from: moment('2024-07-24').valueOf(), to: undefined }; + + component.clear(); + + expect(setFilter.callCount).to.equal(1); + expect(setFilter.args[0]).to.deep.equal([{ date: undefined }]); + setFilter.restore(); + }); + + it('ngOnDestroy should clean up multiple pickers and overlays completely', () => { + // Create two DOM wrappers + const html1 = `
`; + const html2 = `
`; + document.body.insertAdjacentHTML('afterbegin', html1); + document.body.insertAdjacentHTML('afterbegin', html2); + + // Instantiate and setup Component B + const fixtureB = TestBed.createComponent(DateFilterComponent); + const compB = fixtureB.componentInstance; + compB.fieldId = 'field-2'; + compB.ngOnInit(); + + // Setup Component A + component.fieldId = 'field-1'; + + // Initialize both components (which creates hidden inputs) + component.ngAfterViewInit(); + compB.ngAfterViewInit(); + + // Create the pickers and overlay in the DOM + const pickerA = $('
').appendTo('body'); + const pickerB = $('
').appendTo('body'); + $('
').appendTo('body'); + + // Bind pickers to their respective hidden inputs + $('#test-wrapper-1 .nepali-datepicker-input').data('picker', pickerA); + $('#test-wrapper-2 .nepali-datepicker-input').data('picker', pickerB); + + // 1. Destroy Component A + component.ngOnDestroy(); + + // Assert Component A's elements are removed + expect($('#test-wrapper-1 .nepali-datepicker-input')).to.have.lengthOf(0); + expect($('#picker-a')).to.have.lengthOf(0); + + // Assert Component B's elements and overlay still survive + expect($('#test-wrapper-2 .nepali-datepicker-input')).to.have.lengthOf(1); + expect($('#picker-b')).to.have.lengthOf(1); + expect($('.nepali-date-picker-overlay')).to.have.lengthOf(1); + + // 2. Destroy Component B + compB.ngOnDestroy(); + + // Assert Component B's elements and overlay are removed + expect($('#test-wrapper-2 .nepali-datepicker-input')).to.have.lengthOf(0); + expect($('#picker-b')).to.have.lengthOf(0); + expect($('.nepali-date-picker-overlay')).to.have.lengthOf(0); + + // Clean up wrapper DOM nodes + $('#test-wrapper-1').remove(); + $('#test-wrapper-2').remove(); + }); + + it('setLabel should use formatDateService.dayMonth', () => { component.isStartDate = true; component.setLabel({ from: moment('2024-07-24').valueOf(), to: undefined }); @@ -320,5 +449,6 @@ describe('Date Filter Component', () => { expect(formatDateService.dayMonth.args[0][0]).to.equal(startOfDayVal); expect(component.inputLabel).to.equal('Formatted Date'); }); + }); });