diff --git a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts index 575a6958f348..c59ef56e0abd 100644 --- a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts +++ b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts @@ -5,9 +5,9 @@ import { PermissionFlagsBits, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { ContextMenuCommandAssertions, MessageCommandBuilder } from '../../src/index.js'; +import { ContextMenuCommandAssertions, MessageContextCommandBuilder } from '../../src/index.js'; -const getBuilder = () => new MessageCommandBuilder(); +const getBuilder = () => new MessageContextCommandBuilder(); describe('Context Menu Commands', () => { describe('Assertions tests', () => { diff --git a/packages/builders/__tests__/interactions/SlashCommands/ChatInputCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/ChatInputCommands.test.ts index a8acb36ecb05..b4f1440d79f8 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/ChatInputCommands.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/ChatInputCommands.test.ts @@ -109,41 +109,43 @@ describe('ChatInput Commands', () => { getBuilder() .setName('example') .setDescription('Example command') - .addBooleanOption((boolean) => + .addBooleanOptions((boolean) => boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true), ) - .addChannelOption((channel) => channel.setName('iscool').setDescription('Are we cool or what?')) - .addMentionableOption((mentionable) => mentionable.setName('iscool').setDescription('Are we cool or what?')) - .addRoleOption((role) => role.setName('iscool').setDescription('Are we cool or what?')) - .addUserOption((user) => user.setName('iscool').setDescription('Are we cool or what?')) - .addIntegerOption((integer) => + .addChannelOptions((channel) => channel.setName('iscool').setDescription('Are we cool or what?')) + .addMentionableOptions((mentionable) => + mentionable.setName('iscool').setDescription('Are we cool or what?'), + ) + .addRoleOptions((role) => role.setName('iscool').setDescription('Are we cool or what?')) + .addUserOptions((user) => user.setName('iscool').setDescription('Are we cool or what?')) + .addIntegerOptions((integer) => integer .setName('iscool') .setDescription('Are we cool or what?') .addChoices({ name: 'Very cool', value: 1_000 }) .addChoices([{ name: 'Even cooler', value: 2_000 }]), ) - .addNumberOption((number) => + .addNumberOptions((number) => number .setName('iscool') .setDescription('Are we cool or what?') .addChoices({ name: 'Very cool', value: 1.5 }) .addChoices([{ name: 'Even cooler', value: 2.5 }]), ) - .addStringOption((string) => + .addStringOptions((string) => string .setName('iscool') .setDescription('Are we cool or what?') .addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' }) .addChoices([{ name: 'The Whole shebang', value: 'all' }]), ) - .addIntegerOption((integer) => + .addIntegerOptions((integer) => integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), ) - .addNumberOption((number) => + .addNumberOptions((number) => number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), ) - .addStringOption((string) => + .addStringOptions((string) => string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), ) .toJSON(), @@ -153,20 +155,22 @@ describe('ChatInput Commands', () => { test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => { expect(() => // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - getNamedBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean')).toJSON(), + getNamedBuilder().addStringOptions(getStringOption().setAutocomplete('not a boolean')).toJSON(), ).toThrowError(); }); test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => { expect(() => getNamedBuilder() - .addStringOption(getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' })) + .addStringOptions( + getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }), + ) .toJSON(), ).toThrowError(); expect(() => getNamedBuilder() - .addStringOption( + .addStringOptions( getStringOption() .setAutocomplete(true) .addChoices( @@ -180,7 +184,9 @@ describe('ChatInput Commands', () => { expect(() => getNamedBuilder() - .addStringOption(getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true)) + .addStringOptions( + getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true), + ) .toJSON(), ).toThrowError(); }); @@ -188,7 +194,7 @@ describe('ChatInput Commands', () => { test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => { expect(() => getNamedBuilder() - .addChannelOption( + .addChannelOptions( getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]), ) .toJSON(), @@ -196,7 +202,7 @@ describe('ChatInput Commands', () => { expect(() => { getNamedBuilder() - .addChannelOption(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText)) + .addChannelOptions(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText)) .toJSON(); }).not.toThrowError(); }); @@ -204,68 +210,71 @@ describe('ChatInput Commands', () => { test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => { expect(() => // @ts-expect-error: Invalid channel type - getNamedBuilder().addChannelOption(getChannelOption().addChannelTypes(100)).toJSON(), + getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100)).toJSON(), ).toThrowError(); expect(() => // @ts-expect-error: Invalid channel types - getNamedBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200)).toJSON(), + getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100, 200)).toJSON(), ).toThrowError(); }); test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => { // @ts-expect-error: Invalid max value - expect(() => getNamedBuilder().addNumberOption(getNumberOption().setMaxValue('test')).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue('test')).toJSON()).toThrowError(); expect(() => // @ts-expect-error: Invalid max value - getNamedBuilder().addIntegerOption(getIntegerOption().setMaxValue('test')).toJSON(), + getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue('test')).toJSON(), ).toThrowError(); // @ts-expect-error: Invalid min value - expect(() => getNamedBuilder().addNumberOption(getNumberOption().setMinValue('test')).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMinValue('test')).toJSON()).toThrowError(); expect(() => // @ts-expect-error: Invalid min value - getNamedBuilder().addIntegerOption(getIntegerOption().setMinValue('test')).toJSON(), + getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue('test')).toJSON(), ).toThrowError(); - expect(() => getNamedBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5)).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1.5)).toJSON()).toThrowError(); }); test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => { - expect(() => getNamedBuilder().addIntegerOption(getIntegerOption().setMinValue(1)).toJSON()).not.toThrowError(); + expect(() => + getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1)).toJSON(), + ).not.toThrowError(); - expect(() => getNamedBuilder().addNumberOption(getNumberOption().setMinValue(1.5)).toJSON()).not.toThrowError(); + expect(() => + getNamedBuilder().addNumberOptions(getNumberOption().setMinValue(1.5)).toJSON(), + ).not.toThrowError(); - expect(() => getNamedBuilder().addIntegerOption(getIntegerOption().setMaxValue(1)).toJSON()).not.toThrowError(); + expect(() => + getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue(1)).toJSON(), + ).not.toThrowError(); - expect(() => getNamedBuilder().addNumberOption(getNumberOption().setMaxValue(1.5)).toJSON()).not.toThrowError(); + expect(() => + getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue(1.5)).toJSON(), + ).not.toThrowError(); }); test('GIVEN an already built builder THEN does not throw an error', () => { - expect(() => getNamedBuilder().addStringOption(getStringOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addStringOptions(getStringOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addIntegerOption(getIntegerOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addNumberOption(getNumberOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addNumberOptions(getNumberOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addUserOption(getUserOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addUserOptions(getUserOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addChannelOption(getChannelOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addRoleOption(getRoleOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addRoleOptions(getRoleOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addAttachmentOption(getAttachmentOption()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addAttachmentOptions(getAttachmentOption()).toJSON()).not.toThrowError(); - expect(() => getNamedBuilder().addMentionableOption(getMentionableOption()).toJSON()).not.toThrowError(); - }); - - test('GIVEN no valid return for an addOption method THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addBooleanOption().toJSON()).toThrowError(); + expect(() => getNamedBuilder().addMentionableOptions(getMentionableOption()).toJSON()).not.toThrowError(); }); test('GIVEN invalid name THEN throw error', () => { @@ -281,38 +290,38 @@ describe('ChatInput Commands', () => { test('GIVEN invalid returns for builder THEN throw error', () => { // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addBooleanOption(true).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addBooleanOptions(true).toJSON()).toThrowError(); // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addBooleanOption(null).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addBooleanOptions(null).toJSON()).toThrowError(); // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addBooleanOption(undefined).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addBooleanOptions(undefined).toJSON()).toThrowError(); expect(() => getNamedBuilder() // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - .addBooleanOption(() => ChatInputCommandStringOption) + .addBooleanOptions(() => ChatInputCommandStringOption) .toJSON(), ).toThrowError(); expect(() => getNamedBuilder() // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - .addBooleanOption(() => new Collection()) + .addBooleanOptions(() => new Collection()) .toJSON(), ).toThrowError(); }); test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => { expect(() => - getNamedBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()).toJSON(), + getNamedBuilder().addStringOptions(getStringOption().setAutocomplete(true).setChoices()).toJSON(), ).not.toThrowError(); }); test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => { expect(() => getNamedBuilder() - .addStringOption(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' })) + .addStringOptions(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' })) .toJSON(), ).toThrowError(); }); @@ -320,7 +329,7 @@ describe('ChatInput Commands', () => { test('GIVEN an option, THEN setting choices should not throw an error', () => { expect(() => getNamedBuilder() - .addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })) + .addStringOptions(getStringOption().setChoices({ name: 'owo', value: 'uwu' })) .toJSON(), ).not.toThrowError(); }); @@ -334,8 +343,8 @@ describe('ChatInput Commands', () => { test('GIVEN builder with subcommand group THEN does not throw error', () => { expect(() => getNamedBuilder() - .addSubcommandGroup((group) => - group.setName('group').setDescription('Group us together!').addSubcommand(getSubcommand()), + .addSubcommandGroups((group) => + group.setName('group').setDescription('Group us together!').addSubcommands(getSubcommand()), ) .toJSON(), ).not.toThrowError(); @@ -344,7 +353,7 @@ describe('ChatInput Commands', () => { test('GIVEN builder with subcommand THEN does not throw error', () => { expect(() => getNamedBuilder() - .addSubcommand((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)')) + .addSubcommands((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)')) .toJSON(), ).not.toThrowError(); }); @@ -354,8 +363,8 @@ describe('ChatInput Commands', () => { getBuilder() .setName('name') .setDescription('description') - .addSubcommand((option) => option.setName('ye').setDescription('ye')) - .addSubcommand((option) => option.setName('no').setDescription('no')) + .addSubcommands((option) => option.setName('ye').setDescription('ye')) + .addSubcommands((option) => option.setName('no').setDescription('no')) .setDefaultMemberPermissions(1n) .toJSON(), ).not.toThrowError(); @@ -363,52 +372,45 @@ describe('ChatInput Commands', () => { test('GIVEN builder with already built subcommand group THEN does not throw error', () => { expect(() => - getNamedBuilder().addSubcommandGroup(getSubcommandGroup().addSubcommand(getSubcommand())).toJSON(), + getNamedBuilder().addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())).toJSON(), ).not.toThrowError(); }); test('GIVEN builder with already built subcommand THEN does not throw error', () => { - expect(() => getNamedBuilder().addSubcommand(getSubcommand()).toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().addSubcommands(getSubcommand()).toJSON()).not.toThrowError(); }); test('GIVEN builder with already built subcommand with options THEN does not throw error', () => { expect(() => - getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())).toJSON(), + getNamedBuilder().addSubcommands(getSubcommand().addBooleanOptions(getBooleanOption())).toJSON(), ).not.toThrowError(); }); test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => { expect(() => // @ts-expect-error: Checking if check works JS-side too - getNamedBuilder().addSubcommand(getSubcommand()).addIntegerOption(getInteger()).toJSON(), + getNamedBuilder().addSubcommands(getSubcommand()).addIntegerOptions(getInteger()).toJSON(), ).toThrowError(); }); test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => { // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addSubcommandGroup().toJSON()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addSubcommand().toJSON()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getNamedBuilder().addSubcommand(getSubcommandGroup()).toJSON()).toThrowError(); + expect(() => getNamedBuilder().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError(); }); }); describe('Subcommand group builder', () => { test('GIVEN no valid subcommand THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getSubcommandGroup().addSubcommand().toJSON()).toThrowError(); + expect(() => getSubcommandGroup().addSubcommands().toJSON()).toThrowError(); // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup()).toJSON()).toThrowError(); + expect(() => getSubcommandGroup().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError(); }); test('GIVEN a valid subcommand THEN does not throw an error', () => { expect(() => getSubcommandGroup() - .addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123')) + .addSubcommands((sub) => sub.setName('sub').setDescription('Testing 123')) .toJSON(), ).not.toThrowError(); }); @@ -416,7 +418,7 @@ describe('ChatInput Commands', () => { describe('Subcommand builder', () => { test('GIVEN a valid subcommand with options THEN does not throw error', () => { - expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError(); + expect(() => getSubcommand().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError(); }); }); @@ -507,10 +509,10 @@ describe('ChatInput Commands', () => { test('GIVEN valid permission with options THEN does not throw error', () => { expect(() => - getNamedBuilder().addBooleanOption(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(), + getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(), ).not.toThrowError(); - expect(() => getNamedBuilder().addChannelOption(getChannelOption())).not.toThrowError(); + expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError(); }); }); diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts index 3f26eda8c1dc..4a1b0e4b41e1 100644 --- a/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts @@ -8,10 +8,10 @@ import { Mixin } from 'ts-mixer'; import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js'; import { resolveBuilder } from '../../../util/resolveBuilder.js'; import { isValidationEnabled } from '../../../util/validation.js'; -import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js'; -import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js'; import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js'; import { SharedNameAndDescription } from '../SharedNameAndDescription.js'; +import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js'; +import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js'; export interface ChatInputCommandSubcommandGroupData { options?: ChatInputCommandSubcommandBuilder[]; diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts b/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts index 64f450c22e06..a3d03edf8fa6 100644 --- a/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts +++ b/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts @@ -1,7 +1,9 @@ +import type { RestOrArray } from '../../../../util/normalizeArray.js'; +import { normalizeArray } from '../../../../util/normalizeArray.js'; import { resolveBuilder } from '../../../../util/resolveBuilder.js'; import { - ChatInputCommandSubcommandBuilder, ChatInputCommandSubcommandGroupBuilder, + ChatInputCommandSubcommandBuilder, } from '../ChatInputCommandSubcommands.js'; export interface SharedChatInputCommandSubcommandsData { @@ -17,37 +19,41 @@ export class SharedChatInputCommandSubcommands { protected declare readonly data: SharedChatInputCommandSubcommandsData; /** - * Adds a new subcommand group to this command. + * Adds subcommand groups to this command. * - * @param input - A function that returns a subcommand group builder or an already built builder + * @param input - Subcommand groups to add */ - public addSubcommandGroup( - input: + public addSubcommandGroups( + ...input: RestOrArray< | ChatInputCommandSubcommandGroupBuilder - | ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder), + | ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder) + > ): this { - this.data.options ??= []; + const normalized = normalizeArray(input); + const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandGroupBuilder)); - const result = resolveBuilder(input, ChatInputCommandSubcommandGroupBuilder); - this.data.options.push(result); + this.data.options ??= []; + this.data.options.push(...resolved); return this; } /** - * Adds a new subcommand to this command. + * Adds subcommands to this command. * - * @param input - A function that returns a subcommand builder or an already built builder + * @param input - Subcommands to add */ - public addSubcommand( - input: + public addSubcommands( + ...input: RestOrArray< | ChatInputCommandSubcommandBuilder - | ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder), + | ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder) + > ): this { - this.data.options ??= []; + const normalized = normalizeArray(input); + const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandBuilder)); - const result = resolveBuilder(input, ChatInputCommandSubcommandBuilder); - this.data.options.push(result); + this.data.options ??= []; + this.data.options.push(...resolved); return this; }