v2 installed two globals and the fake replaced both. v3 has neither — the runtime is an npm module and the generated bindings call into it. What it has instead is better: setTransport() is a public seam for replacing the IPC transport, and *every* runtime call goes through it, so the fake is smaller than v2's and covers strictly more. The event dispatcher is no longer mirrored at all. v2's fake reimplemented desktop/events.js — the listener list, maxCallbacks expiry, the reverse iteration — because there was no way to reach the real one; emit() now goes through window._wails.dispatchWailsEvent, which is the entry point the backend's own push uses. What is mirrored instead is one line of Go: how EventManager.Emit packs variadic data into an event's single data field. Registration and unregistration are the public Events API. The one non-public thing left is the listener registry, aliased in vitest.config.mts and used only by listenerNames() — a test asks whether importing a store subscribed it, which nothing public can answer. A binding carries a method ID, not a name, so the fake derives the ID -> path map from the generated tree: FNV-1a over the FQN, with the Go type's casing recovered from each package's index.ts, which is the only place it survives (library/library.ts cannot tell you it is FrontendUtil). The map has to be complete rather than lazy because 21 assertions read calls() with no argument and compare the whole list. Two things had to move that are not the fake. fixture() drains microtasks between two renders: a v3 binding settles several hops later than v2's, and tests were already written as though fixture() meant "mounted and loaded". Microtasks and not a timer, which would hang under the suites that install fake ones. tracklist-store keeps its defaults on an empty answer instead of emptying the column list. GetTrackListColumns substitutes DefaultColumns only when the whole config section is missing; a section that exists with no columns returns nothing. Until now this was accidental — the binding was typed Column[], an absent answer arrived as undefined, and .map threw into the catch. 757 tests pass across all 63 files. They are run in batches: a single browser session dies partway through the 58 it queues, which reproduces unchanged at the pre-migration commit and is a resource limit on this machine rather than anything here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
/**
|
|
* Self-tests for the component tier, in the spirit of e2e/specs/
|
|
* harness.spec.ts: prove the rig is what it claims before trusting a
|
|
* single assertion built on it.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { Events } from '../src/events';
|
|
import { emit, calls, wails, flush } from '@test/support/harness';
|
|
// Importing a store must be enough to make it start listening.
|
|
import { queueStore } from '@store/queue-store';
|
|
|
|
describe('component-tier harness', () => {
|
|
it('runs in a real browser with a real shadow DOM', () => {
|
|
const host = document.createElement('div');
|
|
|
|
host.attachShadow({ mode: 'open' }).innerHTML = '<b>x</b>';
|
|
|
|
expect(host.shadowRoot?.querySelector('b')?.textContent).toBe('x');
|
|
});
|
|
|
|
it('routes generated bindings through the fake, not a module mock', async () => {
|
|
// The import path under test is the real generated stub, which does
|
|
// window['go']['queue']['Queue']['GetState']().
|
|
const Queue = await import('@go/queue/queue.js');
|
|
|
|
wails.stub('queue.Queue.GetState', { currentIndex: 4 });
|
|
|
|
await expect(Queue.GetState()).resolves.toEqual({ currentIndex: 4 });
|
|
expect(calls('queue.Queue.GetState')).toHaveLength(1);
|
|
});
|
|
|
|
it('resolves an unstubbed binding instead of hanging', async () => {
|
|
const Queue = await import('@go/queue/queue.js');
|
|
|
|
// The real trap this pays for is the reverse: a *real* binding
|
|
// called with wrong argument types never settles. Here, silence is
|
|
// an immediate undefined so a test fails on the assertion rather
|
|
// than on a timeout.
|
|
await expect(Queue.Play()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('registers listeners merely by importing a store', () => {
|
|
expect(wails.listenerNames()).toContain(Events.QueueChanged);
|
|
});
|
|
|
|
it('delivers events to store listeners with their payload', () => {
|
|
emit(Events.QueueIndexChanged, { currentIndex: 11 });
|
|
|
|
expect(queueStore.getState().currentIndex).toBe(11);
|
|
});
|
|
|
|
it('expires a once-listener after a single delivery', () => {
|
|
let fired = 0;
|
|
|
|
wails.on('SyntheticEvent', () => {
|
|
fired += 1;
|
|
}, 1);
|
|
|
|
wails.notify('SyntheticEvent', []);
|
|
wails.notify('SyntheticEvent', []);
|
|
|
|
expect(fired).toBe(1);
|
|
});
|
|
|
|
it('returns a frontend emit to in-page listeners, one round trip later', async () => {
|
|
// v2 notified JS listeners *before* Go, so a frontend emit was
|
|
// observable synchronously. v3's Events.Emit does not touch the
|
|
// local registry at all — it calls the backend, and
|
|
// EventProcessor.Emit sends the event back out to every window,
|
|
// including the one that emitted it. So the page still sees it,
|
|
// just not on the same tick.
|
|
const { Events } = await import('@wailsio/runtime');
|
|
let seen: unknown;
|
|
|
|
wails.on('SyntheticEmit', (data) => {
|
|
seen = data;
|
|
}, -1);
|
|
|
|
Events.Emit('SyntheticEmit', 42);
|
|
|
|
expect(seen).toBeUndefined();
|
|
|
|
await flush();
|
|
|
|
expect(seen).toBe(42);
|
|
});
|
|
|
|
it('flushes the microtask queue stores notify on', async () => {
|
|
let notified = false;
|
|
|
|
const off = queueStore.subscribe(() => {
|
|
notified = true;
|
|
});
|
|
|
|
emit(Events.QueueIndexChanged, { currentIndex: 1 });
|
|
const beforeFlush = notified;
|
|
|
|
await flush();
|
|
off();
|
|
|
|
expect([beforeFlush, notified]).toEqual([false, true]);
|
|
});
|
|
});
|