feat(planning): warn of an overrun only past the displayed range
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 22s
CI / benchmark (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 34s
CI / security (pull_request) Successful in 55s
CI / test (pull_request) Successful in 1m45s
CI / images (pull_request) Successful in 53s
CI / required (pull_request) Successful in 2s
CI / policy (push) Successful in 12s
CI / validate (push) Successful in 31s
CI / build (push) Successful in 29s
CI / benchmark (push) Successful in 1m53s
CI / security (push) Successful in 1m31s
CI / test (push) Successful in 2m12s
CI / images (push) Successful in 1m15s
CI / required (push) Successful in 3s
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 22s
CI / benchmark (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 34s
CI / security (pull_request) Successful in 55s
CI / test (pull_request) Successful in 1m45s
CI / images (pull_request) Successful in 53s
CI / required (pull_request) Successful in 2s
CI / policy (push) Successful in 12s
CI / validate (push) Successful in 31s
CI / build (push) Successful in 29s
CI / benchmark (push) Successful in 1m53s
CI / security (push) Successful in 1m31s
CI / test (push) Successful in 2m12s
CI / images (push) Successful in 1m15s
CI / required (push) Successful in 3s
The expected wait sits near the middle of recent waits, so about half of all runs pass it. Between the expected wait and the top of the range shown before Calculate, the percentage now holds at 95% with no remaining-time line; "Taking longer than expected" appears and is announced only past that top. Refs #300 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #304.
This commit is contained in:
1 parent
58be749949
commit
c6af6d45ab
4 files changed
+55
-27
No files matched your search
@@ -2062,7 +2062,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
});
|
||||
expect(text('.calculation-remaining')).toBe(remaining);
|
||||
}
|
||||
at(20_000);
|
||||
at(60_000);
|
||||
for (const [language, , , overrun] of localized) {
|
||||
TestBed.inject(TranslationService).language.set(language);
|
||||
fixture.detectChanges();
|
||||
@@ -2073,18 +2073,21 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('announces an overrun once while the overlay status keeps loading', async () => {
|
||||
it('announces an overrun once past the displayed range while the overlay status keeps loading', async () => {
|
||||
try {
|
||||
const { fixture, root, text, at } = await waitingCalendar();
|
||||
at(19_000);
|
||||
expect(text('.calculation-remaining')).toBe('About 1 s left');
|
||||
at(20_000);
|
||||
expect(text('.spinner-text')).toBe('95%');
|
||||
expect(root.querySelector('.calculation-remaining')).toBeNull();
|
||||
expect(text('.sr-only[role="status"]')).toBe('Calculating...');
|
||||
at(60_000);
|
||||
expect(text('.calculation-remaining')).toBe('Taking longer than expected');
|
||||
expect(text('.loading-overlay [role="status"]')).toBe('Calculating...');
|
||||
expect(text('.sr-only[role="status"]')).toBe('Taking longer than expected');
|
||||
fixture.componentInstance['liveMessage'].set('');
|
||||
at(60_000);
|
||||
at(120_000);
|
||||
expect(text('.spinner-text')).toBe('95%');
|
||||
expect(text('.sr-only[role="status"]')).toBe('');
|
||||
expect(root.querySelector('.cancel-calculation')).not.toBeNull();
|
||||
|
||||
@@ -220,7 +220,7 @@ describe('Indicative wait progress', () => {
|
||||
});
|
||||
expect(at(10_000)).toMatchObject({ percent: 47, overrun: false });
|
||||
expect(at(19_999)).toMatchObject({ percent: 94, overrun: false });
|
||||
expect(at(20_000)).toEqual({ percent: 95, remaining: null, overrun: true });
|
||||
expect(at(20_000)).toEqual({ percent: 95, remaining: null, overrun: false });
|
||||
expect(at(600_000)).toEqual({ percent: 95, remaining: null, overrun: true });
|
||||
progress('complete');
|
||||
expect(duration.wait()).toEqual({ percent: 100, remaining: null, overrun: false });
|
||||
@@ -229,10 +229,25 @@ describe('Indicative wait progress', () => {
|
||||
it('shows the remaining time only for expected waits of at least 3 seconds', () => {
|
||||
start(2_999);
|
||||
expect(at(1_000)).toEqual({ percent: 31, remaining: null, overrun: false });
|
||||
expect(at(2_999)).toEqual({ percent: 95, remaining: null, overrun: true });
|
||||
expect(at(2_999)).toEqual({ percent: 95, remaining: null, overrun: false });
|
||||
resolve({ kind: 'PlanningIncomplete', completedStages: [], reasons: [] });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[2_999, { minimum: 1, maximum: 9, unit: 'seconds' }, 9_000],
|
||||
[20_000, { minimum: 10, maximum: 60, unit: 'seconds' }, 60_000],
|
||||
[300_000, { minimum: 2, maximum: 15, unit: 'minutes' }, 900_000],
|
||||
])(
|
||||
'warns of an overrun of a %i ms expected wait only past the top of the range shown before Calculate',
|
||||
(expectedMs, shown, topMs) => {
|
||||
start(expectedMs);
|
||||
expect(duration.estimate()).toEqual(shown);
|
||||
expect(at(expectedMs)).toEqual({ percent: 95, remaining: null, overrun: false });
|
||||
expect(at(topMs - 1)!.overrun).toBe(false);
|
||||
expect(at(topMs)!.overrun).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('rounds the remaining time to seconds, then 5-second steps, then minutes', () => {
|
||||
start(3_000);
|
||||
expect(at(0)!.remaining).toEqual({ value: 3, unit: 'seconds' });
|
||||
|
||||
@@ -9,11 +9,24 @@ export interface PlanningDurationRange {
|
||||
unit: 'seconds' | 'minutes';
|
||||
}
|
||||
|
||||
const UNIT_MS = { seconds: 1_000, minutes: 60_000 } as const;
|
||||
|
||||
function durationRange(duration: number): PlanningDurationRange {
|
||||
const unit = duration / 2 >= 60_000 ? 'minutes' : 'seconds';
|
||||
const minimum = Math.max(1, Math.floor(duration / 2 / UNIT_MS[unit]));
|
||||
return {
|
||||
minimum,
|
||||
maximum: Math.max(minimum + 1, Math.ceil((duration * 3) / UNIT_MS[unit])),
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
/** Progress through the indicative wait of the pending calculation. */
|
||||
export interface PlanningWait {
|
||||
percent: number;
|
||||
/** Approximate remaining time; null when the expected wait is short, overrun or complete. */
|
||||
/** Approximate remaining time; null when the expected wait is short, elapsed or complete. */
|
||||
remaining: { value: number; unit: PlanningDurationRange['unit'] } | null;
|
||||
/** True past the top of the range shown before Calculate. */
|
||||
overrun: boolean;
|
||||
}
|
||||
|
||||
@@ -77,13 +90,15 @@ export class PlanningDurationService {
|
||||
private readonly expected = computed(
|
||||
() => {
|
||||
const pending = this.calculations.pending();
|
||||
return (
|
||||
pending && {
|
||||
id: pending.id,
|
||||
requestedAt: pending.requestedAt,
|
||||
ms: Math.max(1, this.expectedMs(pending.request)),
|
||||
}
|
||||
);
|
||||
if (!pending) return null;
|
||||
const ms = Math.max(1, this.expectedMs(pending.request));
|
||||
const { maximum, unit } = durationRange(ms);
|
||||
return {
|
||||
id: pending.id,
|
||||
requestedAt: pending.requestedAt,
|
||||
ms,
|
||||
overrunMs: maximum * UNIT_MS[unit],
|
||||
};
|
||||
},
|
||||
{ equal: (a, b) => a?.id === b?.id },
|
||||
);
|
||||
@@ -91,15 +106,7 @@ export class PlanningDurationService {
|
||||
readonly estimate = computed<PlanningDurationRange | null>(() => {
|
||||
const request = this.calculations.request();
|
||||
if (!request) return null;
|
||||
const duration = this.expectedMs(request);
|
||||
const unit = duration / 2 >= 60_000 ? 'minutes' : 'seconds';
|
||||
const divisor = unit === 'minutes' ? 60_000 : 1_000;
|
||||
const minimum = Math.max(1, Math.floor(duration / 2 / divisor));
|
||||
return {
|
||||
minimum,
|
||||
maximum: Math.max(minimum + 1, Math.ceil((duration * 3) / divisor)),
|
||||
unit,
|
||||
};
|
||||
return durationRange(this.expectedMs(request));
|
||||
});
|
||||
|
||||
readonly wait = computed<PlanningWait | null>(() => {
|
||||
@@ -112,7 +119,7 @@ export class PlanningDurationService {
|
||||
return {
|
||||
percent: Math.floor(95 * Math.min(1, elapsed / expected.ms)),
|
||||
remaining: expected.ms >= 3_000 && left > 0 ? remainingTime(left) : null,
|
||||
overrun: left <= 0,
|
||||
overrun: elapsed >= expected.overrunMs,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user