diff --git a/src/utilities/watch.js b/src/utilities/watch.js index 1ad48ff..6618cc9 100644 --- a/src/utilities/watch.js +++ b/src/utilities/watch.js @@ -22,6 +22,10 @@ export function watch(get_value, callback, options = null) { } if (initialized || (options?.immediate ?? true)) { + // Keep a single pending callback: it reports the latest value anyway, + // and the disposer cancels only the timer it knows about. + clearTimeout(timer_id); + // Prevent the watcher from detecting its own dependencies. timer_id = setTimeout(() => { callback(new_value, old_value); diff --git a/tests/utilities/watch.test.js b/tests/utilities/watch.test.js new file mode 100644 index 0000000..3e29a58 --- /dev/null +++ b/tests/utilities/watch.test.js @@ -0,0 +1,70 @@ +import { describe, expect, test } from "vitest"; +import { watch } from "@/utilities/watch"; +import Alpine from "alpinejs"; + +// watch takes Alpine from the global scope +globalThis.Alpine = Alpine; + +// Lets Alpine run the scheduled effects, but not the timers the watcher sets +const flush_effects = () => new Promise(queueMicrotask); + +// Lets the timers set so far fire; the effects must be flushed first, +// otherwise the timers they set would fire after this one +const flush_timers = () => new Promise(resolve => setTimeout(resolve)); + +describe("watch", () => { + test("reports changes made before the callback once", async () => { + const data = Alpine.reactive({ value: 0 }); + const tracing = []; + + watch( + () => data.value, + (value, old_value) => tracing.push([value, old_value]), + { immediate: false }); + + data.value = 1; + await flush_effects(); + + data.value = 2; + await flush_effects(); + await flush_timers(); + + expect(tracing).toEqual([[2, 0]]); + }); + + test("immediate: reports a change made before the first callback once", async () => { + const data = Alpine.reactive({ value: 0 }); + const tracing = []; + + watch( + () => data.value, + (value, old_value) => tracing.push([value, old_value])); + + data.value = 1; + await flush_effects(); + await flush_timers(); + + expect(tracing).toEqual([[1, 0]]); + }); + + test("no callback after disposal", async () => { + const data = Alpine.reactive({ value: 0 }); + const tracing = []; + + const dispose = watch( + () => data.value, + (value, old_value) => tracing.push([value, old_value]), + { immediate: false }); + + data.value = 1; + await flush_effects(); + + data.value = 2; + await flush_effects(); + + dispose(); + await flush_timers(); + + expect(tracing).toEqual([]); + }); +}); diff --git a/vitest.config.js b/vitest.config.js index 6a9c657..ead4bc1 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -8,6 +8,9 @@ import { } from "url"; export default defineConfig({ + define: { + __DEV__: true + }, test: { globals: true, environment: "jsdom",