c7644ade69
@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.
148 lines
4.0 KiB
TypeScript
148 lines
4.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { defineComponent, effectScope, nextTick, watch } from 'vue';
|
|
import { mount } from '@vue/test-utils';
|
|
import { broadcastedRef } from '.';
|
|
|
|
type MessageHandler = ((event: MessageEvent) => void) | null;
|
|
|
|
class MockBroadcastChannel {
|
|
static instances: MockBroadcastChannel[] = [];
|
|
|
|
name: string;
|
|
onmessage: MessageHandler = null;
|
|
closed = false;
|
|
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
MockBroadcastChannel.instances.push(this);
|
|
}
|
|
|
|
postMessage(data: unknown) {
|
|
if (this.closed) return;
|
|
|
|
for (const instance of MockBroadcastChannel.instances) {
|
|
if (instance !== this && instance.name === this.name && !instance.closed && instance.onmessage) {
|
|
instance.onmessage(new MessageEvent('message', { data }));
|
|
}
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.closed = true;
|
|
const index = MockBroadcastChannel.instances.indexOf(this);
|
|
if (index > -1) MockBroadcastChannel.instances.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
const mountWithRef = (setup: () => Record<string, any> | void) => {
|
|
return mount(
|
|
defineComponent({
|
|
setup,
|
|
template: '<div></div>',
|
|
}),
|
|
);
|
|
};
|
|
|
|
describe(broadcastedRef, () => {
|
|
let component: ReturnType<typeof mountWithRef>;
|
|
|
|
beforeEach(() => {
|
|
MockBroadcastChannel.instances = [];
|
|
vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
|
|
});
|
|
|
|
afterEach(() => {
|
|
component?.unmount();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('create a ref with the initial value', () => {
|
|
component = mountWithRef(() => {
|
|
const count = broadcastedRef('test-key', 42);
|
|
expect(count.value).toBe(42);
|
|
});
|
|
});
|
|
|
|
it('broadcast value changes to other channels with the same key', () => {
|
|
const ref1 = broadcastedRef('shared', 0);
|
|
const ref2 = broadcastedRef('shared', 0);
|
|
|
|
ref1.value = 100;
|
|
|
|
expect(ref2.value).toBe(100);
|
|
});
|
|
|
|
it('not broadcast to channels with a different key', () => {
|
|
const ref1 = broadcastedRef('key-a', 0);
|
|
const ref2 = broadcastedRef('key-b', 0);
|
|
|
|
ref1.value = 100;
|
|
|
|
expect(ref2.value).toBe(0);
|
|
});
|
|
|
|
it('receive values from other channels and trigger reactivity', async () => {
|
|
const callback = vi.fn();
|
|
|
|
component = mountWithRef(() => {
|
|
const data = broadcastedRef('reactive-test', 'initial');
|
|
watch(data, callback, { flush: 'sync' });
|
|
});
|
|
|
|
const sender = broadcastedRef('reactive-test', '');
|
|
sender.value = 'updated';
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
expect(callback).toHaveBeenCalledWith('updated', 'initial', expect.anything());
|
|
});
|
|
|
|
it('not broadcast initial value by default', () => {
|
|
const ref1 = broadcastedRef('no-immediate', 'first');
|
|
const ref2 = broadcastedRef('no-immediate', 'second');
|
|
|
|
expect(ref1.value).toBe('first');
|
|
expect(ref2.value).toBe('second');
|
|
});
|
|
|
|
it('broadcast initial value when immediate is true', () => {
|
|
const ref1 = broadcastedRef('immediate-test', 'existing');
|
|
broadcastedRef('immediate-test', 'new-value', { immediate: true });
|
|
|
|
expect(ref1.value).toBe('new-value');
|
|
});
|
|
|
|
it('close channel on scope dispose', () => {
|
|
const scope = effectScope();
|
|
|
|
scope.run(() => {
|
|
broadcastedRef('dispose-test', 0);
|
|
});
|
|
|
|
expect(MockBroadcastChannel.instances).toHaveLength(1);
|
|
|
|
scope.stop();
|
|
|
|
expect(MockBroadcastChannel.instances).toHaveLength(0);
|
|
});
|
|
|
|
it('handle complex object values via structured clone', () => {
|
|
const ref1 = broadcastedRef('object-test', { status: 'pending', amount: 0 });
|
|
const ref2 = broadcastedRef('object-test', { status: 'pending', amount: 0 });
|
|
|
|
ref1.value = { status: 'paid', amount: 99.99 };
|
|
|
|
expect(ref2.value).toEqual({ status: 'paid', amount: 99.99 });
|
|
});
|
|
|
|
it('fallback to a regular ref when BroadcastChannel is not available', () => {
|
|
vi.stubGlobal('BroadcastChannel', undefined);
|
|
|
|
const data = broadcastedRef('fallback', 'value');
|
|
|
|
expect(data.value).toBe('value');
|
|
|
|
data.value = 'updated';
|
|
expect(data.value).toBe('updated');
|
|
});
|
|
});
|