Testing
The player core ships a testing subpath with utilities for writing unit tests against plugins without a real browser.
Install
The testing utilities are included with the core package. No additional install is needed:
npm install @nomercy-entertainment/nomercy-player-core@beta
Import from the testing subpath:
import {
describePlugin,
assertNoListenerLeak,
} from '@nomercy-entertainment/nomercy-player-core/testing';
describePlugin
Wraps a test suite with a fresh player and registered plugin per test.
Auto-disposes in afterEach.
import {
describePlugin,
assertNoListenerLeak,
} from '@nomercy-entertainment/nomercy-player-core/testing';
import { MyPlugin } from '../src/plugins/my-plugin';
describePlugin(MyPlugin, ({ player, plugin }) => {
test('emits dataFetched after time threshold', async () => {
const events: unknown[] = [];
player.on('plugin:my-plugin:dataFetched', (event) => events.push(event));
// Simulate a player event the plugin listens to
player.emit('time', { time: 30 });
await vi.waitFor(() => events.length > 0);
expect(events[0]).toMatchObject({ count: expect.any(Number) });
});
});
describePlugin arguments:
describePlugin(
PluginClass: PluginConstructor,
fn: (ctx: { player: StubPlayer; plugin: Plugin }) => void,
opts?: {
opts?: object; // options passed to plugin.initialize()
skipLeakAssertion?: boolean;
createPlayer?: () => StubPlayer;
}
): void
assertNoListenerLeak
Verifies that a plugin’s dispose() cleans up all event listeners.
This is a required test for all plugins.
import {
assertNoListenerLeak,
createStubPlayer,
} from '@nomercy-entertainment/nomercy-player-core/testing';
import { LifecycleRegistry } from '@nomercy-entertainment/nomercy-player-core';
test('MyPlugin releases all listeners on dispose', async () => {
const player = createStubPlayer();
const plugin = new MyPlugin();
const lifecycle = new LifecycleRegistry();
await assertNoListenerLeak({
subjectId: 'my-plugin',
player,
setup: async () => {
plugin.initialize(player, {}, lifecycle);
await plugin.use();
},
teardown: () => {
plugin.dispose();
lifecycle.dispose();
},
});
});
assertNoListenerLeak snapshots the listener count before and after a setup/teardown cycle. If the count after teardown exceeds the count before setup, the test fails with details of the leak.
describePlugin already runs a leak assertion automatically after each test. Use assertNoListenerLeak directly only when you need to test a non-plugin subject or run cycles across multiple setup/teardown pairs via assertNoListenerLeakOverCycles.
Stub player
The stub player is a minimal IPlayer implementation with no browser dependencies. It:
- Supports the full event bus (
on,off,once,emit) - Supports
phase(),metrics(),queue(),time(),volume(), and all transport stubs - Does not start actual media playback
- Works in Node.js without a DOM (Vitest, Jest)
You can use the stub player directly for lower-level testing:
import { createStubPlayer } from '@nomercy-entertainment/nomercy-player-core/testing';
const player = createStubPlayer();
player.addPlugin(MyPlugin, { endpoint: 'http://test.local' });
await player.ready();
player.emit('ended', undefined);
Mock fetch
mockFetch() returns a self-contained fetch stub. Assign mock.fetch to the plugin’s fetch property to intercept outgoing requests:
import { createStubPlayer, mockFetch, describePlugin } from '@nomercy-entertainment/nomercy-player-core/testing';
describePlugin(MyPlugin, ({ plugin }) => {
test('fetches the manifest', async () => {
const mock = mockFetch();
plugin['fetch'] = mock.fetch;
mock.respondWith({ count: 5 });
await plugin.loadManifest('http://test.local/api');
expect(mock.calls[0].url).toBe('http://test.local/api');
});
});
Integration with Vitest
The core’s test suite uses Vitest. If you are using a different runner (Jest), the testing utilities still work, as the subpath has no Vitest imports.
// vitest.config.ts
export default {
test: {
environment: 'node', // or 'jsdom' for DOM-dependent plugins
},
};
For plugins that mount DOM elements (use this.mount(...)), use jsdom or happy-dom as the test environment.