Files
tools/vue/toolkit/src/composables/reactivity/watchIgnorable/index.test.ts
T
robonen c7644ade69 fix(vue): eslint/tsconfig migration + resolve type errors
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig;
fix composable + test type errors (writable computed returns, null guards,
overload-compatible signatures, typed test helpers) — all type-level.
2026-06-07 16:29:39 +07:00

204 lines
5.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { effectScope, nextTick, ref } from 'vue';
import { watchIgnorable } from '.';
describe(watchIgnorable, () => {
it('fires the callback on normal updates (async flush)', async () => {
const count = ref(0);
const cb = vi.fn();
watchIgnorable(count, cb);
count.value = 1;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function));
});
it('suppresses the callback inside ignoreUpdates (async flush)', async () => {
const count = ref(0);
const cb = vi.fn();
const { ignoreUpdates } = watchIgnorable(count, cb);
ignoreUpdates(() => {
count.value = 1;
});
await nextTick();
expect(cb).not.toHaveBeenCalled();
count.value = 2;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(2, 1, expect.any(Function));
});
it('commits when an ignored update is followed by a real change before flush (async)', async () => {
const count = ref(0);
const cb = vi.fn();
const { ignoreUpdates } = watchIgnorable(count, cb);
ignoreUpdates(() => {
count.value = 1;
});
// A real (non-ignored) change after the ignored one: syncCounter > ignoreCounter
count.value = 2;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(2, 0, expect.any(Function));
});
it('supports multiple chained ignored updates (async)', async () => {
const count = ref(0);
const cb = vi.fn();
const { ignoreUpdates } = watchIgnorable(count, cb);
ignoreUpdates(() => {
count.value = 1;
count.value = 2;
count.value = 3;
});
await nextTick();
expect(cb).not.toHaveBeenCalled();
});
it('fires the callback on normal updates (sync flush)', () => {
const count = ref(0);
const cb = vi.fn();
watchIgnorable(count, cb, { flush: 'sync' });
count.value = 1;
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function));
});
it('suppresses the callback inside ignoreUpdates (sync flush)', () => {
const count = ref(0);
const cb = vi.fn();
const { ignoreUpdates } = watchIgnorable(count, cb, { flush: 'sync' });
ignoreUpdates(() => {
count.value = 1;
count.value = 2;
});
expect(cb).not.toHaveBeenCalled();
count.value = 3;
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(3, 2, expect.any(Function));
});
it('ignorePrevAsyncUpdates suppresses already-queued changes (async)', async () => {
const count = ref(0);
const cb = vi.fn();
const { ignorePrevAsyncUpdates } = watchIgnorable(count, cb);
count.value = 1;
// Drop the pending change before the async callback flushes
ignorePrevAsyncUpdates();
await nextTick();
expect(cb).not.toHaveBeenCalled();
});
it('ignorePrevAsyncUpdates only drops prior changes, not subsequent ones (async)', async () => {
const count = ref(0);
const cb = vi.fn();
const { ignorePrevAsyncUpdates } = watchIgnorable(count, cb);
count.value = 1;
ignorePrevAsyncUpdates();
count.value = 2;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(2, 0, expect.any(Function));
});
it('ignorePrevAsyncUpdates is a no-op for sync flush', () => {
const count = ref(0);
const cb = vi.fn();
const { ignorePrevAsyncUpdates } = watchIgnorable(count, cb, { flush: 'sync' });
count.value = 1;
expect(cb).toHaveBeenCalledTimes(1);
// Calling after a sync change does nothing
ignorePrevAsyncUpdates();
count.value = 2;
expect(cb).toHaveBeenCalledTimes(2);
});
it('stop tears down the watcher (async flush)', async () => {
const count = ref(0);
const cb = vi.fn();
const { stop } = watchIgnorable(count, cb);
stop();
count.value = 1;
await nextTick();
expect(cb).not.toHaveBeenCalled();
});
it('stop tears down the watcher (sync flush)', () => {
const count = ref(0);
const cb = vi.fn();
const { stop } = watchIgnorable(count, cb, { flush: 'sync' });
stop();
count.value = 1;
expect(cb).not.toHaveBeenCalled();
});
it('watches an array of sources', async () => {
const a = ref(0);
const b = ref('x');
const cb = vi.fn();
const { ignoreUpdates } = watchIgnorable([a, b], cb);
a.value = 1;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith([1, 'x'], [0, 'x'], expect.any(Function));
ignoreUpdates(() => {
b.value = 'y';
});
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
});
it('respects an eventFilter', async () => {
const count = ref(0);
const cb = vi.fn();
// A filter that drops every invocation
watchIgnorable(count, cb, { eventFilter: () => {} });
count.value = 1;
await nextTick();
expect(cb).not.toHaveBeenCalled();
});
it('supports the immediate option', () => {
const count = ref(5);
const cb = vi.fn();
watchIgnorable(count, cb, { immediate: true, flush: 'sync' });
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith(5, undefined, expect.any(Function));
});
it('stops with the owning effect scope', async () => {
const count = ref(0);
const cb = vi.fn();
const scope = effectScope();
scope.run(() => watchIgnorable(count, cb));
count.value = 1;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
scope.stop();
count.value = 2;
await nextTick();
expect(cb).toHaveBeenCalledTimes(1);
});
});