Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions tests/e2e/default/reports/sidebar-filter.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Nice work moving the picker query selectors into the page object. These interaction selectors are still inline though, $('#fromDateFilter') here and at :215, $('#toDateFilter') at :226, $('#date-filter-accordion mat-expansion-panel-header') at :217, and each already exists in reports.wdio.page.js (sidebarFilterSelectors.fromDate() :69, .toDate() :68, .dateAccordionHeader() :66). Since sidebarFilterSelectors is private (not exported), add small named action helpers in the page object (e.g. openBikFromDatePicker() clicking sidebarFilterSelectors.fromDate()) and call those, so no raw selector strings live in the spec and there's one place to update if the DOM changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (non-blocking): These sidebar clicks are written as raw selectors here (:202, :215, :217, :226), but the same ones already exist in the page object, tests/page-objects/default/reports/reports.wdio.page.js: fromDate() (:69), toDate() (:68), dateAccordionHeader() (:66). CHT's e2e convention keeps selectors out of specs. Since sidebarFilterSelectors is private, the tidy fix is a small action helper in the page object (e.g. an "open From picker" click) that the spec calls. Non-blocking, and you already did this correctly for the picker query selectors, this is just finishing the same pattern for the click targets.

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);
Expand Down
2 changes: 1 addition & 1 deletion tests/page-objects/default/common/common.wdio.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const waitForLoaders = async (timeout = 5000) => {
});
};

const waitForAngularLoaded = async (timeout = 40000) => {
const waitForAngularLoaded = async (timeout = 60000) => {
await hamburgerMenuSelectors.hamburgerMenu().waitForDisplayed({ timeout });
};

Expand Down
48 changes: 48 additions & 0 deletions tests/page-objects/default/reports/reports.wdio.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -503,6 +548,9 @@ module.exports = {
setSidebarFilterToDate,
setSidebarFilterBikFromDate,
setSidebarFilterBikToDate,
clickSidebarFilterFromDate,
clickSidebarFilterToDate,
clickSidebarFilterDateAccordionHeader,
filterByForm,
filterByFacility,
filterByStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div id="scroll-test-wrapper">
<input type="text" id="scroll-trigger" />
<input type="text" id="scroll-hidden-input" style="display: none;" />
</div>
`;
$('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();
});
});
134 changes: 132 additions & 2 deletions webapp/tests/karma/ts/components/filters/date-filter.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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 = `<div id="from-test-wrapper"><input id="from-test-field" /></div>`;
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 = `<div id="to-test-wrapper"><input id="to-test-field" /></div>`;
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', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): Titled "clean up multiple pickers and overlays completely," but it destroys a single component and asserts only that one hidden input is gone (#test-wrapper-1 …, :402), then hand-removes the pickers and overlay itself (:405-408). Because it deletes the very elements it should verify, it can't fail even if ngOnDestroy left them behind. It's also a weaker duplicate of the existing ngOnDestroy should clean up specific picker container and overlay test (:285), which already asserts input + picker + overlay are all removed. Either drop it (the existing test covers single-component cleanup), or make it earn its name: destroy component A → assert A's input+picker gone while B's survive and the overlay remains, then destroy B → assert B's gone and the overlay is now removed. Don't hand-delete the elements under test before asserting.

// Create two DOM wrappers
const html1 = `<div id="test-wrapper-1"><input id="field-1" /></div>`;
const html2 = `<div id="test-wrapper-2"><input id="field-2" /></div>`;
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 = $('<div class="nepali-date-picker" id="picker-a"></div>').appendTo('body');
const pickerB = $('<div class="nepali-date-picker" id="picker-b"></div>').appendTo('body');
$('<div class="nepali-date-picker-overlay"></div>').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 });
Expand All @@ -320,5 +449,6 @@ describe('Date Filter Component', () => {
expect(formatDateService.dayMonth.args[0][0]).to.equal(startOfDayVal);
expect(component.inputLabel).to.equal('Formatted Date');
});

});
});
Loading