feat(calendar): add explained period feature and update translations

This commit is contained in:
nessar committed 2026-10-03 17:40:42 +02:00
1 parent 01c87d1e44
commit dd16db8c87
12 files changed
+134 -123

No files matched your search

@@ -498,6 +498,9 @@ describe('CalendarYear planning outcome boundary', () => {
)!;
expect(contextDate.classList).toContain('context-only');
expect(contextDate.disabled).toBe(true);
expect(contextDate.classList).toContain('explained-period');
expect(contextDate.getAttribute('aria-label')).toContain('Explained time-off period');
expect(root.querySelectorAll('button.explained-period')).toHaveLength(4);
expect(contextDate.getAttribute('aria-label')).toContain(
'Context date outside the planning horizon',
);
@@ -614,6 +617,12 @@ describe('CalendarYear planning outcome boundary', () => {
);
expect(root.querySelectorAll('button[data-date="2024-05-03"].selected')).toHaveLength(1);
expect(root.querySelectorAll('button[data-date="2024-05-06"].selected')).toHaveLength(1);
expect(
Array.from(
root.querySelectorAll<HTMLButtonElement>('button.explained-period'),
(button) => button.dataset['date'],
),
).toEqual(['2024-05-03', '2024-05-04', '2024-05-05', '2024-05-06']);
expect(root.querySelector('.technical-details')?.textContent).toContain(
'The planner’s priorities',
);
@@ -635,8 +644,9 @@ describe('CalendarYear planning outcome boundary', () => {
await fixture.whenStable();
fixture.detectChanges();
expect(document.activeElement).toBe(next);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[1]);
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(root.querySelector('[role="dialog"]')).toBeNull();
expect(root.querySelectorAll('button.explained-period')).toHaveLength(0);
const firstButton = root.querySelector<HTMLButtonElement>('button[data-step="first"]')!;
activateWithKeyboard(firstButton);
@@ -702,33 +712,8 @@ describe('CalendarYear planning outcome boundary', () => {
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2);
});
it('shows the initial placement and starts alternatives on selection without opening the dialog', async () => {
it('keeps explanation selection quiet and analyzes alternatives only from Compare dates', async () => {
const { calendar, calculation } = calendarFor(plan, explanation);
calendar.selectedDatesForPlan.mockImplementation((value) =>
selectedDatesForPlan(value, true),
);
const counterfactualWithoutHistoricalStatus: CounterfactualAnalysis = {
kind: 'Counterfactual',
calculationId: 'test',
placement: plan.placements[0],
hypothetical: true,
baseline: objectiveVector,
results: [
{ date: '2024-05-03', status: 'eligible' },
{
date: '2024-05-04',
status: 'ineligible',
},
{
date: '2024-05-08',
status: 'ineligible',
},
],
};
vi.spyOn(calculation, 'analyzeCounterfactual').mockImplementation(async (selection) => ({
...counterfactualWithoutHistoricalStatus,
placement: placementForSelection(selection),
}));
await TestBed.configureTestingModule({
imports: [CalendarYear],
providers: [
@@ -742,77 +727,65 @@ describe('CalendarYear planning outcome boundary', () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
fixture.detectChanges();
const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement;
const root = fixture.nativeElement as HTMLElement;
const details = root.querySelector<HTMLDetailsElement>('.plan-details')!;
expect(details.open).toBe(true);
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-03',
);
const placement = fixture.nativeElement.querySelector(
'button[data-date="2024-05-03"]',
) as HTMLButtonElement;
placement.click();
fixture.detectChanges();
expect(details.open).toBe(true);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]);
expect(fixture.nativeElement.querySelector('[role="dialog"]')).toBeNull();
expect(fixture.nativeElement.querySelector('.technical-details').open).toBe(false);
const compare = fixture.nativeElement.querySelector(
'.compare-dates-button',
) as HTMLButtonElement;
compare.click();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(3);
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-03',
);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]);
expect(fixture.nativeElement.textContent).toContain('Paid leave');
expect(fixture.componentInstance['dateStatuses']()['2024-05-04']).toMatchObject({
kind: 'ineligible',
label: 'Ineligible',
reason: 'Weekend',
});
expect(fixture.componentInstance['dateStatuses']()['2024-05-08']).toMatchObject({
kind: 'ineligible',
label: 'Ineligible',
reason: 'Public Holiday',
});
const weekend = fixture.nativeElement.querySelector(
'button[data-date="2024-05-04"]',
) as HTMLButtonElement;
expect(weekend.getAttribute('aria-label')).toContain('Ineligible');
expect(weekend.getAttribute('aria-label')).toContain('Weekend');
expect(weekend.querySelector('.status-marker')?.textContent).toBe('×');
expect(weekend.querySelector('.hypothetical-marker')?.textContent).toBe('~×');
const holidayButton = fixture.nativeElement.querySelector(
'button[data-date="2024-05-08"]',
) as HTMLButtonElement;
expect(holidayButton.getAttribute('aria-label')).toContain('Ineligible');
expect(holidayButton.getAttribute('aria-label')).toContain('Public Holiday');
expect(holidayButton.querySelector('.status-marker')?.textContent).toBe('×');
expect(holidayButton.querySelector('.hypothetical-marker')?.textContent).toBe('~×');
const next = fixture.nativeElement.querySelector(
'button[data-step="next"]',
) as HTMLButtonElement;
next.click();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-06',
);
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-06',
);
details.open = false;
details.dispatchEvent(new Event('toggle'));
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
expect(root.querySelector('button.current')).toBeNull();
details.open = true;
details.dispatchEvent(new Event('toggle'));
fixture.detectChanges();
root.querySelector<HTMLButtonElement>('button[data-date="2024-05-06"]')!.click();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06',
);
expect(root.querySelectorAll('.status-marker, .hypothetical-marker')).toHaveLength(0);
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click();
await fixture.whenStable();
fixture.detectChanges();
expect(calculation.analyzeCounterfactual).toHaveBeenCalledExactlyOnceWith(
plan.placements[1],
);
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
expect(root.querySelectorAll('button.selected')).toHaveLength(2);
expect(root.querySelectorAll('.status-marker, .hypothetical-marker')).toHaveLength(0);
expect(root.querySelector('.calendar-legend')?.textContent).not.toContain('Eligible');
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06',
);
const input = document.querySelector<HTMLInputElement>('.compare-date-field input')!;
input.value = '04/05/2024';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(document.querySelector('.unavailable-date')?.textContent).toContain(
'No leave is needed.',
);
expect(root.querySelector('button[data-date="2024-05-04"]')?.classList).not.toContain(
'ineligible',
);
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06',
);
document.querySelector<HTMLButtonElement>('.comparison-close')!.click();
await fixture.whenStable();
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
root.querySelector<HTMLButtonElement>('button[data-date="2024-05-03"]')!.click();
fixture.detectChanges();
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-03',
);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledTimes(1);
});
it('explains a whole break and compares another date against the current plan', async () => {
@@ -1185,7 +1158,7 @@ describe('CalendarYear planning outcome boundary', () => {
expect(root.querySelector('.plan-summary')).not.toBeNull();
});
it('announces background counterfactual failure and retries from explicit comparison', async () => {
it('announces an explicit comparison failure and retries inside the dialog', async () => {
const { calendar, calculation } = calendarFor(plan, explanation);
const firstAttempt = deferred<CounterfactualAnalysis>();
const retryAttempt = deferred<CounterfactualAnalysis>();
@@ -1212,26 +1185,30 @@ describe('CalendarYear planning outcome boundary', () => {
)!;
activateWithKeyboard(plannedDate);
fixture.detectChanges();
expect(root.querySelector('.counterfactual-progress')?.textContent).toContain(
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(document.activeElement).toBe(plannedDate);
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click();
fixture.detectChanges();
expect(document.querySelector('.comparison-loading')?.textContent).toContain(
'Analyzing every date',
);
expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain(
'Analyzing every date',
);
expect(document.activeElement).toBe(plannedDate);
firstAttempt.reject(new Error('worker unavailable'));
await fixture.whenStable();
fixture.detectChanges();
expect(
root.querySelector('.counterfactual-inline-error[role="alert"]')?.textContent,
document.querySelector(
'.counterfactual-panel .counterfactual-inline-error[role="alert"]',
)?.textContent,
).toContain('worker unavailable');
expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain(
'Date alternatives could not be calculated.',
);
expect(document.activeElement).toBe(plannedDate);
expect(root.querySelector('.counterfactual-inline-error')).toBeNull();
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click();
document.querySelector<HTMLButtonElement>('.comparison-done')!.click();
fixture.detectChanges();
expect(document.querySelector('.comparison-loading')).not.toBeNull();
const focusedBeforeResult = document.activeElement;