test: replace jest with vitest (#10472)

* chore: vitest config

* feat: vitest

* fix: do not actually create ws

* chore: config

* chore: lockfile

* chore: revert downgrade, up node

* chore: package - 'git add -A'

* chore: delete mock-socket

* chore: delete mock-socket

* fix: lockfile

---------

Co-authored-by: almeidx <github@almeidx.dev>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
pat
2024-10-07 01:26:53 +11:00
committed by GitHub
parent bb04e09f8b
commit 24128a3c45
21 changed files with 267 additions and 219 deletions

View File

@@ -1 +0,0 @@
export { WebSocket as default } from 'mock-socket';

View File

@@ -5,6 +5,7 @@ import { Buffer } from 'node:buffer';
import { once } from 'node:events';
import process from 'node:process';
import { Readable } from 'node:stream';
import { describe, test, expect, vitest, type Mock, beforeEach, afterEach } from 'vitest';
import { addAudioPlayer, deleteAudioPlayer } from '../src/DataStore';
import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
import type { AudioPlayer } from '../src/audio/AudioPlayer';
@@ -13,14 +14,31 @@ import { AudioPlayerError } from '../src/audio/AudioPlayerError';
import { AudioResource } from '../src/audio/AudioResource';
import { NoSubscriberBehavior } from '../src/index';
jest.mock('../src/DataStore');
jest.mock('../src/VoiceConnection');
jest.mock('../src/audio/AudioPlayerError');
vitest.mock('../src/DataStore', () => {
return {
addAudioPlayer: vitest.fn(),
deleteAudioPlayer: vitest.fn(),
};
});
const addAudioPlayerMock = addAudioPlayer as unknown as jest.Mock<typeof addAudioPlayer>;
const deleteAudioPlayerMock = deleteAudioPlayer as unknown as jest.Mock<typeof deleteAudioPlayer>;
const AudioPlayerErrorMock = AudioPlayerError as unknown as jest.Mock<typeof AudioPlayerError>;
const VoiceConnectionMock = VoiceConnection as unknown as jest.Mock<VoiceConnection>;
vitest.mock('../src/VoiceConnection', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importOriginal<typeof import('../src/VoiceConnection')>();
const VoiceConnection = vitest.fn();
VoiceConnection.prototype.setSpeaking = vitest.fn();
VoiceConnection.prototype.dispatchAudio = vitest.fn();
VoiceConnection.prototype.prepareAudioPacket = vitest.fn();
return {
...actual,
VoiceConnection,
};
});
vitest.mock('../src/audio/AudioPlayerError', () => {
return {
AudioPlayerError: vitest.fn(),
};
});
function* silence() {
while (true) {
@@ -29,15 +47,15 @@ function* silence() {
}
function createVoiceConnectionMock() {
const connection = new VoiceConnectionMock();
const connection = new VoiceConnection({} as any, {} as any);
connection.state = {
status: VoiceConnectionStatus.Signalling,
adapter: {
sendPayload: jest.fn(),
destroy: jest.fn(),
sendPayload: vitest.fn(),
destroy: vitest.fn(),
},
};
connection.subscribe = jest.fn((player) => player['subscribe'](connection));
connection.subscribe = vitest.fn((player) => player['subscribe'](connection));
return connection;
}
@@ -57,10 +75,7 @@ async function started(resource: AudioResource) {
let player: AudioPlayer | undefined;
beforeEach(() => {
AudioPlayerErrorMock.mockReset();
VoiceConnectionMock.mockReset();
addAudioPlayerMock.mockReset();
deleteAudioPlayerMock.mockReset();
vitest.resetAllMocks();
});
afterEach(() => {
@@ -71,8 +86,8 @@ describe('State transitions', () => {
test('Starts in Idle state', () => {
player = createAudioPlayer();
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(addAudioPlayerMock).toBeCalledTimes(0);
expect(deleteAudioPlayerMock).toBeCalledTimes(0);
expect(addAudioPlayer).toBeCalledTimes(0);
expect(deleteAudioPlayer).toBeCalledTimes(0);
});
test('Playing resource with pausing and resuming', async () => {
@@ -86,11 +101,11 @@ describe('State transitions', () => {
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(player.unpause()).toEqual(false);
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(addAudioPlayerMock).toBeCalledTimes(0);
expect(addAudioPlayer).toBeCalledTimes(0);
player.play(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(addAudioPlayer).toBeCalledTimes(1);
// Expect pause() to return true and transition to paused state
expect(player.pause()).toEqual(true);
@@ -109,7 +124,7 @@ describe('State transitions', () => {
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
// The audio player should not have been deleted throughout these changes
expect(deleteAudioPlayerMock).toBeCalledTimes(0);
expect(deleteAudioPlayer).toBeCalledTimes(0);
});
test('Playing to Stopping', async () => {
@@ -122,13 +137,13 @@ describe('State transitions', () => {
player.play(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(deleteAudioPlayerMock).toBeCalledTimes(0);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(deleteAudioPlayer).toBeCalledTimes(0);
expect(player.stop()).toEqual(true);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(deleteAudioPlayerMock).toBeCalledTimes(0);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(deleteAudioPlayer).toBeCalledTimes(0);
expect(resource.silenceRemaining).toEqual(5);
});
@@ -142,8 +157,8 @@ describe('State transitions', () => {
await started(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toHaveBeenCalled();
expect(deleteAudioPlayerMock).not.toHaveBeenCalled();
expect(addAudioPlayer).toHaveBeenCalled();
expect(deleteAudioPlayer).not.toHaveBeenCalled();
});
describe('NoSubscriberBehavior transitions', () => {
@@ -188,11 +203,11 @@ describe('State transitions', () => {
player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Stop } });
player.play(resource);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(player.checkPlayable()).toEqual(true);
player['_stepPrepare']();
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(deleteAudioPlayerMock).toBeCalledTimes(1);
expect(deleteAudioPlayer).toBeCalledTimes(1);
});
});
@@ -217,7 +232,7 @@ describe('State transitions', () => {
player.play(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(player.checkPlayable()).toEqual(true);
// Run through a few packet cycles
@@ -241,7 +256,8 @@ describe('State transitions', () => {
expect(connection.dispatchAudio).toHaveBeenCalledTimes(6);
await wait();
player['_stepPrepare']();
const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock<
const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock<
[Buffer],
typeof connection.prepareAudioPacket
>;
expect(prepareAudioPacket).toHaveBeenCalledTimes(6);
@@ -251,7 +267,7 @@ describe('State transitions', () => {
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(connection.setSpeaking).toBeCalledTimes(1);
expect(connection.setSpeaking).toHaveBeenLastCalledWith(false);
expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1);
expect(deleteAudioPlayer).toHaveBeenCalledTimes(1);
});
test('stop() causes resource to use silence padding frames', async () => {
@@ -275,7 +291,7 @@ describe('State transitions', () => {
player.play(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(player.checkPlayable()).toEqual(true);
player.stop();
@@ -298,7 +314,8 @@ describe('State transitions', () => {
await wait();
expect(player.checkPlayable()).toEqual(false);
const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock<
const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock<
[Buffer],
typeof connection.prepareAudioPacket
>;
expect(prepareAudioPacket).toHaveBeenCalledTimes(5);
@@ -306,7 +323,7 @@ describe('State transitions', () => {
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(connection.setSpeaking).toBeCalledTimes(1);
expect(connection.setSpeaking).toHaveBeenLastCalledWith(false);
expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1);
expect(deleteAudioPlayer).toHaveBeenCalledTimes(1);
});
test('Plays silence 5 times for unreadable stream before quitting', async () => {
@@ -328,10 +345,11 @@ describe('State transitions', () => {
player.play(resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
expect(addAudioPlayerMock).toBeCalledTimes(1);
expect(addAudioPlayer).toBeCalledTimes(1);
expect(player.checkPlayable()).toEqual(true);
const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock<
const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock<
[Buffer],
typeof connection.prepareAudioPacket
>;
@@ -351,7 +369,7 @@ describe('State transitions', () => {
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
expect(connection.setSpeaking).toBeCalledTimes(1);
expect(connection.setSpeaking).toHaveBeenLastCalledWith(false);
expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1);
expect(deleteAudioPlayer).toHaveBeenCalledTimes(1);
});
test('checkPlayable() transitions to Idle for unreadable stream', async () => {
@@ -397,6 +415,6 @@ test('Propagates errors from streams', async () => {
const res = await once(player, 'error');
const playerError = res[0] as AudioPlayerError;
expect(playerError).toBeInstanceOf(AudioPlayerError);
expect(AudioPlayerErrorMock).toHaveBeenCalledWith(error, resource);
expect(AudioPlayerError).toHaveBeenCalledWith(error, resource);
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
});

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-promise-executor-return */
import { Buffer } from 'node:buffer';
import { describe, test, expect } from 'vitest';
import { SILENCE_FRAME } from '../src/audio/AudioPlayer';
import { AudioReceiveStream, EndBehaviorType } from '../src/receive/AudioReceiveStream';

View File

@@ -2,12 +2,12 @@ import { Buffer } from 'node:buffer';
import process from 'node:process';
import { PassThrough, Readable } from 'node:stream';
import { opus, VolumeTransformer } from 'prism-media';
import { describe, test, expect, vitest, type MockedFunction, beforeAll, beforeEach } from 'vitest';
import { SILENCE_FRAME } from '../src/audio/AudioPlayer';
import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource';
import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph';
jest.mock('prism-media');
jest.mock('../src/audio/TransformerGraph');
vitest.mock('../src/audio/TransformerGraph');
async function wait() {
// eslint-disable-next-line no-promise-executor-return
@@ -22,7 +22,7 @@ async function started(resource: AudioResource) {
return resource;
}
const findPipeline = _findPipeline as unknown as jest.MockedFunction<typeof _findPipeline>;
const findPipeline = _findPipeline as unknown as MockedFunction<typeof _findPipeline>;
beforeAll(() => {
// @ts-expect-error: No type
@@ -37,7 +37,8 @@ beforeAll(() => {
if (constraint === VOLUME_CONSTRAINT) {
base.push({
cost: 1,
transformer: () => new VolumeTransformer({} as any),
// Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer
transformer: () => new VolumeTransformer({ type: 's16le' } as any),
type: TransformerType.InlineVolume,
});
}
@@ -96,7 +97,8 @@ describe('createAudioResource', () => {
});
test('Infers from VolumeTransformer', () => {
const stream = new VolumeTransformer({} as any);
// Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer
const stream = new VolumeTransformer({ type: 's16le' } as any);
const resource = createAudioResource(stream, { inlineVolume: true });
expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT);
expect(resource.volume).toEqual(stream);

View File

@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/dot-notation */
import { GatewayOpcodes } from 'discord-api-types/v10';
import { describe, test, expect, vitest, type Mocked, beforeEach } from 'vitest';
import * as DataStore from '../src/DataStore';
import type { VoiceConnection } from '../src/VoiceConnection';
import * as _AudioPlayer from '../src/audio/AudioPlayer';
jest.mock('../src/VoiceConnection');
jest.mock('../src/audio/AudioPlayer');
vitest.mock('../src/VoiceConnection');
vitest.mock('../src/audio/AudioPlayer');
const AudioPlayer = _AudioPlayer as unknown as jest.Mocked<typeof _AudioPlayer>;
const AudioPlayer = _AudioPlayer as unknown as Mocked<typeof _AudioPlayer>;
function createVoiceConnection(joinConfig: Pick<DataStore.JoinConfig, 'group' | 'guildId'>): VoiceConnection {
return {
@@ -71,8 +72,8 @@ describe('DataStore', () => {
});
test('Managing Audio Players', async () => {
const player = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer());
const dispatchSpy = jest.spyOn(player as any, '_stepDispatch');
const prepareSpy = jest.spyOn(player as any, '_stepPrepare');
const dispatchSpy = vitest.spyOn(player as any, '_stepDispatch');
const prepareSpy = vitest.spyOn(player as any, '_stepPrepare');
expect(DataStore.hasAudioPlayer(player)).toEqual(true);
expect(DataStore.addAudioPlayer(player)).toEqual(player);
DataStore.deleteAudioPlayer(player);
@@ -87,12 +88,12 @@ describe('DataStore', () => {
test('Preparing Audio Frames', async () => {
// Test functional player
const player2 = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer());
player2['checkPlayable'] = jest.fn(() => true);
player2['checkPlayable'] = vitest.fn(() => true);
const player3 = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer());
const dispatchSpy2 = jest.spyOn(player2 as any, '_stepDispatch');
const prepareSpy2 = jest.spyOn(player2 as any, '_stepPrepare');
const dispatchSpy3 = jest.spyOn(player3 as any, '_stepDispatch');
const prepareSpy3 = jest.spyOn(player3 as any, '_stepPrepare');
const dispatchSpy2 = vitest.spyOn(player2 as any, '_stepDispatch');
const prepareSpy2 = vitest.spyOn(player2 as any, '_stepPrepare');
const dispatchSpy3 = vitest.spyOn(player3 as any, '_stepDispatch');
const prepareSpy3 = vitest.spyOn(player3 as any, '_stepPrepare');
await waitForEventLoop();
DataStore.deleteAudioPlayer(player2);
await waitForEventLoop();

View File

@@ -1,5 +1,6 @@
import { type EventEmitter, once } from 'node:events';
import process from 'node:process';
import { describe, test, expect } from 'vitest';
import { SSRCMap, type VoiceUserData } from '../src/receive/SSRCMap';
async function onceOrThrow<Emitter extends EventEmitter>(target: Emitter, event: string, after: number) {

View File

@@ -1,8 +1,9 @@
import { test, expect, vitest } from 'vitest';
import { methods } from '../src/util/Secretbox';
jest.mock('tweetnacl');
vitest.mock('tweetnacl');
test('Does not throw error with a package installed', () => {
// @ts-expect-error: Unknown type
expect(() => methods.open()).not.toThrowError();
// @ts-expect-error We are testing
expect(() => methods.open()).toThrow(TypeError);
});

View File

@@ -1,7 +1,8 @@
import { describe, test, expect, vitest } from 'vitest';
import { SpeakingMap } from '../src/receive/SpeakingMap';
import { noop } from '../src/util/util';
jest.useFakeTimers();
vitest.useFakeTimers();
describe('SpeakingMap', () => {
test('Emits start and end', () => {
@@ -17,17 +18,17 @@ describe('SpeakingMap', () => {
for (let index = 0; index < 10; index++) {
speaking.onPacket(userId);
setTimeout(noop, SpeakingMap.DELAY / 2);
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
expect(starts).toEqual([userId]);
expect(ends).toEqual([]);
}
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
expect(ends).toEqual([userId]);
speaking.onPacket(userId);
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
expect(starts).toEqual([userId, userId]);
});
});

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import { describe, test, expect } from 'vitest';
import { findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph';
const noConstraint = () => true;

View File

@@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/dot-notation */
// @ts-nocheck
import { EventEmitter } from 'node:events';
import { vitest, describe, test, expect, beforeEach } from 'vitest';
import * as _DataStore from '../src/DataStore';
import {
createVoiceConnection,
@@ -14,34 +15,42 @@ import {
} from '../src/VoiceConnection';
import * as _AudioPlayer from '../src/audio/AudioPlayer';
import { PlayerSubscription as _PlayerSubscription } from '../src/audio/PlayerSubscription';
import * as _Networking from '../src/networking/Networking';
import * as Networking from '../src/networking/Networking';
import type { DiscordGatewayAdapterLibraryMethods } from '../src/util/adapter';
jest.mock('../src/audio/AudioPlayer');
jest.mock('../src/audio/PlayerSubscription');
jest.mock('../src/DataStore');
jest.mock('../src/networking/Networking');
vitest.mock('../src/audio/AudioPlayer');
vitest.mock('../src/audio/PlayerSubscription');
vitest.mock('../src/DataStore');
vitest.mock('../src/networking/Networking', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importOriginal<typeof import('../src/networking/Networking')>();
const Networking = actual.Networking;
Networking.prototype.createWebSocket = vitest.fn();
return {
...actual,
Networking,
};
});
const DataStore = _DataStore as unknown as jest.Mocked<typeof _DataStore>;
const Networking = _Networking as unknown as jest.Mocked<typeof _Networking>;
const AudioPlayer = _AudioPlayer as unknown as jest.Mocked<typeof _AudioPlayer>;
const PlayerSubscription = _PlayerSubscription as unknown as jest.Mock<_PlayerSubscription>;
const DataStore = _DataStore as unknown as vitest.Mocked<typeof _DataStore>;
const AudioPlayer = _AudioPlayer as unknown as vitest.Mocked<typeof _AudioPlayer>;
const PlayerSubscription = _PlayerSubscription as unknown as vitest.Mock<_PlayerSubscription>;
Networking.Networking.mockImplementation(function mockedConstructor() {
this.state = {};
return this;
const _NetworkingClass = Networking.Networking;
vitest.spyOn(Networking, 'Networking').mockImplementation((...args) => {
return new _NetworkingClass(...args);
});
function createFakeAdapter() {
const sendPayload = jest.fn();
const sendPayload = vitest.fn();
sendPayload.mockReturnValue(true);
const destroy = jest.fn();
const destroy = vitest.fn();
const libMethods: Partial<DiscordGatewayAdapterLibraryMethods> = {};
return {
sendPayload,
destroy,
libMethods,
creator: jest.fn((methods) => {
creator: vitest.fn((methods) => {
Object.assign(libMethods, methods);
return {
sendPayload,
@@ -124,7 +133,7 @@ describe('createVoiceConnection', () => {
adapterCreator: existingAdapter.creator,
});
const stateSetter = jest.spyOn(existingVoiceConnection, 'state', 'set');
const stateSetter = vitest.spyOn(existingVoiceConnection, 'state', 'set');
// @ts-expect-error: We're testing
DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
@@ -163,7 +172,7 @@ describe('createVoiceConnection', () => {
reason: VoiceConnectionDisconnectReason.EndpointRemoved,
};
const rejoinSpy = jest.spyOn(existingVoiceConnection, 'rejoin');
const rejoinSpy = vitest.spyOn(existingVoiceConnection, 'rejoin');
// @ts-expect-error: We're testing
DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
@@ -222,7 +231,7 @@ describe('createVoiceConnection', () => {
describe('VoiceConnection#addServerPacket', () => {
test('Stores the packet and attempts to configure networking', () => {
const { voiceConnection } = createFakeVoiceConnection();
voiceConnection.configureNetworking = jest.fn();
voiceConnection.configureNetworking = vitest.fn();
const dummy = {
endpoint: 'discord.com',
guild_id: 123,
@@ -236,7 +245,7 @@ describe('VoiceConnection#addServerPacket', () => {
test('Overwrites existing packet', () => {
const { voiceConnection } = createFakeVoiceConnection();
voiceConnection['packets'].server = Symbol('old') as any;
voiceConnection.configureNetworking = jest.fn();
voiceConnection.configureNetworking = vitest.fn();
const dummy = {
endpoint: 'discord.com',
guild_id: 123,
@@ -250,7 +259,7 @@ describe('VoiceConnection#addServerPacket', () => {
test('Disconnects when given a null endpoint', () => {
const { voiceConnection } = createFakeVoiceConnection();
voiceConnection['packets'].server = Symbol('old') as any;
voiceConnection.configureNetworking = jest.fn();
voiceConnection.configureNetworking = vitest.fn();
const dummy = {
endpoint: null,
guild_id: 123,
@@ -344,7 +353,7 @@ describe('VoiceConnection#configureNetworking', () => {
adapter,
});
expect((voiceConnection.state as unknown as VoiceConnectionConnectingState).networking).toBeInstanceOf(
Networking.Networking,
_NetworkingClass,
);
});
});
@@ -399,24 +408,24 @@ describe('VoiceConnection#onNetworkingClose', () => {
describe('VoiceConnection#onNetworkingStateChange', () => {
test('Does nothing when status code identical', () => {
const { voiceConnection } = createFakeVoiceConnection();
const stateSetter = jest.spyOn(voiceConnection, 'state', 'set');
const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
voiceConnection['onNetworkingStateChange'](
{ code: _Networking.NetworkingStatusCode.Ready } as any,
{ code: _Networking.NetworkingStatusCode.Ready } as any,
{ code: Networking.NetworkingStatusCode.Ready } as any,
{ code: Networking.NetworkingStatusCode.Ready } as any,
);
voiceConnection['onNetworkingStateChange'](
{ code: _Networking.NetworkingStatusCode.Closed } as any,
{ code: _Networking.NetworkingStatusCode.Closed } as any,
{ code: Networking.NetworkingStatusCode.Closed } as any,
{ code: Networking.NetworkingStatusCode.Closed } as any,
);
expect(stateSetter).not.toHaveBeenCalled();
});
test('Does nothing when not in Ready or Connecting states', () => {
const { voiceConnection } = createFakeVoiceConnection();
const stateSetter = jest.spyOn(voiceConnection, 'state', 'set');
const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
const call = [
{ code: _Networking.NetworkingStatusCode.Ready } as any,
{ code: _Networking.NetworkingStatusCode.Closed } as any,
{ code: Networking.NetworkingStatusCode.Ready } as any,
{ code: Networking.NetworkingStatusCode.Closed } as any,
];
voiceConnection['_state'] = { status: VoiceConnectionStatus.Signalling } as any;
voiceConnection['onNetworkingStateChange'](call[0], call[1]);
@@ -429,7 +438,7 @@ describe('VoiceConnection#onNetworkingStateChange', () => {
test('Transitions to Ready', () => {
const { voiceConnection } = createFakeVoiceConnection();
const stateSetter = jest.spyOn(voiceConnection, 'state', 'set');
const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
voiceConnection['_state'] = {
...(voiceConnection.state as VoiceConnectionSignallingState),
status: VoiceConnectionStatus.Connecting,
@@ -437,8 +446,8 @@ describe('VoiceConnection#onNetworkingStateChange', () => {
};
voiceConnection['onNetworkingStateChange'](
{ code: _Networking.NetworkingStatusCode.Closed } as any,
{ code: _Networking.NetworkingStatusCode.Ready } as any,
{ code: Networking.NetworkingStatusCode.Closed } as any,
{ code: Networking.NetworkingStatusCode.Ready } as any,
);
expect(stateSetter).toHaveBeenCalledTimes(1);
@@ -447,7 +456,7 @@ describe('VoiceConnection#onNetworkingStateChange', () => {
test('Transitions to Connecting', () => {
const { voiceConnection } = createFakeVoiceConnection();
const stateSetter = jest.spyOn(voiceConnection, 'state', 'set');
const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set');
voiceConnection['_state'] = {
...(voiceConnection.state as VoiceConnectionSignallingState),
status: VoiceConnectionStatus.Connecting,
@@ -455,8 +464,8 @@ describe('VoiceConnection#onNetworkingStateChange', () => {
};
voiceConnection['onNetworkingStateChange'](
{ code: _Networking.NetworkingStatusCode.Ready } as any,
{ code: _Networking.NetworkingStatusCode.Identifying } as any,
{ code: Networking.NetworkingStatusCode.Ready } as any,
{ code: Networking.NetworkingStatusCode.Identifying } as any,
);
expect(stateSetter).toHaveBeenCalledTimes(1);
@@ -598,7 +607,7 @@ describe('VoiceConnection#subscribe', () => {
test('Does nothing in Destroyed state', () => {
const { voiceConnection } = createFakeVoiceConnection();
const player = new AudioPlayer.AudioPlayer();
player['subscribe'] = jest.fn();
player['subscribe'] = vitest.fn();
voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
expect(voiceConnection.subscribe(player)).toBeUndefined();
expect(player['subscribe']).not.toHaveBeenCalled();
@@ -610,7 +619,7 @@ describe('VoiceConnection#subscribe', () => {
const adapter = (voiceConnection.state as VoiceConnectionSignallingState).adapter;
const player = new AudioPlayer.AudioPlayer();
const dummy = Symbol('dummy');
player['subscribe'] = jest.fn().mockImplementation(() => dummy);
player['subscribe'] = vitest.fn().mockImplementation(() => dummy);
expect(voiceConnection.subscribe(player)).toEqual(dummy);
expect(player['subscribe']).toHaveBeenCalledWith(voiceConnection);
expect(voiceConnection.state).toMatchObject({
@@ -624,7 +633,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
test('Does nothing in Destroyed state', () => {
const { voiceConnection } = createFakeVoiceConnection();
const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
subscription.unsubscribe = jest.fn();
subscription.unsubscribe = vitest.fn();
voiceConnection.state = { status: VoiceConnectionStatus.Destroyed };
voiceConnection['onSubscriptionRemoved'](subscription);
@@ -635,7 +644,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
test('Does nothing when subscription is not the same as the stored one', () => {
const { voiceConnection } = createFakeVoiceConnection();
const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
subscription.unsubscribe = jest.fn();
subscription.unsubscribe = vitest.fn();
voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription };
voiceConnection['onSubscriptionRemoved'](Symbol('new subscription') as any);
@@ -649,7 +658,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
test('Unsubscribes in a live state with matching subscription', () => {
const { voiceConnection } = createFakeVoiceConnection();
const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer());
subscription.unsubscribe = jest.fn();
subscription.unsubscribe = vitest.fn();
voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription };
voiceConnection['onSubscriptionRemoved'](subscription);
@@ -667,7 +676,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
const oldNetworking = new Networking.Networking({} as any, false);
oldNetworking.state = {
code: _Networking.NetworkingStatusCode.Ready,
code: Networking.NetworkingStatusCode.Ready,
connectionData: {} as any,
connectionOptions: {} as any,
udp: new EventEmitter() as any,
@@ -697,7 +706,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
const oldNetworking = new Networking.Networking({} as any, false);
oldNetworking.state = {
code: _Networking.NetworkingStatusCode.Ready,
code: Networking.NetworkingStatusCode.Ready,
connectionData: {} as any,
connectionOptions: {} as any,
udp,
@@ -726,7 +735,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
const newNetworking = new Networking.Networking({} as any, false);
newNetworking.state = {
code: _Networking.NetworkingStatusCode.Ready,
code: Networking.NetworkingStatusCode.Ready,
connectionData: {} as any,
connectionOptions: {} as any,
udp: new EventEmitter() as any,
@@ -749,7 +758,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => {
describe('Adapter', () => {
test('onVoiceServerUpdate', () => {
const { adapter, voiceConnection } = createFakeVoiceConnection();
voiceConnection['addServerPacket'] = jest.fn();
voiceConnection['addServerPacket'] = vitest.fn();
const dummy = Symbol('dummy') as any;
adapter.libMethods.onVoiceServerUpdate!(dummy);
expect(voiceConnection['addServerPacket']).toHaveBeenCalledWith(dummy);
@@ -757,7 +766,7 @@ describe('Adapter', () => {
test('onVoiceStateUpdate', () => {
const { adapter, voiceConnection } = createFakeVoiceConnection();
voiceConnection['addStatePacket'] = jest.fn();
voiceConnection['addStatePacket'] = vitest.fn();
const dummy = Symbol('dummy') as any;
adapter.libMethods.onVoiceStateUpdate!(dummy);
expect(voiceConnection['addStatePacket']).toHaveBeenCalledWith(dummy);

View File

@@ -5,20 +5,27 @@ import { Buffer } from 'node:buffer';
import { once } from 'node:events';
import process from 'node:process';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import { describe, test, expect, vitest, beforeEach } from 'vitest';
import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from '../__mocks__/rtp';
import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
import { VoiceReceiver } from '../src/receive/VoiceReceiver';
import { methods } from '../src/util/Secretbox';
jest.mock('../src/VoiceConnection');
jest.mock('../src/receive/SSRCMap');
vitest.mock('../src/VoiceConnection', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importOriginal<typeof import('../src/VoiceConnection')>();
return {
...actual,
VoiceConnection: vitest.fn(),
};
});
const openSpy = jest.spyOn(methods, 'open');
vitest.mock('../src/receive/SSRCMap');
const openSpy = vitest.spyOn(methods, 'open');
openSpy.mockImplementation((buffer) => buffer);
const VoiceConnection = _VoiceConnection as unknown as jest.Mocked<typeof _VoiceConnection>;
async function nextTick() {
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => process.nextTick(resolve));
@@ -56,9 +63,9 @@ describe('VoiceReceiver', () => {
['RTP Packet Chrome', RTP_PACKET_CHROME],
['RTP Packet Android', RTP_PACKET_ANDROID],
])('onUdpMessage: %s', async (testName, RTP_PACKET) => {
receiver['decrypt'] = jest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted);
receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted);
const spy = jest.spyOn(receiver.ssrcMap, 'get');
const spy = vitest.spyOn(receiver.ssrcMap, 'get');
spy.mockImplementation(() => ({
audioSSRC: RTP_PACKET.ssrc,
userId: '123',
@@ -76,9 +83,9 @@ describe('VoiceReceiver', () => {
});
test('onUdpMessage: destroys stream on decrypt failure', async () => {
receiver['decrypt'] = jest.fn().mockImplementationOnce(() => null);
receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => null);
const spy = jest.spyOn(receiver.ssrcMap, 'get');
const spy = vitest.spyOn(receiver.ssrcMap, 'get');
spy.mockImplementation(() => ({
audioSSRC: RTP_PACKET_DESKTOP.ssrc,
userId: '123',
@@ -95,7 +102,7 @@ describe('VoiceReceiver', () => {
});
test('subscribe: only allows one subscribe stream per SSRC', () => {
const spy = jest.spyOn(receiver.ssrcMap, 'get');
const spy = vitest.spyOn(receiver.ssrcMap, 'get');
spy.mockImplementation(() => ({
audioSSRC: RTP_PACKET_DESKTOP.ssrc,
userId: '123',
@@ -107,7 +114,7 @@ describe('VoiceReceiver', () => {
describe('onWsPacket', () => {
test('CLIENT_DISCONNECT packet', () => {
const spy = jest.spyOn(receiver.ssrcMap, 'delete');
const spy = vitest.spyOn(receiver.ssrcMap, 'delete');
receiver['onWsPacket']({
op: VoiceOpcodes.ClientDisconnect,
d: {
@@ -118,7 +125,7 @@ describe('VoiceReceiver', () => {
});
test('SPEAKING packet', () => {
const spy = jest.spyOn(receiver.ssrcMap, 'update');
const spy = vitest.spyOn(receiver.ssrcMap, 'update');
receiver['onWsPacket']({
op: VoiceOpcodes.Speaking,
d: {
@@ -134,7 +141,7 @@ describe('VoiceReceiver', () => {
});
test('CLIENT_CONNECT packet', () => {
const spy = jest.spyOn(receiver.ssrcMap, 'update');
const spy = vitest.spyOn(receiver.ssrcMap, 'update');
receiver['onWsPacket']({
op: VoiceOpcodes.ClientConnect,
d: {

View File

@@ -2,12 +2,13 @@
import { Buffer } from 'node:buffer';
import { createSocket as _createSocket } from 'node:dgram';
import { EventEmitter } from 'node:events';
import { describe, test, expect, vitest, beforeEach, afterEach } from 'vitest';
import { VoiceUDPSocket } from '../src/networking/VoiceUDPSocket';
jest.mock('node:dgram');
jest.useFakeTimers();
vitest.mock('node:dgram');
vitest.useFakeTimers();
const createSocket = _createSocket as unknown as jest.Mock<typeof _createSocket>;
const createSocket = _createSocket as unknown as vitest.Mock<typeof _createSocket>;
beforeEach(() => {
createSocket.mockReset();
@@ -32,7 +33,7 @@ const VALID_RESPONSE = Buffer.from([
async function wait() {
return new Promise((resolve) => {
setImmediate(resolve);
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
});
}
@@ -48,7 +49,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
*/
test('Resolves and cleans up with a successful flow', async () => {
const fake = new FakeSocket();
fake.send = jest.fn().mockImplementation((buffer: Buffer, port: number, address: string) => {
fake.send = vitest.fn().mockImplementation((buffer: Buffer, port: number, address: string) => {
fake.emit('message', VALID_RESPONSE);
});
createSocket.mockImplementation((type) => fake as any);
@@ -71,7 +72,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
test('Waits for a valid response in an unexpected flow', async () => {
const fake = new FakeSocket();
const fakeResponse = Buffer.from([1, 2, 3, 4, 5]);
fake.send = jest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => {
fake.send = vitest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => {
fake.emit('message', fakeResponse);
await wait();
fake.emit('message', VALID_RESPONSE);
@@ -91,7 +92,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
test('Rejects if socket closes before IP discovery can be completed', async () => {
const fake = new FakeSocket();
fake.send = jest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => {
fake.send = vitest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => {
await wait();
fake.close();
});
@@ -104,7 +105,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
test('Stays alive when messages are echoed back', async () => {
const fake = new FakeSocket();
fake.send = jest.fn().mockImplementation(async (buffer: Buffer) => {
fake.send = vitest.fn().mockImplementation(async (buffer: Buffer) => {
await wait();
fake.emit('message', buffer);
});
@@ -115,7 +116,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
socket.on('close', () => (closed = true));
for (let index = 0; index < 30; index++) {
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
await wait();
}
@@ -124,7 +125,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
test('Recovers from intermittent responses', async () => {
const fake = new FakeSocket();
const fakeSend = jest.fn();
const fakeSend = vitest.fn();
fake.send = fakeSend;
createSocket.mockImplementation(() => fake as any);
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
@@ -134,7 +135,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
socket.on('close', () => (closed = true));
for (let index = 0; index < 10; index++) {
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
await wait();
}
@@ -144,7 +145,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
});
expect(closed).toEqual(false);
for (let index = 0; index < 30; index++) {
jest.advanceTimersToNextTimer();
vitest.advanceTimersToNextTimer();
await wait();
}

View File

@@ -1,6 +1,7 @@
import { type EventEmitter, once } from 'node:events';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import WS from 'jest-websocket-mock';
import { describe, test, expect, beforeEach } from 'vitest';
import WS from 'vitest-websocket-mock';
import { VoiceWebSocket } from '../src/networking/VoiceWebSocket';
beforeEach(() => {

View File

@@ -1,15 +1,16 @@
import { describe, test, expect, vitest } from 'vitest';
import { abortAfter } from '../src/util/abortAfter';
jest.useFakeTimers();
vitest.useFakeTimers();
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
const clearTimeoutSpy = vitest.spyOn(global, 'clearTimeout');
describe('abortAfter', () => {
test('Aborts after the given delay', () => {
const [ac, signal] = abortAfter(100);
expect(ac.signal).toEqual(signal);
expect(signal.aborted).toEqual(false);
jest.runAllTimers();
vitest.runAllTimers();
expect(signal.aborted).toEqual(true);
});

View File

@@ -4,13 +4,14 @@ import EventEmitter, { once } from 'node:events';
import process from 'node:process';
import { Readable } from 'node:stream';
import { opus as _opus } from 'prism-media';
import { describe, test, expect, vitest, type Mock, beforeAll, beforeEach } from 'vitest';
import { StreamType } from '../src/audio/index';
import { demuxProbe } from '../src/util/demuxProbe';
jest.mock('prism-media');
vitest.mock('prism-media');
const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>;
const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>;
const WebmDemuxer = _opus.WebmDemuxer as unknown as Mock<_opus.WebmDemuxer>;
const OggDemuxer = _opus.OggDemuxer as unknown as Mock<_opus.OggDemuxer>;
async function nextTick() {
// eslint-disable-next-line no-promise-executor-return
@@ -47,8 +48,8 @@ async function collectStream(stream: Readable): Promise<Buffer> {
}
describe('demuxProbe', () => {
const webmWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();
const oggWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();
const webmWrite: Mock<(buffer: Buffer) => void> = vitest.fn();
const oggWrite: Mock<(buffer: Buffer) => void> = vitest.fn();
beforeAll(() => {
WebmDemuxer.prototype = {

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from 'node:events';
import process from 'node:process';
import { describe, test, expect, vitest, beforeEach } from 'vitest';
import { VoiceConnectionStatus, type VoiceConnection } from '../src/VoiceConnection';
import { entersState } from '../src/util/entersState';
@@ -10,12 +11,12 @@ function createFakeVoiceConnection(status = VoiceConnectionStatus.Signalling) {
}
beforeEach(() => {
jest.useFakeTimers();
vitest.useFakeTimers();
});
describe('entersState', () => {
test('Returns the target once the state has been entered before timeout', async () => {
jest.useRealTimers();
vitest.useRealTimers();
const vc = createFakeVoiceConnection();
process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any));
const result = await entersState(vc, VoiceConnectionStatus.Ready, 1_000);
@@ -25,12 +26,12 @@ describe('entersState', () => {
test('Rejects once the timeout is exceeded', async () => {
const vc = createFakeVoiceConnection();
const promise = entersState(vc, VoiceConnectionStatus.Ready, 1_000);
jest.runAllTimers();
vitest.runAllTimers();
await expect(promise).rejects.toThrowError();
});
test('Returns the target once the state has been entered before signal is aborted', async () => {
jest.useRealTimers();
vitest.useRealTimers();
const vc = createFakeVoiceConnection();
const ac = new AbortController();
process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any));

View File

@@ -1,9 +1,10 @@
// @ts-nocheck
import { describe, test, expect, vitest, beforeAll, beforeEach } from 'vitest';
import * as VoiceConnection from '../src/VoiceConnection';
import { joinVoiceChannel } from '../src/joinVoiceChannel';
const adapterCreator = () => ({ destroy: jest.fn(), send: jest.fn() }) as any;
const createVoiceConnection = jest.spyOn(VoiceConnection, 'createVoiceConnection');
const adapterCreator = () => ({ destroy: vitest.fn(), send: vitest.fn() }) as any;
const createVoiceConnection = vitest.spyOn(VoiceConnection, 'createVoiceConnection');
beforeAll(() => {
createVoiceConnection.mockImplementation(() => null as any);

View File

@@ -1,17 +0,0 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = {
parserOpts: { strictMode: true },
sourceMaps: 'inline',
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'commonjs',
},
],
'@babel/preset-typescript',
],
};

View File

@@ -1,11 +0,0 @@
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'cobertura'],
};

View File

@@ -6,7 +6,7 @@
"scripts": {
"build": "tsc --noEmit && tsup && node scripts/postbuild.mjs",
"build:docs": "tsc -p tsconfig.docs.json",
"test": "jest --coverage",
"test": "vitest run",
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__",
"fmt": "pnpm run format",
@@ -70,27 +70,24 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@babel/core": "^7.24.6",
"@babel/preset-env": "^7.24.6",
"@babel/preset-typescript": "^7.24.6",
"@discordjs/api-extractor": "workspace:^",
"@discordjs/opus": "^0.9.0",
"@discordjs/scripts": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@types/jest": "^29.5.12",
"@types/node": "^16.18.105",
"@types/node": "18.19.45",
"@vitest/coverage-v8": "2.0.5",
"cross-env": "^7.0.3",
"esbuild-plugin-version-injector": "^1.2.1",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"jest": "^29.7.0",
"jest-websocket-mock": "^2.5.0",
"mock-socket": "^9.3.1",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"tweetnacl": "^1.0.3",
"typescript": "~5.5.4"
"typescript": "~5.5.4",
"vitest": "^2.0.5",
"vitest-websocket-mock": "^0.3.0"
},
"engines": {
"node": ">=18"