rdesign/frontend/node_modules/signal-polyfill/tests/Signal/subtle/watcher.test.ts

194 lines
5.6 KiB
TypeScript

import {afterEach, describe, expect, it, vi} from 'vitest';
import {Signal} from '../../../src/wrapper.js';
describe('Watcher', () => {
type Destructor = () => void;
const notifySpy = vi.fn();
const watcher = new Signal.subtle.Watcher(() => {
notifySpy();
});
function effect(cb: () => Destructor | void): () => void {
let destructor: Destructor | void;
const c = new Signal.Computed(() => (destructor = cb()));
watcher.watch(c);
c.get();
return () => {
destructor?.();
watcher.unwatch(c);
};
}
function flushPending() {
for (const signal of watcher.getPending()) {
signal.get();
}
expect(watcher.getPending()).toStrictEqual([]);
}
afterEach(() => watcher.unwatch(...Signal.subtle.introspectSources(watcher)));
it('should work', () => {
const watchedSpy = vi.fn();
const unwatchedSpy = vi.fn();
const stateSignal = new Signal.State(1, {
[Signal.subtle.watched]: watchedSpy,
[Signal.subtle.unwatched]: unwatchedSpy,
});
stateSignal.set(100);
stateSignal.set(5);
const computedSignal = new Signal.Computed(() => stateSignal.get() * 2);
let calls = 0;
let output = 0;
let computedOutput = 0;
// Ensure the call backs are not called yet
expect(watchedSpy).not.toHaveBeenCalled();
expect(unwatchedSpy).not.toHaveBeenCalled();
// Expect the watcher to not have any sources as nothing has been connected yet
expect(Signal.subtle.introspectSources(watcher)).toHaveLength(0);
expect(Signal.subtle.introspectSinks(computedSignal)).toHaveLength(0);
expect(Signal.subtle.introspectSinks(stateSignal)).toHaveLength(0);
expect(Signal.subtle.hasSinks(stateSignal)).toEqual(false);
const destructor = effect(() => {
output = stateSignal.get();
computedOutput = computedSignal.get();
calls++;
return () => {};
});
// The signal is now watched
expect(Signal.subtle.hasSinks(stateSignal)).toEqual(true);
// Now that the effect is created, there will be a source
expect(Signal.subtle.introspectSources(watcher)).toHaveLength(1);
expect(Signal.subtle.introspectSinks(computedSignal)).toHaveLength(1);
// Note: stateSignal has more sinks because one is for the computed signal and one is the effect.
expect(Signal.subtle.introspectSinks(stateSignal)).toHaveLength(2);
// Now the watched callback should be called
expect(watchedSpy).toHaveBeenCalled();
expect(unwatchedSpy).not.toHaveBeenCalled();
// It should not have notified yet
expect(notifySpy).not.toHaveBeenCalled();
stateSignal.set(10);
// After a signal has been set, it should notify
expect(notifySpy).toHaveBeenCalled();
// Initially, the effect should not have run
expect(calls).toEqual(1);
expect(output).toEqual(5);
expect(computedOutput).toEqual(10);
flushPending();
// The effect should run, and thus increment the value
expect(calls).toEqual(2);
expect(output).toEqual(10);
expect(computedOutput).toEqual(20);
// Kicking it off again, the effect should run again
watcher.watch();
stateSignal.set(20);
expect(watcher.getPending()).toHaveLength(1);
flushPending();
// After a signal has been set, it should notify again
expect(notifySpy).toHaveBeenCalledTimes(2);
expect(calls).toEqual(3);
expect(output).toEqual(20);
expect(computedOutput).toEqual(40);
Signal.subtle.untrack(() => {
// Untrack doesn't affect set, only get
stateSignal.set(999);
expect(calls).toEqual(3);
flushPending();
expect(calls).toEqual(4);
});
// Destroy and un-subscribe
destructor();
// Since now it is un-subscribed, it should now be called
expect(unwatchedSpy).toHaveBeenCalled();
// We can confirm that it is un-watched by checking it
expect(Signal.subtle.hasSinks(stateSignal)).toEqual(false);
// Since now it is un-subscribed, this should have no effect now
stateSignal.set(200);
flushPending();
// Make sure that effect is no longer running
// Everything should stay the same
expect(calls).toEqual(4);
expect(output).toEqual(999);
expect(computedOutput).toEqual(1998);
expect(watcher.getPending()).toHaveLength(0);
// Adding any other effect after an unwatch should work as expected
const destructor2 = effect(() => {
output = stateSignal.get();
return () => {};
});
stateSignal.set(300);
flushPending();
});
it('provides `this` to notify as normal function', () => {
const mockGetPending = vi.fn();
const watcher = new Signal.subtle.Watcher(function () {
this.getPending();
});
watcher.getPending = mockGetPending;
const signal = new Signal.State<number>(0);
watcher.watch(signal);
signal.set(1);
expect(mockGetPending).toBeCalled();
});
it('can be closed in if needed in notify as an arrow function', () => {
const mockGetPending = vi.fn();
const watcher = new Signal.subtle.Watcher(() => {
watcher.getPending();
});
watcher.getPending = mockGetPending;
const signal = new Signal.State<number>(0);
watcher.watch(signal);
signal.set(1);
expect(mockGetPending).toBeCalled();
});
it('should not break a computed signal to watch it before getting its value', () => {
const signal = new Signal.State(0);
const computedSignal = new Signal.Computed(() => signal.get());
const watcher = new Signal.subtle.Watcher(() => {});
expect(computedSignal.get()).toBe(0);
signal.set(1);
watcher.watch(computedSignal);
expect(computedSignal.get()).toBe(1);
watcher.unwatch(computedSignal);
expect(computedSignal.get()).toBe(1);
});
});