fix(planning): complete explanation and trace behavior (#46)
This commit is contained in:
1 parent
57a39c81ce
commit
c9dbe82b79
10 files changed
+925
-140
No files matched your search
@@ -112,8 +112,12 @@ const explanation: PlanningExplanation = {
|
||||
})),
|
||||
};
|
||||
|
||||
function selectedDatesForPlan(value: PlanOutcome): SelectedDates {
|
||||
function selectedDatesForPlan(value: PlanOutcome, includeHoliday = false): SelectedDates {
|
||||
const selected = new SelectedDates();
|
||||
if (includeHoliday) {
|
||||
const holiday = new Date('2024-05-08T00:00:00');
|
||||
selected.push(new SelectedDate(DayType.CLOSED_DAY, new DateRange(holiday, holiday)));
|
||||
}
|
||||
for (const placement of value.placements) {
|
||||
const date = new Date(`${placement.date}T00:00:00`);
|
||||
selected.push(new SelectedDate(DayType.CP, new DateRange(date, date)));
|
||||
@@ -145,6 +149,18 @@ function calculationFor(
|
||||
date: '2024-05-04',
|
||||
status: 'ineligible',
|
||||
reason: { code: 'non-working', message: 'Weekend' },
|
||||
historicalStatus: 'ineligible',
|
||||
},
|
||||
{
|
||||
date: '2024-05-07',
|
||||
status: 'ineligible',
|
||||
reason: { code: 'rule-invalid', message: 'Hypothetical constraint' },
|
||||
},
|
||||
{
|
||||
date: '2024-05-08',
|
||||
status: 'ineligible',
|
||||
reason: { code: 'non-working', message: 'Public Holiday' },
|
||||
historicalStatus: 'ineligible',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -276,8 +292,32 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps the simple result visible and reveals placement details on demand', async () => {
|
||||
it('shows the first plan explanation by default and navigates placements', 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').mockResolvedValue(
|
||||
counterfactualWithoutHistoricalStatus,
|
||||
);
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
@@ -292,17 +332,37 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement;
|
||||
expect(details.open).toBe(false);
|
||||
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2);
|
||||
expect(fixture.nativeElement.querySelector('button.current')).toBeNull();
|
||||
expect(fixture.nativeElement.querySelectorAll('.legend-symbol')).toHaveLength(0);
|
||||
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
|
||||
|
||||
details.open = true;
|
||||
details.dispatchEvent(new Event('toggle'));
|
||||
fixture.detectChanges();
|
||||
expect(details.open).toBe(true);
|
||||
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"]',
|
||||
@@ -404,6 +464,9 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
completedStages: ['at-risk'],
|
||||
};
|
||||
setup = calendarFor(plan, explanation, trace);
|
||||
setup.calendar.selectedDatesForPlan.mockImplementation((value) =>
|
||||
selectedDatesForPlan(value, true),
|
||||
);
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
@@ -433,6 +496,33 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
expect(fixture.componentInstance['traceState']()?.placements).toHaveLength(1);
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-06'].change).toBe('unchanged');
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-03'].change).toBe('added');
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-04']).toMatchObject({
|
||||
kind: 'ineligible',
|
||||
reason: 'Weekend',
|
||||
});
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-08']).toMatchObject({
|
||||
kind: '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');
|
||||
const holiday = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-08"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(holiday.getAttribute('aria-label')).toContain('Public Holiday');
|
||||
const legend = fixture.nativeElement.querySelector('.calendar-legend').textContent;
|
||||
expect(legend).toContain('Evaluated and rejected');
|
||||
expect(legend).toContain('Not examined');
|
||||
const unevaluatedDate = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-07"]',
|
||||
) as HTMLButtonElement;
|
||||
unevaluatedDate.click();
|
||||
fixture.detectChanges();
|
||||
expect(unevaluatedDate.classList).toContain('current');
|
||||
expect(fixture.componentInstance['liveMessage']()).toContain('Not examined');
|
||||
const afterButton = fixture.nativeElement.querySelectorAll('.trace-toolbar button')[1];
|
||||
afterButton.click();
|
||||
fixture.detectChanges();
|
||||
@@ -443,7 +533,65 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
compareButton.click();
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-04'].kind).toBe('ineligible');
|
||||
expect(fixture.componentInstance['traceReplay']()).toBe(true);
|
||||
expect(fixture.componentInstance['selectedTraceDecision']()?.id).toBe('decision:1');
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-04']).toMatchObject({
|
||||
kind: 'ineligible',
|
||||
label: 'Ineligible',
|
||||
reason: 'Weekend',
|
||||
hypotheticalLabel: 'Ineligible',
|
||||
hypotheticalReason: 'Weekend',
|
||||
});
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-03']).toMatchObject({
|
||||
kind: 'chosen',
|
||||
label: 'Chosen',
|
||||
hypotheticalLabel: 'Eligible',
|
||||
});
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-07']).toMatchObject({
|
||||
kind: 'not-examined',
|
||||
label: 'Not examined',
|
||||
hypotheticalLabel: 'Ineligible',
|
||||
});
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-08']).toMatchObject({
|
||||
kind: 'ineligible',
|
||||
label: 'Ineligible',
|
||||
reason: 'Public Holiday',
|
||||
hypotheticalLabel: 'Ineligible',
|
||||
});
|
||||
const holidayAlternative = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-08"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(holidayAlternative.querySelector('.status-marker')?.textContent).toBe('×');
|
||||
expect(holidayAlternative.querySelector('.hypothetical-marker')?.textContent).toBe('~×');
|
||||
const notExaminedAlternative = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-07"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(notExaminedAlternative.querySelector('.status-marker')?.textContent).toBe('?');
|
||||
expect(notExaminedAlternative.querySelector('.hypothetical-marker')?.textContent).toBe(
|
||||
'~×',
|
||||
);
|
||||
expect(notExaminedAlternative.getAttribute('aria-label')).toContain('Not examined');
|
||||
expect(notExaminedAlternative.getAttribute('aria-label')).toContain(
|
||||
'Date alternatives: Ineligible',
|
||||
);
|
||||
const counterfactualLegend = fixture.nativeElement.querySelector('.calendar-legend');
|
||||
expect(counterfactualLegend.textContent).toContain('~✓Eligible');
|
||||
expect(counterfactualLegend.textContent).toContain('~×Ineligible');
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-03'].change).toBe('added');
|
||||
expect(fixture.componentInstance['dateStatuses']()['2024-05-06'].change).toBe('unchanged');
|
||||
const addedDate = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-03"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(addedDate.querySelector('.status-marker')?.textContent).toBe('+');
|
||||
expect(addedDate.querySelector('.hypothetical-marker')?.textContent).toBe('~✓');
|
||||
expect(addedDate.classList).toContain('change-added');
|
||||
expect(
|
||||
(
|
||||
fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-06"]',
|
||||
) as HTMLButtonElement
|
||||
).classList,
|
||||
).toContain('change-unchanged');
|
||||
const alternativeDate = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-04"]',
|
||||
) as HTMLButtonElement;
|
||||
@@ -451,6 +599,28 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
fixture.detectChanges();
|
||||
expect(alternativeDate.classList).toContain('current');
|
||||
expect(alternativeDate.getAttribute('aria-current')).toBe('date');
|
||||
expect(alternativeDate.getAttribute('aria-label')).toContain('Ineligible');
|
||||
expect(alternativeDate.getAttribute('aria-label')).toContain(
|
||||
'Date alternatives: Ineligible',
|
||||
);
|
||||
expect(alternativeDate.getAttribute('aria-label')).toContain('Reason: Weekend');
|
||||
expect(fixture.componentInstance['liveMessage']()).toContain(
|
||||
'Date alternatives: Ineligible',
|
||||
);
|
||||
const counterfactualResult = fixture.nativeElement.querySelector(
|
||||
'.counterfactual-result',
|
||||
) as HTMLElement;
|
||||
expect(counterfactualResult.textContent).toContain('Ineligible');
|
||||
expect(counterfactualResult.textContent).toContain('Original search status: Ineligible');
|
||||
const chosenDate = fixture.nativeElement.querySelector(
|
||||
'button[data-date="2024-05-03"]',
|
||||
) as HTMLButtonElement;
|
||||
chosenDate.click();
|
||||
fixture.detectChanges();
|
||||
expect(chosenDate.getAttribute('aria-label')).toContain('Chosen');
|
||||
expect(chosenDate.getAttribute('aria-label')).toContain('Date alternatives: Eligible');
|
||||
expect(counterfactualResult.textContent).toContain('Eligible');
|
||||
expect(counterfactualResult.textContent).toContain('Original search status: Chosen');
|
||||
});
|
||||
|
||||
it('turns planner rejection into an incomplete error and clears loading', async () => {
|
||||
|
||||
Reference in new issue
Block a user