Skip to content

Commit 102ef28

Browse files
jasonvargaclaude
andauthored
[6.x] Fix progress bar triggering excessive recursion (#14931)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 840d093 commit 102ef28

2 files changed

Lines changed: 64 additions & 16 deletions

File tree

resources/js/composables/progress-bar.js

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref, watch } from 'vue';
1+
import { ref } from 'vue';
22
import progress from 'nprogress';
33

44
progress.configure({ showSpinner: false });
@@ -25,6 +25,8 @@ function stop() {
2525
function add(name) {
2626
if (progressNames.value.indexOf(name) == -1) {
2727
progressNames.value = [...progressNames.value, name];
28+
29+
if (!progressing.value) start();
2830
}
2931
}
3032

@@ -36,6 +38,8 @@ function remove(name) {
3638

3739
newValues.splice(i, 1);
3840
progressNames.value = newValues;
41+
42+
if (newValues.length === 0 && progressing.value) stop();
3943
}
4044

4145
function loading(name, loading) {
@@ -47,23 +51,9 @@ function count() {
4751
}
4852

4953
function isComplete() {
50-
return count() === 0;
54+
return !progressing.value;
5155
}
5256

53-
watch(
54-
names,
55-
(newNames) => {
56-
if (newNames.length > 0 && !progressing.value) {
57-
start();
58-
}
59-
60-
if (newNames.length === 0 && progressing.value) {
61-
stop();
62-
}
63-
},
64-
{ immediate: true },
65-
);
66-
6757
export default function useProgressBar() {
6858
return {
6959
loading,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { test, expect, beforeEach } from 'vitest';
2+
import { effect, nextTick } from 'vue';
3+
import useProgressBar from '@/composables/progress-bar.js';
4+
5+
const progress = useProgressBar();
6+
7+
beforeEach(() => {
8+
// Reset any leftover operations between tests.
9+
progress.names().slice().forEach((name) => progress.complete(name));
10+
});
11+
12+
test('loading is reported while operations are in flight', () => {
13+
expect(progress.isComplete()).toBe(true);
14+
15+
progress.loading('a', true);
16+
expect(progress.isComplete()).toBe(false);
17+
expect(progress.count()).toBe(1);
18+
19+
progress.loading('a', false);
20+
expect(progress.isComplete()).toBe(true);
21+
expect(progress.count()).toBe(0);
22+
});
23+
24+
test('reactive consumers are only notified on start and stop transitions', async () => {
25+
let runs = 0;
26+
let lastComplete;
27+
28+
effect(() => {
29+
lastComplete = progress.isComplete();
30+
runs++;
31+
});
32+
33+
expect(runs).toBe(1); // initial run
34+
expect(lastComplete).toBe(true);
35+
36+
// Add a large batch of operations synchronously.
37+
for (let i = 0; i < 200; i++) {
38+
progress.loading(`op-${i}`, true);
39+
}
40+
await nextTick();
41+
42+
// Only one additional run for the idle -> loading transition.
43+
expect(runs).toBe(2);
44+
expect(lastComplete).toBe(false);
45+
46+
// Remove all but the last operation. Still loading, so no new notification.
47+
for (let i = 0; i < 199; i++) {
48+
progress.loading(`op-${i}`, false);
49+
}
50+
await nextTick();
51+
expect(runs).toBe(2);
52+
53+
// Remove the final operation. Now one run for the loading -> idle transition.
54+
progress.loading('op-199', false);
55+
await nextTick();
56+
expect(runs).toBe(3);
57+
expect(lastComplete).toBe(true);
58+
});

0 commit comments

Comments
 (0)