Skip to content

Commit

Permalink
refactor: builders
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Almeida <[email protected]>
  • Loading branch information
3 people committed Sep 25, 2024
1 parent cda8d88 commit 291ed99
Show file tree
Hide file tree
Showing 91 changed files with 3,832 additions and 3,895 deletions.
73 changes: 41 additions & 32 deletions packages/builders/__tests__/components/actionRow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
PrimaryButtonBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from '../../src/index.js';
Expand Down Expand Up @@ -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,
Expand All @@ -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();
});

Expand Down Expand Up @@ -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'),
Expand All @@ -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();
});
});
});
131 changes: 17 additions & 114 deletions packages/builders/__tests__/components/button.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,157 +5,63 @@ 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, SKUIdButtonBuilder, URLButtonBuilder } 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' });

button.toJSON();
}).not.toThrowError();

expect(() => {
const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium);
const button = new SKUIdButtonBuilder().setSKUId('123456789012345678');
button.toJSON();
}).not.toThrowError();

expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError();
expect(() => new URLButtonBuilder().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');
button.toJSON();
}).toThrowError();

expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary);
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');
const button = new PrimaryButtonBuilder().setEmoji('test');
button.toJSON();
}).toThrowError();

expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test');
const button = new PrimaryButtonBuilder();
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', () => {
Expand All @@ -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);
Expand All @@ -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 URLButtonBuilder(linkData).toJSON()).toEqual(linkData);
});
});
});
6 changes: 3 additions & 3 deletions packages/builders/__tests__/components/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down
Loading

0 comments on commit 291ed99

Please sign in to comment.