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 | void) => { return mount( defineComponent({ setup, template: '
', }), ); }; describe(broadcastedRef, () => { let component: ReturnType; 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'); }); });