refactor: builders (#10448)

BREAKING CHANGE: formatters export removed (prev. deprecated)
BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated)
BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options
BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components`
BREAKING CHANGE: Removed `equals` methods
BREAKING CHANGE: Sapphire -> zod for validation
BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead
BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command"
BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder`
BREAKING CHANGE: Removed support for passing the "string key"s of enums
BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style
BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs`

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
Denis Cristea
2024-10-01 19:11:56 +03:00
committed by GitHub
parent c633d5c7f6
commit ab32f26cbb
91 changed files with 3772 additions and 3824 deletions

View File

@@ -7,8 +7,8 @@ import {
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
PrimaryButtonBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from '../../src/index.js';
@@ -41,21 +41,14 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
value: 'two',
},
],
max_values: 10,
min_values: 12,
max_values: 2,
min_values: 2,
},
],
};
describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError();
});
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
@@ -72,22 +65,10 @@ describe('Action Row Components', () => {
style: ButtonStyle.Link,
url: 'https://google.com',
},
{
type: ComponentType.StringSelect,
placeholder: 'test',
custom_id: 'test',
options: [
{
label: 'option',
value: 'option',
},
],
},
],
};
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
@@ -120,24 +101,23 @@ describe('Action Row Components', () => {
value: 'two',
},
],
max_values: 10,
min_values: 12,
max_values: 1,
min_values: 1,
},
],
};
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setMaxValues(10)
.setMinValues(12)
.setMaxValues(2)
.setMinValues(2)
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
@@ -147,10 +127,39 @@ describe('Action Row Components', () => {
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
]);
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addPrimaryButtonComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addPrimaryButtonComponents([button]).toJSON()).toEqual(rowWithButtonData);
});
test('GIVEN 2 select menus THEN it throws', () => {
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
);
expect(() =>
new ActionRowBuilder()
.addStringSelectMenuComponent(selectMenu)
.addStringSelectMenuComponent(selectMenu)
.toJSON(),
).toThrowError();
});
test('GIVEN a button and a select menu THEN it throws', () => {
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
);
expect(() =>
new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).addPrimaryButtonComponents(button).toJSON(),
).toThrowError();
});
});
});

View File

@@ -5,45 +5,21 @@ import {
type APIButtonComponentWithURL,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions.js';
import { ButtonBuilder } from '../../src/components/button/Button.js';
const buttonComponent = () => new ButtonBuilder();
import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js';
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
describe('Button Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid label THEN validator does not throw', () => {
expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid label THEN validator does throw', () => {
expect(() => buttonLabelValidator.parse(null)).toThrowError();
expect(() => buttonLabelValidator.parse('')).toThrowError();
expect(() => buttonLabelValidator.parse(longStr)).toThrowError();
});
test('GIVEN valid style THEN validator does not throw', () => {
expect(() => buttonStyleValidator.parse(3)).not.toThrowError();
expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError();
});
test('GIVEN invalid style THEN validator does throw', () => {
expect(() => buttonStyleValidator.parse(7)).toThrowError();
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() =>
buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'),
).not.toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test')).not.toThrowError();
expect(() => {
const button = buttonComponent()
const button = new PrimaryButtonBuilder()
.setCustomId('custom')
.setStyle(ButtonStyle.Primary)
.setLabel('test')
.setDisabled(true)
.setEmoji({ name: 'test' });
@@ -51,111 +27,41 @@ describe('Button Components', () => {
}).not.toThrowError();
expect(() => {
const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium);
const button = new PremiumButtonBuilder().setSKUId('123456789012345678');
button.toJSON();
}).not.toThrowError();
expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError();
expect(() => new LinkButtonBuilder().setURL('https://google.com')).not.toThrowError();
});
test('GIVEN invalid fields THEN build does throw', () => {
expect(() => {
buttonComponent().setCustomId(longStr);
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setCustomId('custom')
.setStyle(ButtonStyle.Primary)
.setDisabled(true)
.setLabel('test')
.setURL('https://google.com')
.setEmoji({ name: 'test' });
button.toJSON();
new PrimaryButtonBuilder().setCustomId(longStr).toJSON();
}).toThrowError();
expect(() => {
// @ts-expect-error: Invalid emoji
const button = buttonComponent().setEmoji('test');
const button = new PrimaryButtonBuilder().setEmoji('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary);
const button = new PrimaryButtonBuilder();
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Link);
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary).setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Secondary)
.setLabel('button')
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Success)
.setEmoji({ name: '😇' })
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Danger)
.setCustomId('test')
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Link)
.setURL('https://google.com')
.setSKUId('123456789012345678');
const button = new PrimaryButtonBuilder().setCustomId('test');
button.toJSON();
}).toThrowError();
// @ts-expect-error: Invalid style
expect(() => buttonComponent().setStyle(24)).toThrowError();
expect(() => buttonComponent().setLabel(longStr)).toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setStyle(24).toJSON()).toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setLabel(longStr).toJSON()).toThrowError();
// @ts-expect-error: Invalid parameter for disabled
expect(() => buttonComponent().setDisabled(0)).toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setDisabled(0).toJSON()).toThrowError();
// @ts-expect-error: Invalid emoji
expect(() => buttonComponent().setEmoji('foo')).toThrowError();
expect(() => buttonComponent().setURL('foobar')).toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setEmoji('foo').toJSON()).toThrowError();
});
test('GiVEN valid input THEN valid JSON outputs are given', () => {
@@ -167,13 +73,12 @@ describe('Button Components', () => {
disabled: true,
};
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect(new PrimaryButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect(
buttonComponent()
new PrimaryButtonBuilder()
.setCustomId(interactionData.custom_id)
.setLabel(interactionData.label!)
.setStyle(interactionData.style)
.setDisabled(interactionData.disabled)
.toJSON(),
).toEqual(interactionData);
@@ -186,9 +91,7 @@ describe('Button Components', () => {
url: 'https://google.com',
};
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
expect(buttonComponent().setLabel(linkData.label!).setDisabled(true).setURL(linkData.url));
expect(new LinkButtonBuilder(linkData).toJSON()).toEqual(linkData);
});
});
});

View File

@@ -11,14 +11,14 @@ import {
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
CustomIdButtonBuilder,
StringSelectMenuBuilder,
TextInputBuilder,
} from '../../src/index.js';
describe('createComponentBuilder', () => {
test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])(
test.each([StringSelectMenuBuilder, TextInputBuilder])(
'passing an instance of %j should return itself',
(Builder) => {
const builder = new Builder();
@@ -42,7 +42,7 @@ describe('createComponentBuilder', () => {
type: ComponentType.Button,
};
expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder);
expect(createComponentBuilder(button)).toBeInstanceOf(CustomIdButtonBuilder);
});
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {

View File

@@ -3,6 +3,7 @@ import { describe, test, expect } from 'vitest';
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';
const selectMenu = () => new StringSelectMenuBuilder();
const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' });
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
const longStr = 'a'.repeat(256);
@@ -16,10 +17,10 @@ const selectMenuOptionData: APISelectMenuOption = {
};
const selectMenuDataWithoutOptions = {
type: ComponentType.SelectMenu,
type: ComponentType.StringSelect,
custom_id: 'test',
max_values: 10,
min_values: 3,
max_values: 1,
min_values: 1,
disabled: true,
placeholder: 'test',
} as const;
@@ -109,49 +110,87 @@ describe('Select Menu Components', () => {
});
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
expect(() => selectMenu().setCustomId(longStr)).toThrowError();
expect(() => selectMenu().setMaxValues(30)).toThrowError();
expect(() => selectMenu().setMinValues(-20)).toThrowError();
expect(() => selectMenu().setCustomId(longStr).toJSON()).toThrowError();
expect(() => selectMenuWithId().setMaxValues(30).toJSON()).toThrowError();
expect(() => selectMenuWithId().setMinValues(-20).toJSON()).toThrowError();
// @ts-expect-error: Invalid disabled value
expect(() => selectMenu().setDisabled(0)).toThrowError();
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
expect(() => selectMenuWithId().setDisabled(0).toJSON()).toThrowError();
expect(() => selectMenuWithId().setPlaceholder(longStr).toJSON()).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError();
expect(() => selectMenuWithId().addOptions({ label: 'test' }).toJSON()).toThrowError();
expect(() => selectMenuWithId().addOptions({ label: longStr, value: 'test' }).toJSON()).toThrowError();
expect(() => selectMenuWithId().addOptions({ value: longStr, label: 'test' }).toJSON()).toThrowError();
expect(() =>
selectMenuWithId().addOptions({ label: 'test', value: 'test', description: longStr }).toJSON(),
).toThrowError();
expect(() =>
// @ts-expect-error: Invalid option
selectMenuWithId().addOptions({ label: 'test', value: 'test', default: 100 }).toJSON(),
).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError();
expect(() => selectMenuWithId().addOptions({ value: 'test' }).toJSON()).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ default: true }])).toThrowError();
expect(() => selectMenuWithId().addOptions({ default: true }).toJSON()).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ label: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ label: longStr, value: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ value: longStr, label: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ label: 'test', value: 'test', description: longStr }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ label: 'test', value: 'test', default: 100 }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ value: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ default: true }])
.toJSON(),
).toThrowError();
const tooManyOptions = Array.from<APISelectMenuOption>({ length: 26 }).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError();
expect(() =>
selectMenu()
.setOptions(...tooManyOptions)
.toJSON(),
).toThrowError();
expect(() => selectMenu().setOptions(tooManyOptions).toJSON()).toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...tooManyOptions),
.addOptions(...tooManyOptions)
.toJSON(),
).toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
.addOptions(tooManyOptions),
.addOptions(tooManyOptions)
.toJSON(),
).toThrowError();
expect(() => {
@@ -162,7 +201,8 @@ describe('Select Menu Components', () => {
.setDefault(-1)
// @ts-expect-error: Invalid emoji
.setEmoji({ name: 1 })
.setDescription(longStr);
.setDescription(longStr)
.toJSON();
}).toThrowError();
});
@@ -212,17 +252,16 @@ describe('Select Menu Components', () => {
).toStrictEqual([selectMenuOptionData]);
expect(() =>
makeStringSelectMenuWithOptions().spliceOptions(
0,
0,
...Array.from({ length: 26 }, () => selectMenuOptionData),
),
makeStringSelectMenuWithOptions()
.spliceOptions(0, 0, ...Array.from({ length: 26 }, () => selectMenuOptionData))
.toJSON(),
).toThrowError();
expect(() =>
makeStringSelectMenuWithOptions()
.setOptions(Array.from({ length: 25 }, () => selectMenuOptionData))
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData),
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData)
.toJSON(),
).toThrowError();
});
});

View File

@@ -1,13 +1,5 @@
import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
labelValidator,
maxLengthValidator,
minLengthValidator,
placeholderValidator,
valueValidator,
textInputStyleValidator,
} from '../../src/components/textInput/Assertions.js';
import { TextInputBuilder } from '../../src/components/textInput/TextInput.js';
const superLongStr = 'a'.repeat(5_000);
@@ -16,56 +8,6 @@ const textInputComponent = () => new TextInputBuilder();
describe('Text Input Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid label THEN validator does not throw', () => {
expect(() => labelValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid label THEN validator does throw', () => {
expect(() => labelValidator.parse(24)).toThrowError();
expect(() => labelValidator.parse(undefined)).toThrowError();
});
test('GIVEN valid style THEN validator does not throw', () => {
expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError();
expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError();
});
test('GIVEN invalid style THEN validator does throw', () => {
expect(() => textInputStyleValidator.parse(24)).toThrowError();
});
test('GIVEN valid min length THEN validator does not throw', () => {
expect(() => minLengthValidator.parse(10)).not.toThrowError();
});
test('GIVEN invalid min length THEN validator does throw', () => {
expect(() => minLengthValidator.parse(-1)).toThrowError();
});
test('GIVEN valid max length THEN validator does not throw', () => {
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
});
test('GIVEN invalid min length THEN validator does throw 2', () => {
expect(() => maxLengthValidator.parse(4_001)).toThrowError();
});
test('GIVEN valid value THEN validator does not throw', () => {
expect(() => valueValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid value THEN validator does throw', () => {
expect(() => valueValidator.parse(superLongStr)).toThrowError();
});
test('GIVEN valid placeholder THEN validator does not throw', () => {
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid value THEN validator does throw 2', () => {
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => {
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
@@ -84,9 +26,7 @@ describe('Text Input Components', () => {
}).not.toThrowError();
expect(() => {
// Issue #8107
// @ts-expect-error: Shapeshift maps the enum key to the value when parsing
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle('Short').toJSON();
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON();
}).not.toThrowError();
});
});

View File

@@ -0,0 +1,516 @@
import {
ApplicationCommandType,
ApplicationIntegrationType,
ChannelType,
InteractionContextType,
PermissionFlagsBits,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ChatInputCommandBooleanOption,
ChatInputCommandBuilder,
ChatInputCommandChannelOption,
ChatInputCommandIntegerOption,
ChatInputCommandMentionableOption,
ChatInputCommandNumberOption,
ChatInputCommandRoleOption,
ChatInputCommandAttachmentOption,
ChatInputCommandStringOption,
ChatInputCommandSubcommandBuilder,
ChatInputCommandSubcommandGroupBuilder,
ChatInputCommandUserOption,
} from '../../../src/index.js';
const getBuilder = () => new ChatInputCommandBuilder();
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
const getIntegerOption = () => new ChatInputCommandIntegerOption().setName('owo').setDescription('Testing 123');
const getNumberOption = () => new ChatInputCommandNumberOption().setName('owo').setDescription('Testing 123');
const getBooleanOption = () => new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123');
const getUserOption = () => new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123');
const getChannelOption = () => new ChatInputCommandChannelOption().setName('owo').setDescription('Testing 123');
const getRoleOption = () => new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123');
const getAttachmentOption = () => new ChatInputCommandAttachmentOption().setName('owo').setDescription('Testing 123');
const getMentionableOption = () => new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123');
const getSubcommandGroup = () =>
new ChatInputCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
class Collection {
public readonly [Symbol.toStringTag] = 'Map';
}
describe('ChatInput Commands', () => {
describe('ChatInputCommandBuilder', () => {
describe('Builder with no options', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
});
});
describe('Builder with simple options', () => {
test('GIVEN valid builder THEN returns type included', () => {
expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput });
});
test('GIVEN valid builder with options THEN does not throw error', () => {
expect(() =>
getBuilder()
.setName('example')
.setDescription('Example command')
.addBooleanOptions((boolean) =>
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
)
.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 }]),
)
.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 }]),
)
.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' }]),
)
.addIntegerOptions((integer) =>
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addNumberOptions((number) =>
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addStringOptions((string) =>
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.toJSON(),
).not.toThrowError();
});
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().addStringOptions(getStringOption().setAutocomplete('not a boolean')).toJSON(),
).toThrowError();
});
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption()
.setAutocomplete(true)
.addChoices(
{ name: 'Fancy Pants', value: 'fp_1' },
{ name: 'Fancy Shoes', value: 'fs_1' },
{ name: 'The Whole shebang', value: 'all' },
),
)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
)
.toJSON(),
).toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
expect(() =>
getNamedBuilder()
.addChannelOptions(
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
)
.toJSON(),
).not.toThrowError();
expect(() => {
getNamedBuilder()
.addChannelOptions(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText))
.toJSON();
}).not.toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
expect(() =>
// @ts-expect-error: Invalid channel type
getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100)).toJSON(),
).toThrowError();
expect(() =>
// @ts-expect-error: Invalid channel types
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().addNumberOptions(getNumberOption().setMaxValue('test')).toJSON()).toThrowError();
expect(() =>
// @ts-expect-error: Invalid max value
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue('test')).toJSON(),
).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMinValue('test')).toJSON()).toThrowError();
expect(() =>
// @ts-expect-error: Invalid min value
getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue('test')).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().addIntegerOptions(getIntegerOption().setMinValue(1)).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().addNumberOptions(getNumberOption().setMinValue(1.5)).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue(1)).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().addStringOptions(getStringOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addNumberOptions(getNumberOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addUserOptions(getUserOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addRoleOptions(getRoleOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addAttachmentOptions(getAttachmentOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addMentionableOptions(getMentionableOption()).toJSON()).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('TEST_COMMAND').setDescription(':3').toJSON()).toThrowError();
expect(() => getBuilder().setName('ĂĂĂĂĂĂ').setDescription(':3').toJSON()).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there').setDescription(':3')).not.toThrowError();
expect(() => getBuilder().setName('o_comandă').setDescription(':3')).not.toThrowError();
expect(() => getBuilder().setName('どうも').setDescription(':3')).not.toThrowError();
});
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().addBooleanOptions(true).toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addBooleanOptions(null).toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addBooleanOptions(undefined).toJSON()).toThrowError();
expect(() =>
getNamedBuilder()
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
.addBooleanOptions(() => ChatInputCommandStringOption)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
.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().addStringOptions(getStringOption().setAutocomplete(true).setChoices()).toJSON(),
).not.toThrowError();
});
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }))
.toJSON(),
).toThrowError();
});
test('GIVEN an option, THEN setting choices should not throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(getStringOption().setChoices({ name: 'owo', value: 'uwu' }))
.toJSON(),
).not.toThrowError();
});
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
expect(() => getNamedBuilder().setName('foo').setDescription('foo').setNSFW(true).toJSON()).not.toThrowError();
});
});
describe('Builder with subcommand (group) options', () => {
test('GIVEN builder with subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder()
.addSubcommandGroups((group) =>
group.setName('group').setDescription('Group us together!').addSubcommands(getSubcommand()),
)
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN does not throw error', () => {
expect(() =>
getNamedBuilder()
.addSubcommands((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'))
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN has regular ChatInput command fields', () => {
expect(() =>
getBuilder()
.setName('name')
.setDescription('description')
.addSubcommands((option) => option.setName('ye').setDescription('ye'))
.addSubcommands((option) => option.setName('no').setDescription('no'))
.setDefaultMemberPermissions(1n)
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())).toJSON(),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommands(getSubcommand()).toJSON()).not.toThrowError();
});
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
expect(() =>
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().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().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
});
});
describe('Subcommand group builder', () => {
test('GIVEN no valid subcommand THEN throw error', () => {
expect(() => getSubcommandGroup().addSubcommands().toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getSubcommandGroup().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
});
test('GIVEN a valid subcommand THEN does not throw an error', () => {
expect(() =>
getSubcommandGroup()
.addSubcommands((sub) => sub.setName('sub').setDescription('Testing 123'))
.toJSON(),
).not.toThrowError();
});
});
describe('Subcommand builder', () => {
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
expect(() => getSubcommand().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
});
});
describe('ChatInput command localizations', () => {
const expectedSingleLocale = { 'en-US': 'foobar' };
const expectedMultipleLocales = {
...expectedSingleLocale,
bg: 'test',
};
test('GIVEN valid name localizations THEN does not throw error', () => {
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getNamedBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getNamedBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getNamedBuilder().setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
expectedSingleLocale,
);
expect(
getNamedBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON().name_localizations,
).toEqual(expectedMultipleLocales);
expect(getNamedBuilder().clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
expect(getNamedBuilder().clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
'en-US': undefined,
});
});
test('GIVEN valid description localizations THEN does not throw error', () => {
expect(() => getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON()).not.toThrowError();
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' }).toJSON()).not.toThrowError();
});
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization description
expect(() => getNamedBuilder().setDescriptionLocalization('en-U', 'foobar').toJSON()).toThrowError();
// @ts-expect-error: Invalid localization description
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
});
test('GIVEN valid description localizations THEN valid data is stored', () => {
expect(
getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON(false).description_localizations,
).toEqual(expectedSingleLocale);
expect(
getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON(false)
.description_localizations,
).toEqual(expectedMultipleLocales);
expect(
getNamedBuilder().clearDescriptionLocalizations().toJSON(false).description_localizations,
).toBeUndefined();
expect(getNamedBuilder().clearDescriptionLocalization('en-US').toJSON(false).description_localizations).toEqual(
{
'en-US': undefined,
},
);
});
});
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
expect(() => getNamedBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
getNamedBuilder().setDefaultMemberPermissions(
PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles,
),
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getNamedBuilder().clearDefaultMemberPermissions()).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getNamedBuilder().setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
expect(() => getNamedBuilder().setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
});
test('GIVEN valid permission with options THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(),
).not.toThrowError();
expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError();
});
});
describe('contexts', () => {
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
expect(() =>
getNamedBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM).toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getNamedBuilder().setContexts(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getNamedBuilder().setContexts([999, 998]).toJSON()).toThrowError();
});
});
describe('integration types', () => {
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
expect(() =>
getNamedBuilder()
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall])
.toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder()
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
.toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getNamedBuilder().setIntegrationTypes(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getNamedBuilder().setIntegrationTypes([999, 998]).toJSON()).toThrowError();
});
});
});
});

View File

@@ -13,32 +13,32 @@ import {
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandAttachmentOption,
SlashCommandBooleanOption,
SlashCommandChannelOption,
SlashCommandIntegerOption,
SlashCommandMentionableOption,
SlashCommandNumberOption,
SlashCommandRoleOption,
SlashCommandStringOption,
SlashCommandUserOption,
ChatInputCommandAttachmentOption,
ChatInputCommandBooleanOption,
ChatInputCommandChannelOption,
ChatInputCommandIntegerOption,
ChatInputCommandMentionableOption,
ChatInputCommandNumberOption,
ChatInputCommandRoleOption,
ChatInputCommandStringOption,
ChatInputCommandUserOption,
} from '../../../src/index.js';
const getBooleanOption = () =>
new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getChannelOption = () =>
new SlashCommandChannelOption()
new ChatInputCommandChannelOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText);
const getStringOption = () =>
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getIntegerOption = () =>
new SlashCommandIntegerOption()
new ChatInputCommandIntegerOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
@@ -46,22 +46,24 @@ const getIntegerOption = () =>
.setMaxValue(10);
const getNumberOption = () =>
new SlashCommandNumberOption()
new ChatInputCommandNumberOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
.setMinValue(-1.23)
.setMaxValue(10);
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getUserOption = () =>
new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getRoleOption = () =>
new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getMentionableOption = () =>
new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getAttachmentOption = () =>
new SlashCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
new ChatInputCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
describe('Application Command toJSON() results', () => {
test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => {
@@ -101,7 +103,6 @@ describe('Application Command toJSON() results', () => {
max_value: 10,
min_value: -1,
autocomplete: true,
// TODO
choices: [],
});

View File

@@ -1,74 +1,31 @@
import { ApplicationIntegrationType, InteractionContextType, PermissionFlagsBits } from 'discord-api-types/v10';
import {
ApplicationCommandType,
ApplicationIntegrationType,
InteractionContextType,
PermissionFlagsBits,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js';
import { MessageContextCommandBuilder } from '../../src/index.js';
const getBuilder = () => new ContextMenuCommandBuilder();
const getBuilder = () => new MessageContextCommandBuilder();
describe('Context Menu Commands', () => {
describe('Assertions tests', () => {
test('GIVEN valid name THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError();
// Too short of a name
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
// Invalid characters used
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
// Too long of a name
expect(() =>
ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
).toThrowError();
});
test('GIVEN valid type THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError();
});
test('GIVEN invalid type THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError();
// Out of range
expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError();
});
test('GIVEN valid required parameters THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError();
});
test('GIVEN valid default_permission THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
});
test('GIVEN invalid default_permission THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError();
});
});
describe('ContextMenuCommandBuilder', () => {
describe('Builder tests', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('$$$')).toThrowError();
expect(() => getBuilder().setName('$$$').toJSON()).toThrowError();
expect(() => getBuilder().setName(' ')).toThrowError();
expect(() => getBuilder().setName(' ').toJSON()).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
expect(() => getBuilder().setName('hi_there').toJSON()).not.toThrowError();
expect(() => getBuilder().setName('A COMMAND')).not.toThrowError();
expect(() => getBuilder().setName('A COMMAND').toJSON()).not.toThrowError();
// Translation: a_command
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
@@ -76,20 +33,6 @@ describe('Context Menu Commands', () => {
// Translation: thx (according to GTranslate)
expect(() => getBuilder().setName('どうも')).not.toThrowError();
});
test('GIVEN valid types THEN does not throw error', () => {
expect(() => getBuilder().setType(2)).not.toThrowError();
expect(() => getBuilder().setType(3)).not.toThrowError();
});
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
});
test('GIVEN valid builder with dmPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDMPermission(false)).not.toThrowError();
});
});
describe('Context menu command localizations', () => {
@@ -106,19 +49,22 @@ describe('Context Menu Commands', () => {
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
expect(() => getBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
expectedMultipleLocales,
expect(getBuilder().setName('hi').setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
expectedSingleLocale,
);
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
'en-US': null,
expect(
getBuilder().setName('hi').setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON()
.name_localizations,
).toEqual(expectedMultipleLocales);
expect(getBuilder().setName('hi').clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
expect(getBuilder().setName('hi').clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
'en-US': undefined,
});
});
});
@@ -134,14 +80,10 @@ describe('Context Menu Commands', () => {
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
});
});
@@ -158,10 +100,10 @@ describe('Context Menu Commands', () => {
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts(999)).toThrowError();
expect(() => getBuilder().setName('hi').setContexts(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts([999, 998])).toThrowError();
expect(() => getBuilder().setName('hi').setContexts([999, 998]).toJSON()).toThrowError();
});
});
@@ -184,10 +126,10 @@ describe('Context Menu Commands', () => {
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes(999)).toThrowError();
expect(() => getBuilder().setName('hi').setIntegrationTypes(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError();
expect(() => getBuilder().setName('hi').setIntegrationTypes([999, 998]).toJSON()).toThrowError();
});
});
});

View File

@@ -1,593 +0,0 @@
import {
ApplicationCommandType,
ApplicationIntegrationType,
ChannelType,
InteractionContextType,
PermissionFlagsBits,
type APIApplicationCommandOptionChoice,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandAssertions,
SlashCommandBooleanOption,
SlashCommandBuilder,
SlashCommandChannelOption,
SlashCommandIntegerOption,
SlashCommandMentionableOption,
SlashCommandNumberOption,
SlashCommandRoleOption,
SlashCommandAttachmentOption,
SlashCommandStringOption,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
SlashCommandUserOption,
} from '../../../src/index.js';
const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice);
const getBuilder = () => new SlashCommandBuilder();
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
const getIntegerOption = () => new SlashCommandIntegerOption().setName('owo').setDescription('Testing 123');
const getNumberOption = () => new SlashCommandNumberOption().setName('owo').setDescription('Testing 123');
const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123');
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123');
const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123');
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123');
const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123');
const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123');
const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
class Collection {
public readonly [Symbol.toStringTag] = 'Map';
}
describe('Slash Commands', () => {
describe('Assertions tests', () => {
test('GIVEN valid name THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateName('ping')).not.toThrowError();
expect(() => SlashCommandAssertions.validateName('hello-world_command')).not.toThrowError();
expect(() => SlashCommandAssertions.validateName('aˇ㐆1٢〣²अก')).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => SlashCommandAssertions.validateName(null)).toThrowError();
// Too short of a name
expect(() => SlashCommandAssertions.validateName('')).toThrowError();
// Invalid characters used
expect(() => SlashCommandAssertions.validateName('ABC')).toThrowError();
expect(() => SlashCommandAssertions.validateName('ABC123$%^&')).toThrowError();
expect(() => SlashCommandAssertions.validateName('help ping')).toThrowError();
expect(() => SlashCommandAssertions.validateName('🦦')).toThrowError();
// Too long of a name
expect(() =>
SlashCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
).toThrowError();
});
test('GIVEN valid description THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateDescription('This is an OwO moment fur sure!~')).not.toThrowError();
});
test('GIVEN invalid description THEN throw error', () => {
expect(() => SlashCommandAssertions.validateDescription(null)).toThrowError();
// Too short of a description
expect(() => SlashCommandAssertions.validateDescription('')).toThrowError();
// Too long of a description
expect(() =>
SlashCommandAssertions.validateDescription(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magnam autem libero expedita vitae accusamus nostrum ipsam tempore repudiandae deserunt ipsum facilis, velit fugiat facere accusantium, explicabo corporis aliquam non quos.',
),
).toThrowError();
});
test('GIVEN valid default_permission THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
});
test('GIVEN invalid default_permission THEN throw error', () => {
expect(() => SlashCommandAssertions.validateDefaultPermission(null)).toThrowError();
});
test('GIVEN valid array of options or choices THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(25)).not.toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
});
test('GIVEN invalid options or choices THEN throw error', () => {
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
// Given an array that's too big
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
});
test('GIVEN valid required parameters THEN does not throw error', () => {
expect(() =>
SlashCommandAssertions.validateRequiredParameters(
'owo',
'My fancy command that totally exists, to test assertions',
[],
),
).not.toThrowError();
});
});
describe('SlashCommandBuilder', () => {
describe('Builder with no options', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
});
});
describe('Builder with simple options', () => {
test('GIVEN valid builder THEN returns type included', () => {
expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput });
});
test('GIVEN valid builder with options THEN does not throw error', () => {
expect(() =>
getBuilder()
.setName('example')
.setDescription('Example command')
.setDMPermission(false)
.addBooleanOption((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) =>
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) =>
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) =>
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) =>
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addNumberOption((number) =>
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addStringOption((string) =>
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean'))).toThrowError();
});
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
expect(() =>
getBuilder().addStringOption(
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
getStringOption()
.setAutocomplete(true)
.addChoices(
{ name: 'Fancy Pants', value: 'fp_1' },
{ name: 'Fancy Shoes', value: 'fs_1' },
{ name: 'The Whole shebang', value: 'all' },
),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
),
).toThrowError();
expect(() => {
const option = getStringOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
expect(() => {
const option = getNumberOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
expect(() => {
const option = getIntegerOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
expect(() =>
getBuilder().addChannelOption(
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
),
).not.toThrowError();
expect(() => {
getBuilder().addChannelOption(
getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText),
);
}).not.toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
// @ts-expect-error: Invalid channel type
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError();
// @ts-expect-error: Invalid channel types
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
});
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
// @ts-expect-error: Invalid max value
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError();
// @ts-expect-error: Invalid max value
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError();
});
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1))).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue(1.5))).not.toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue(1))).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue(1.5))).not.toThrowError();
});
test('GIVEN an already built builder THEN does not throw an error', () => {
expect(() => getBuilder().addStringOption(getStringOption())).not.toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption())).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption())).not.toThrowError();
expect(() => getBuilder().addBooleanOption(getBooleanOption())).not.toThrowError();
expect(() => getBuilder().addUserOption(getUserOption())).not.toThrowError();
expect(() => getBuilder().addChannelOption(getChannelOption())).not.toThrowError();
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
expect(() => getBuilder().addMentionableOption(getMentionableOption())).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(() => getBuilder().addBooleanOption()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(getRoleOption())).toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('TEST_COMMAND')).toThrowError();
expect(() => getBuilder().setName('ĂĂĂĂĂĂ')).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
// Translation: a_command
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
// Translation: thx (according to GTranslate)
expect(() => getBuilder().setName('どうも')).not.toThrowError();
});
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(() => getBuilder().addBooleanOption(true)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(() => SlashCommandStringOption)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(() => new Collection())).toThrowError();
});
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
});
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
expect(() =>
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()),
).not.toThrowError();
});
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
expect(() =>
getBuilder().addStringOption(
getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }),
),
).toThrowError();
});
test('GIVEN an option, THEN setting choices should not throw an error', () => {
expect(() =>
getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })),
).not.toThrowError();
});
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDescription('foo').setNSFW(true)).not.toThrowError();
});
});
describe('Builder with subcommand (group) options', () => {
test('GIVEN builder with subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommandGroup((group) => group.setName('group').setDescription('Group us together!')),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommand((subcommand) =>
subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'),
),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN has regular slash command fields', () => {
expect(() =>
getBuilder()
.setName('name')
.setDescription('description')
.addSubcommand((option) => option.setName('ye').setDescription('ye'))
.addSubcommand((option) => option.setName('no').setDescription('no'))
.setDMPermission(false)
.setDefaultMemberPermissions(1n),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommandGroup(getSubcommandGroup())).not.toThrowError();
});
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommand(getSubcommand())).not.toThrowError();
});
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())),
).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()).addInteger(getInteger()),
).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(() => getBuilder().addSubcommandGroup()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addSubcommand()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addSubcommand(getSubcommandGroup())).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()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup())).toThrowError();
});
test('GIVEN a valid subcommand THEN does not throw an error', () => {
expect(() =>
getSubcommandGroup()
.addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123'))
.toJSON(),
).not.toThrowError();
});
});
describe('Subcommand builder', () => {
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError();
});
});
describe('Slash command localizations', () => {
const expectedSingleLocale = { 'en-US': 'foobar' };
const expectedMultipleLocales = {
...expectedSingleLocale,
bg: 'test',
};
test('GIVEN valid name localizations THEN does not throw error', () => {
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
expectedMultipleLocales,
);
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
'en-US': null,
});
});
test('GIVEN valid description localizations THEN does not throw error', () => {
expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization description
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error: Invalid localization description
expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError();
});
test('GIVEN valid description localizations THEN valid data is stored', () => {
expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual(
expectedSingleLocale,
);
expect(
getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations,
).toEqual(expectedMultipleLocales);
expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull();
expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({
'en-US': null,
});
});
});
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
});
test('GIVEN valid permission with options THEN does not throw error', () => {
expect(() =>
getBuilder().addBooleanOption(getBooleanOption()).setDefaultMemberPermissions('1'),
).not.toThrowError();
expect(() => getBuilder().addChannelOption(getChannelOption()).setDMPermission(false)).not.toThrowError();
});
});
describe('contexts', () => {
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
expect(() =>
getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]),
).not.toThrowError();
expect(() =>
getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM),
).not.toThrowError();
});
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts(999)).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts([999, 998])).toThrowError();
});
});
describe('integration types', () => {
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
expect(() =>
getBuilder().setIntegrationTypes([
ApplicationIntegrationType.GuildInstall,
ApplicationIntegrationType.UserInstall,
]),
).not.toThrowError();
expect(() =>
getBuilder().setIntegrationTypes(
ApplicationIntegrationType.GuildInstall,
ApplicationIntegrationType.UserInstall,
),
).not.toThrowError();
});
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes(999)).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError();
});
});
});
});

View File

@@ -1,71 +1,21 @@
import {
ComponentType,
TextInputStyle,
type APIModalInteractionResponseCallbackData,
type APITextInputComponent,
} from 'discord-api-types/v10';
import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
ModalBuilder,
TextInputBuilder,
type ModalActionRowComponentBuilder,
} from '../../src/index.js';
import {
componentsValidator,
titleValidator,
validateRequiredParameters,
} from '../../src/interactions/modals/Assertions.js';
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js';
const modal = () => new ModalBuilder();
const textInput = () =>
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short),
);
describe('Modals', () => {
describe('Assertion Tests', () => {
test('GIVEN valid title THEN validator does not throw', () => {
expect(() => titleValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid title THEN validator does throw', () => {
expect(() => titleValidator.parse(42)).toThrowError();
});
test('GIVEN valid components THEN validator does not throw', () => {
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
});
test('GIVEN invalid components THEN validator does throw', () => {
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
});
test('GIVEN valid required parameters THEN validator does not throw', () => {
expect(() =>
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
).not.toThrowError();
});
test('GIVEN invalid required parameters THEN validator does throw', () => {
expect(() =>
// @ts-expect-error: Missing required parameter
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
).toThrowError();
});
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() =>
modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()),
).not.toThrowError();
expect(() =>
// @ts-expect-error: You can pass a TextInputBuilder and it will add it to an action row
modal().setTitle('test').setCustomId('foobar').addComponents(new TextInputBuilder()),
).not.toThrowError();
expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError();
expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError();
});
test('GIVEN invalid fields THEN builder does throw', () => {
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
// @ts-expect-error: CustomId is invalid
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
});
@@ -106,68 +56,17 @@ describe('Modals', () => {
modal()
.setTitle(modalData.title)
.setCustomId('custom id')
.setComponents(
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
.setActionRows(
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
)
.addComponents([
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
.addActionRows([
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
])
.toJSON(),
).toEqual(modalData);
});
describe('equals()', () => {
const textInput1 = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label')
.setStyle(TextInputStyle.Paragraph);
const textInput2: APITextInputComponent = {
type: ComponentType.TextInput,
custom_id: 'custom id',
label: 'label',
style: TextInputStyle.Paragraph,
};
test('GIVEN equal builders THEN returns true', () => {
const equalTextInput = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label')
.setStyle(TextInputStyle.Paragraph);
expect(textInput1.equals(equalTextInput)).toBeTruthy();
});
test('GIVEN the same builder THEN returns true', () => {
expect(textInput1.equals(textInput1)).toBeTruthy();
});
test('GIVEN equal builder and data THEN returns true', () => {
expect(textInput1.equals(textInput2)).toBeTruthy();
});
test('GIVEN different builders THEN returns false', () => {
const diffTextInput = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label 2')
.setStyle(TextInputStyle.Paragraph);
expect(textInput1.equals(diffTextInput)).toBeFalsy();
});
test('GIVEN different text input builder and data THEN returns false', () => {
const diffTextInputData: APITextInputComponent = {
type: ComponentType.TextInput,
custom_id: 'custom id',
label: 'label 2',
style: TextInputStyle.Short,
};
expect(textInput1.equals(diffTextInputData)).toBeFalsy();
});
});
});

View File

@@ -3,6 +3,16 @@ import { EmbedBuilder, embedLength } from '../../src/index.js';
const alpha = 'abcdefghijklmnopqrstuvwxyz';
const dummy = {
title: 'ooooo aaaaa uuuuuu aaaa',
};
const base = {
author: undefined,
fields: [],
footer: undefined,
};
describe('Embed', () => {
describe('Embed getters', () => {
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
@@ -14,127 +24,136 @@ describe('Embed', () => {
footer: { text: alpha },
});
expect(embedLength(embed.data)).toEqual(alpha.length * 6);
expect(embedLength(embed.toJSON())).toEqual(alpha.length * 6);
});
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
const embed = new EmbedBuilder();
expect(embedLength(embed.data)).toEqual(0);
expect(embedLength(embed.toJSON(false))).toEqual(0);
});
});
describe('Embed title', () => {
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
});
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setTitle('foo');
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ title: 'foo' });
embed.setTitle(null);
const embed = new EmbedBuilder({ title: 'foo', description: ':3' });
embed.clearTitle();
expect(embed.toJSON()).toStrictEqual({ title: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, description: ':3', title: undefined });
});
test('GIVEN an embed with an invalid title THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
embed.setTitle('a'.repeat(257));
expect(() => embed.toJSON()).toThrowError();
});
});
describe('Embed description', () => {
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
});
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setDescription('foo');
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
});
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ description: 'foo' });
embed.setDescription(null);
const embed = new EmbedBuilder({ description: 'foo', ...dummy });
embed.clearDescription();
expect(embed.toJSON()).toStrictEqual({ description: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, description: undefined });
});
test('GIVEN an embed with an invalid description THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setDescription('a'.repeat(4_097))).toThrowError();
embed.setDescription('a'.repeat(4_097));
expect(() => embed.toJSON()).toThrowError();
});
});
describe('Embed URL', () => {
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
const embed = new EmbedBuilder({ url: 'https://discord.js.org/', ...dummy });
expect(embed.toJSON()).toStrictEqual({
...base,
...dummy,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
const embed = new EmbedBuilder(dummy);
embed.setURL('https://discord.js.org/');
expect(embed.toJSON()).toStrictEqual({
...base,
...dummy,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
embed.setURL(null);
const embed = new EmbedBuilder({ url: 'https://discord.js.org', ...dummy });
embed.clearURL();
expect(embed.toJSON()).toStrictEqual({ url: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, url: undefined });
});
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
const embed = new EmbedBuilder();
expect(() => embed.setURL(input)).toThrowError();
embed.setURL(input);
expect(() => embed.toJSON()).toThrowError();
});
});
describe('Embed Color', () => {
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ color: 0xff0000 });
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
const embed = new EmbedBuilder({ color: 0xff0000, ...dummy });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
});
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
expect(new EmbedBuilder(dummy).setColor(0xff0000).toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
});
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ color: 0xff0000 });
embed.setColor(null);
const embed = new EmbedBuilder({ ...dummy, color: 0xff0000 });
embed.clearColor();
expect(embed.toJSON()).toStrictEqual({ color: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: undefined });
});
test('GIVEN an embed with an invalid color THEN throws error', () => {
const embed = new EmbedBuilder();
// @ts-expect-error: Invalid color
expect(() => embed.setColor('RED')).toThrowError();
embed.setColor('RED');
expect(() => embed.toJSON()).toThrowError();
// @ts-expect-error: Invalid color
expect(() => embed.setColor([42, 36])).toThrowError();
expect(() => embed.setColor([42, 36, 1_000])).toThrowError();
embed.setColor([42, 36]);
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -142,98 +161,92 @@ describe('Embed', () => {
const now = new Date();
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
});
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
test('GIVEN an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder(dummy);
embed.setTimestamp(now);
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
const embed = new EmbedBuilder(dummy);
embed.setTimestamp(now.getTime());
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
const embed = new EmbedBuilder(dummy);
embed.setTimestamp();
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: embed.toJSON().timestamp });
});
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
embed.setTimestamp(null);
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
embed.clearTimestamp();
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: undefined });
});
});
describe('Embed Thumbnail', () => {
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
});
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setThumbnail('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
});
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
embed.setThumbnail(null);
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, ...dummy });
embed.clearThumbnail();
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, thumbnail: undefined });
});
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setThumbnail('owo')).toThrowError();
embed.setThumbnail('owo');
expect(() => embed.toJSON()).toThrowError();
});
});
describe('Embed Image', () => {
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
image: { url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
});
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setImage('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
image: { url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
});
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
embed.setImage(null);
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' }, ...dummy });
embed.clearImage();
expect(embed.toJSON()).toStrictEqual({ image: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, image: undefined });
});
test('GIVEN an embed with an invalid image THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setImage('owo')).toThrowError();
embed.setImage('owo');
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -243,19 +256,19 @@ describe('Embed', () => {
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
expect(embed.toJSON()).toStrictEqual({
...base,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setAuthor({
name: 'Wumpus',
iconURL: 'https://discord.js.org/static/logo.svg',
url: 'https://discord.js.org',
});
embed.setAuthor((author) =>
author.setName('Wumpus').setIconURL('https://discord.js.org/static/logo.svg').setURL('https://discord.js.org'),
);
expect(embed.toJSON()).toStrictEqual({
...base,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
@@ -263,16 +276,18 @@ describe('Embed', () => {
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
...dummy,
});
embed.setAuthor(null);
embed.clearAuthor();
expect(embed.toJSON()).toStrictEqual({ author: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, author: undefined });
});
test('GIVEN an embed with an invalid author name THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
embed.setAuthor({ name: 'a'.repeat(257) });
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -282,32 +297,36 @@ describe('Embed', () => {
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({
...base,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
embed.setFooter({ text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' });
expect(embed.toJSON()).toStrictEqual({
...base,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({
...dummy,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
embed.setFooter(null);
embed.clearFooter();
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, footer: undefined });
});
test('GIVEN an embed with invalid footer text THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() => embed.setFooter({ text: 'a'.repeat(2_049) })).toThrowError();
embed.setFooter({ text: 'a'.repeat(2_049) });
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -316,9 +335,7 @@ describe('Embed', () => {
const embed = new EmbedBuilder({
fields: [{ name: 'foo', value: 'bar' }],
});
expect(embed.toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'bar' }],
});
expect(embed.toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'bar' }] });
});
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
@@ -327,6 +344,7 @@ describe('Embed', () => {
embed.addFields([{ name: 'foo', value: 'bar' }]);
expect(embed.toJSON()).toStrictEqual({
...base,
fields: [
{ name: 'foo', value: 'bar' },
{ name: 'foo', value: 'bar' },
@@ -338,56 +356,51 @@ describe('Embed', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'baz' }],
});
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'baz' }] });
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
});
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
});
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
expect(() =>
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
});
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
});
describe('GIVEN invalid field amount THEN throws error', () => {
test('1', () => {
const embed = new EmbedBuilder();
expect(() =>
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -395,7 +408,8 @@ describe('Embed', () => {
test('2', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
embed.addFields({ name: '', value: 'bar' });
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -403,7 +417,8 @@ describe('Embed', () => {
test('3', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
embed.addFields({ name: 'a'.repeat(257), value: 'bar' });
expect(() => embed.toJSON()).toThrowError();
});
});
@@ -411,7 +426,8 @@ describe('Embed', () => {
test('4', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError();
embed.addFields({ name: '', value: 'a'.repeat(1_025) });
expect(() => embed.toJSON()).toThrowError();
});
});
});

View File

@@ -1,11 +1,15 @@
import { expectTypeOf } from 'vitest';
import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandSubcommandBuilder } from '../src/index.js';
import {
ChatInputCommandBuilder,
ChatInputCommandStringOption,
ChatInputCommandSubcommandBuilder,
} from '../src/index.js';
const getBuilder = () => new SlashCommandBuilder();
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
const getBuilder = () => new ChatInputCommandBuilder();
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
type BuilderPropsOnly<Type = SlashCommandBuilder> = Pick<
type BuilderPropsOnly<Type = ChatInputCommandBuilder> = Pick<
Type,
keyof {
[Key in keyof Type as Type[Key] extends (...args: any) => any ? never : Key]: any;