diff --git a/e2e/date-picker-revisit.spec.ts b/e2e/date-picker-revisit.spec.ts index 2170db2d0..6ba61fa37 100644 --- a/e2e/date-picker-revisit.spec.ts +++ b/e2e/date-picker-revisit.spec.ts @@ -152,22 +152,19 @@ test.describe('Date pickers on revisited forms', () => { listUrl: '/products/floating-rates', createButton: 'Create Floating Rate', createRole: 'button' as const, - // This form's pickers live inside a @for over rate periods, so they are created after the - // first render rather than with the page. The button and its modal then mount in the same - // change-detection pass and the button binds to nothing -- #548, which `createPickersReady` - // does not reach because the flag is already true by the time a row is added. Expected to - // fail until that is fixed; when it starts passing, drop the marker with the fix. - knownBroken: true, + // Pickers live inside a @for over rate periods (#548). Add two rows so the + // revisit assertion also covers unique ids — a shared id would bind both + // buttons to the first picker. prepare: async (page: import('@playwright/test').Page) => { await page.getByRole('button', { name: 'Add Period', exact: true }).click(); + await page.getByRole('button', { name: 'Add Period', exact: true }).click(); }, + expectUniquePickerIds: true, }, ]; - for (const { name, listUrl, createButton, createRole, prepare, knownBroken } of cases) { + for (const { name, listUrl, createButton, createRole, prepare, expectUniquePickerIds } of cases) { test(`${name} form keeps its pickers usable on every visit`, async ({ page }) => { - if (knownBroken) test.fail(); - const pickerErrors: string[] = []; page.on('console', (message) => { const text = message.text(); @@ -186,6 +183,14 @@ test.describe('Date pickers on revisited forms', () => { .poll(() => blankPickers(page), { message: `visit ${visit} left a picker unbound` }) .toBe(0); + if (expectUniquePickerIds) { + const ids = await page + .locator('ion-datetime-button') + .evaluateAll((buttons) => buttons.map((button) => button.getAttribute('datetime'))); + expect(ids.length).toBeGreaterThan(1); + expect(new Set(ids).size).toBe(ids.length); + } + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); await expect(page).toHaveURL(listUrl); } diff --git a/src/app/features/products/floating-rates/floating-rate-form.component.test.ts b/src/app/features/products/floating-rates/floating-rate-form.component.test.ts index 9192799e3..d388aeaca 100644 --- a/src/app/features/products/floating-rates/floating-rate-form.component.test.ts +++ b/src/app/features/products/floating-rates/floating-rate-form.component.test.ts @@ -25,6 +25,10 @@ import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { of } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideIonicTesting } from '../../../testing/ionic-testing'; + +const PERIOD_FROM_DATE_PICKER_0 = 'periodfromDate-picker-0'; +const PERIOD_FROM_DATE_PICKER_1 = 'periodfromDate-picker-1'; describe('FloatingRateFormComponent', () => { let component: FloatingRateFormComponent; @@ -47,6 +51,7 @@ describe('FloatingRateFormComponent', () => { { provide: Router, useValue: routerSpy }, { provide: ActivatedRoute, useValue: { paramMap: of(convertToParamMap({})) } }, provideNoopAnimations(), + provideIonicTesting(), ], }).compileComponents(); @@ -75,10 +80,10 @@ describe('FloatingRateFormComponent', () => { ); component.rate.set({ name: 'BLR', isBaseLendingRate: true, isActive: true }); component.periods.set([ - { fromDate: new Date(2026, 0, 1), interestRate: 9.5, isDifferentialToBaseLendingRate: false }, - { fromDate: new Date(2026, 1, 1), interestRate: 0, isDifferentialToBaseLendingRate: true }, + { fromDate: '2026-01-01', interestRate: 9.5, isDifferentialToBaseLendingRate: false }, + { fromDate: '2026-02-01', interestRate: 0, isDifferentialToBaseLendingRate: true }, { - fromDate: new Date(2026, 2, 1), + fromDate: '2026-03-01', interestRate: null, isDifferentialToBaseLendingRate: false, }, @@ -104,7 +109,7 @@ describe('FloatingRateFormComponent', () => { component.isEditMode.set(true); component.rate.set({ name: 'Updated Rate', isBaseLendingRate: false, isActive: true }); component.periods.set([ - { fromDate: new Date(2026, 5, 1), interestRate: 12, isDifferentialToBaseLendingRate: false }, + { fromDate: '2026-06-01', interestRate: 12, isDifferentialToBaseLendingRate: false }, ]); component.onSubmit(); @@ -116,4 +121,35 @@ describe('FloatingRateFormComponent', () => { expect(arg.ratePeriods?.length).toBe(1); expect(arg.ratePeriods?.[0].interestRate).toBe(12); }); + + it('gives each period row a unique picker id and stamps it on the button', async () => { + component.periods.set([ + { fromDate: '2026-01-01', interestRate: 9.5, isDifferentialToBaseLendingRate: false }, + { fromDate: '2026-02-01', interestRate: 10, isDifferentialToBaseLendingRate: false }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.periodFromDatePickerId(0)).toBe(PERIOD_FROM_DATE_PICKER_0); + expect(component.periodFromDatePickerId(1)).toBe(PERIOD_FROM_DATE_PICKER_1); + + const buttons: HTMLElement[] = Array.from( + fixture.nativeElement.querySelectorAll('ion-datetime-button'), + ); + expect(buttons).toHaveLength(2); + expect(buttons[0].getAttribute('datetime')).toBe(PERIOD_FROM_DATE_PICKER_0); + expect(buttons[1].getAttribute('datetime')).toBe(PERIOD_FROM_DATE_PICKER_1); + expect(buttons[0].getAttribute('datetime')).not.toBe(buttons[1].getAttribute('datetime')); + + const pickers: HTMLElement[] = Array.from( + fixture.nativeElement.querySelectorAll('ion-datetime'), + ); + const pickerIds = pickers.map((el) => el.id); + if (pickerIds.length > 0) { + expect(pickerIds).toEqual([PERIOD_FROM_DATE_PICKER_0, PERIOD_FROM_DATE_PICKER_1]); + } + expect(fixture.nativeElement.querySelector('#periodfromDate-picker')).toBeNull(); + }); }); diff --git a/src/app/features/products/floating-rates/floating-rate-form.component.ts b/src/app/features/products/floating-rates/floating-rate-form.component.ts index 995927c0c..4390ce382 100644 --- a/src/app/features/products/floating-rates/floating-rate-form.component.ts +++ b/src/app/features/products/floating-rates/floating-rate-form.component.ts @@ -34,7 +34,6 @@ import { IonCardTitle, IonCheckbox, IonDatetime, - IonDatetimeButton, IonIcon, IonInput, IonItem, @@ -46,12 +45,13 @@ import { formatDateToFineract, FINERACT_DATE_FORMAT, FINERACT_LOCALE, + toIsoDate, } from '../../../core/utils/date-formatter'; -import { createPickersReady } from '../../../shared/utils/pickers-ready'; +import { DeferredDatetimeButtonComponent } from '../../../ui/deferred-datetime-button/deferred-datetime-button.component'; /** A single editable rate period row in the form. */ interface RatePeriodRow { - fromDate: Date; + fromDate: string; interestRate: number | null; isDifferentialToBaseLendingRate: boolean; } @@ -77,8 +77,8 @@ interface RatePeriodRow { IonCard, IonCheckbox, IonDatetime, - IonDatetimeButton, IonModal, + DeferredDatetimeButtonComponent, ], template: `