Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions resources/js/composables/progress-bar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ref, watch } from 'vue';
import { ref } from 'vue';
import progress from 'nprogress';

progress.configure({ showSpinner: false });
Expand All @@ -25,6 +25,8 @@ function stop() {
function add(name) {
if (progressNames.value.indexOf(name) == -1) {
progressNames.value = [...progressNames.value, name];

if (!progressing.value) start();
}
}

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

newValues.splice(i, 1);
progressNames.value = newValues;

if (newValues.length === 0 && progressing.value) stop();
}

function loading(name, loading) {
Expand All @@ -47,23 +51,9 @@ function count() {
}

function isComplete() {
return count() === 0;
return !progressing.value;
}

watch(
names,
(newNames) => {
if (newNames.length > 0 && !progressing.value) {
start();
}

if (newNames.length === 0 && progressing.value) {
stop();
}
},
{ immediate: true },
);

export default function useProgressBar() {
return {
loading,
Expand Down
58 changes: 58 additions & 0 deletions resources/js/tests/ProgressBar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { test, expect, beforeEach } from 'vitest';
import { effect, nextTick } from 'vue';
import useProgressBar from '@/composables/progress-bar.js';

const progress = useProgressBar();

beforeEach(() => {
// Reset any leftover operations between tests.
progress.names().slice().forEach((name) => progress.complete(name));
});

test('loading is reported while operations are in flight', () => {
expect(progress.isComplete()).toBe(true);

progress.loading('a', true);
expect(progress.isComplete()).toBe(false);
expect(progress.count()).toBe(1);

progress.loading('a', false);
expect(progress.isComplete()).toBe(true);
expect(progress.count()).toBe(0);
});

test('reactive consumers are only notified on start and stop transitions', async () => {
let runs = 0;
let lastComplete;

effect(() => {
lastComplete = progress.isComplete();
runs++;
});

expect(runs).toBe(1); // initial run
expect(lastComplete).toBe(true);

// Add a large batch of operations synchronously.
for (let i = 0; i < 200; i++) {
progress.loading(`op-${i}`, true);
}
await nextTick();

// Only one additional run for the idle -> loading transition.
expect(runs).toBe(2);
expect(lastComplete).toBe(false);

// Remove all but the last operation. Still loading, so no new notification.
for (let i = 0; i < 199; i++) {
progress.loading(`op-${i}`, false);
}
await nextTick();
expect(runs).toBe(2);

// Remove the final operation. Now one run for the loading -> idle transition.
progress.loading('op-199', false);
await nextTick();
expect(runs).toBe(3);
expect(lastComplete).toBe(true);
});
Loading