diff --git a/src/features/core/index.ts b/src/features/core/index.ts index adf08c14..d69b76c4 100644 --- a/src/features/core/index.ts +++ b/src/features/core/index.ts @@ -1,9 +1,9 @@ export { Client } from './internal/Client/index.js'; export { - Voice, getJoinableStateStatus, JOINABLE_STATE_STATUS, getActorConnectionState, type ConnectionState, getVoiceInstance, } from './internal/Voice/index.js'; +export type { Voice } from './internal/Voice/index.js'; diff --git a/src/features/core/internal/Voice/index.ts b/src/features/core/internal/Voice/index.ts index a8cd5dcf..ad4d43c6 100644 --- a/src/features/core/internal/Voice/index.ts +++ b/src/features/core/internal/Voice/index.ts @@ -1,7 +1,14 @@ -export { Voice, getVoiceInstance } from './internal/Voice.class.js'; +import { singleton } from '@/features/others/singleton/index.js'; + +import { Voice } from './internal/Voice.class.js'; + +export type { Voice } from './internal/Voice.class.js'; export type { ConnectionState } from './internal/Voice.types.js'; export { getJoinableStateStatus, JOINABLE_STATE_STATUS, } from './internal/funcs/getJoinableStateStatus/index.js'; export { getActorConnectionState } from './internal/funcs/getActorConnectionState/index.js'; + +const createVoiceInstance = () => new Voice(); +export const getVoiceInstance = singleton(createVoiceInstance); diff --git a/src/features/core/internal/Voice/internal/Voice.class.spec.ts b/src/features/core/internal/Voice/internal/Voice.class.spec.ts index 42d89403..cfc41f99 100644 --- a/src/features/core/internal/Voice/internal/Voice.class.spec.ts +++ b/src/features/core/internal/Voice/internal/Voice.class.spec.ts @@ -171,6 +171,59 @@ describe('Voice', () => { ); }); + it('should throw an error if the channel is null', async () => { + const mockedInteraction = { + reply: vi.fn(), + } as unknown as Readonly; + + (getActorId as MockedFunction).mockResolvedValueOnce({ + voice: { channel: null }, + } as GuildMember); + ( + getJoinableStateStatus as MockedFunction + ).mockReturnValueOnce(JOINABLE_STATE_STATUS.ALREADY_JOINED); + + await expect( + voice.join({ interaction: mockedInteraction }) + ).rejects.toThrowError( + 'Channel is null. Please check getJoinableStateStatus()' + ); + }); + + // JOINABLE_STATE_STATUS.ALREADY_JOINED + it('should return false if the channel is already joined', async () => { + const mockedInteraction = { + reply: vi.fn(), + } as unknown as Readonly; + + (getActorId as MockedFunction).mockResolvedValueOnce({ + voice: { + channel: { + id: 'channelId', + guild: { + id: 'guildId', + }, + }, + }, + } as GuildMember); + ( + getJoinableStateStatus as MockedFunction + ).mockReturnValueOnce(JOINABLE_STATE_STATUS.ALREADY_JOINED); + + const result = await voice.join({ interaction: mockedInteraction }); + + expect(getJoinableStateStatus).toHaveBeenCalledWith({ + channel: { + id: 'channelId', + guild: { + id: 'guildId', + }, + }, + }); + + expect(result).toBe(false); + }); + it('should join the voice channel and return true', async () => { const mockedGuildMember = { voice: { @@ -230,7 +283,7 @@ describe('Voice', () => { .mockReturnValueOnce('unknown' as any); await expect(voice.join({ interaction })).rejects.toThrowError( - 'unknown is unexpected value. Should have been never.' + 'Unknown joinable type.' ); }); }); diff --git a/src/features/core/internal/Voice/internal/Voice.class.ts b/src/features/core/internal/Voice/internal/Voice.class.ts index b9f902ea..e818514d 100644 --- a/src/features/core/internal/Voice/internal/Voice.class.ts +++ b/src/features/core/internal/Voice/internal/Voice.class.ts @@ -5,9 +5,8 @@ import type { } from '@/features/library/index.js'; import { isNil, joinVoiceChannel } from '@/features/library/index.js'; import { LogicException } from '@/features/others/Error/LogicException.js'; -import { assertNever } from '@/features/others/assertNever.js'; +import { assertNever } from '@/features/others/assertNever/index.js'; import { getActorId } from '@/features/others/discord/index.js'; -import { singleton } from '@/features/others/singleton/index.js'; import { getActorConnectionState } from './funcs/getActorConnectionState/index.js'; import { @@ -15,9 +14,6 @@ import { getJoinableStateStatus, } from './funcs/getJoinableStateStatus/index.js'; -const createVoiceInstance = () => new Voice(); -export const getVoiceInstance = singleton(createVoiceInstance); - export class Voice { connection: Array<{ guildId: string; @@ -80,7 +76,7 @@ export class Voice { adapterCreator: channel.guild.voiceAdapterCreator, }); this.connection.push({ - guildId: actor.guild.id, + guildId: channel.guildId, connection: connectedChannel, player: undefined, }); @@ -110,10 +106,11 @@ export class Voice { return true; default: - assertNever(joinable); + assertNever(joinable, false); + break; + // HACK: カバレッジを100%にするための処置 } - /* c8 ignore next */ - return; + return Promise.reject(new LogicException('Unknown joinable type.')); } public async leave({ diff --git a/src/features/core/internal/Voice/internal/funcs/getJoinableStateStatus/internal/getJoinableStateStatus.spec.ts b/src/features/core/internal/Voice/internal/funcs/getJoinableStateStatus/internal/getJoinableStateStatus.spec.ts index 5b903fb5..415c3975 100644 --- a/src/features/core/internal/Voice/internal/funcs/getJoinableStateStatus/internal/getJoinableStateStatus.spec.ts +++ b/src/features/core/internal/Voice/internal/funcs/getJoinableStateStatus/internal/getJoinableStateStatus.spec.ts @@ -1,7 +1,7 @@ import type { VoiceBasedChannel } from '@/features/library/index.js'; -import { getJoinableStateStatus } from './getJoinableStateStatus.func.js'; import { JOINABLE_STATE_STATUS } from './getJoinableStateStatus.constants.js'; +import { getJoinableStateStatus } from './getJoinableStateStatus.func.js'; describe('getJoinableStateStatus', () => { it('should return NOT_FOUND if the channel is null', () => { diff --git a/src/features/others/assertNever.ts b/src/features/others/assertNever.ts deleted file mode 100644 index efc231b0..00000000 --- a/src/features/others/assertNever.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const assertNever = (x: never): never => { - throw new Error(`${x} is unexpected value. Should have been never.`); -}; diff --git a/src/features/others/assertNever/index.ts b/src/features/others/assertNever/index.ts new file mode 100644 index 00000000..89d27f92 --- /dev/null +++ b/src/features/others/assertNever/index.ts @@ -0,0 +1 @@ +export { assertNever } from './internal/assertNever.func.js'; diff --git a/src/features/others/assertNever/internal/assertNever.func.ts b/src/features/others/assertNever/internal/assertNever.func.ts new file mode 100644 index 00000000..bfe7f856 --- /dev/null +++ b/src/features/others/assertNever/internal/assertNever.func.ts @@ -0,0 +1,4 @@ +export const assertNever = (x: never, shouldThrow: boolean = true): never => { + if (shouldThrow) throw new Error(`Unexpected object: ${x}`); + return x; +}; diff --git a/src/features/others/assertNever/internal/assertNever.spec.ts b/src/features/others/assertNever/internal/assertNever.spec.ts new file mode 100644 index 00000000..98fec024 --- /dev/null +++ b/src/features/others/assertNever/internal/assertNever.spec.ts @@ -0,0 +1,15 @@ +import { assertNever } from './assertNever.func.js'; + +describe('assertNever', () => { + it('should throw an error with the unexpected object', () => { + const unexpectedObject = {} as never; + expect(() => assertNever(unexpectedObject)).toThrowError( + 'Unexpected object: [object Object]' + ); + }); + + it('should not throw an error when shouldThrow is false', () => { + const unexpectedObject = {} as never; + expect(() => assertNever(unexpectedObject, false)).not.toThrow(); + }); +});