feat(builders)!: Support select in modals (#11034)

BREAKING CHANGE: Text inputs no longer accept a label.
BREAKING CHANGE: Modals now only set labels instead of action rows.
This commit is contained in:
Jiralite
2025-09-05 20:56:14 +04:00
committed by GitHub
parent ddf9f818e8
commit f7c77a73de
13 changed files with 364 additions and 115 deletions

View File

@@ -0,0 +1,109 @@
import type { APILabelComponent, APIStringSelectComponent, APITextInputComponent } from 'discord-api-types/v10';
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { LabelBuilder } from '../../src/index.js';
describe('Label components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() =>
new LabelBuilder()
.setLabel('label')
.setStringSelectMenuComponent((stringSelectMenu) =>
stringSelectMenu
.setCustomId('test')
.setOptions((stringSelectMenuOption) => stringSelectMenuOption.setLabel('label').setValue('value'))
.setRequired(),
)
.toJSON(),
).not.toThrow();
expect(() =>
new LabelBuilder()
.setLabel('label')
.setId(5)
.setTextInputComponent((textInput) =>
textInput.setCustomId('test').setStyle(TextInputStyle.Paragraph).setRequired(),
)
.toJSON(),
).not.toThrow();
});
test('GIVEN invalid fields THEN build does throw', () => {
expect(() => new LabelBuilder().toJSON()).toThrow();
expect(() => new LabelBuilder().setId(5).toJSON()).toThrow();
expect(() => new LabelBuilder().setLabel('label').toJSON()).toThrow();
expect(() =>
new LabelBuilder()
.setLabel('l'.repeat(1_000))
.setStringSelectMenuComponent((stringSelectMenu) => stringSelectMenu)
.toJSON(),
).toThrow();
});
test('GIVEN valid input THEN valid JSON outputs are given', () => {
const labelWithTextInputData = {
type: ComponentType.Label,
component: {
type: ComponentType.TextInput,
custom_id: 'custom_id',
placeholder: 'placeholder',
style: TextInputStyle.Paragraph,
} satisfies APITextInputComponent,
label: 'label',
description: 'description',
id: 5,
} satisfies APILabelComponent;
const labelWithStringSelectData = {
type: ComponentType.Label,
component: {
type: ComponentType.StringSelect,
custom_id: 'custom_id',
placeholder: 'placeholder',
options: [
{ label: 'first', value: 'first' },
{ label: 'second', value: 'second' },
],
required: true,
} satisfies APIStringSelectComponent,
label: 'label',
description: 'description',
id: 5,
} satisfies APILabelComponent;
expect(new LabelBuilder(labelWithTextInputData).toJSON()).toEqual(labelWithTextInputData);
expect(new LabelBuilder(labelWithStringSelectData).toJSON()).toEqual(labelWithStringSelectData);
expect(
new LabelBuilder()
.setTextInputComponent((textInput) =>
textInput.setCustomId('custom_id').setPlaceholder('placeholder').setStyle(TextInputStyle.Paragraph),
)
.setLabel('label')
.setDescription('description')
.setId(5)
.toJSON(),
).toEqual(labelWithTextInputData);
expect(
new LabelBuilder()
.setStringSelectMenuComponent((stringSelectMenu) =>
stringSelectMenu
.setCustomId('custom_id')
.setPlaceholder('placeholder')
.setOptions(
(stringSelectMenuOption) => stringSelectMenuOption.setLabel('first').setValue('first'),
(stringSelectMenuOption) => stringSelectMenuOption.setLabel('second').setValue('second'),
)
.setRequired(),
)
.setLabel('label')
.setDescription('description')
.setId(5)
.toJSON(),
).toEqual(labelWithStringSelectData);
});
});
});

View File

@@ -8,13 +8,12 @@ describe('Text Input Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => {
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
textInputComponent().setCustomId('foobar').setStyle(TextInputStyle.Paragraph).toJSON();
}).not.toThrowError();
expect(() => {
textInputComponent()
.setCustomId('foobar')
.setLabel('test')
.setMaxLength(100)
.setMinLength(1)
.setPlaceholder('bar')
@@ -24,7 +23,7 @@ describe('Text Input Components', () => {
}).not.toThrowError();
expect(() => {
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON();
textInputComponent().setCustomId('Custom').setStyle(TextInputStyle.Short).toJSON();
}).not.toThrowError();
});
});
@@ -33,10 +32,10 @@ describe('Text Input Components', () => {
expect(() => textInputComponent().toJSON()).toThrowError();
expect(() => {
textInputComponent()
.setCustomId('test')
.setCustomId('a'.repeat(500))
.setMaxLength(100)
.setPlaceholder('hello')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('a'.repeat(500))
.setStyle(3 as TextInputStyle)
.toJSON();
}).toThrowError();
});
@@ -44,7 +43,6 @@ describe('Text Input Components', () => {
test('GIVEN valid input THEN valid JSON outputs are given', () => {
const textInputData = {
type: ComponentType.TextInput,
label: 'label',
custom_id: 'custom id',
placeholder: 'placeholder',
max_length: 100,
@@ -58,11 +56,10 @@ describe('Text Input Components', () => {
expect(
textInputComponent()
.setCustomId(textInputData.custom_id)
.setLabel(textInputData.label)
.setPlaceholder(textInputData.placeholder!)
.setMaxLength(textInputData.max_length!)
.setMinLength(textInputData.min_length!)
.setValue(textInputData.value!)
.setPlaceholder(textInputData.placeholder)
.setMaxLength(textInputData.max_length)
.setMinLength(textInputData.min_length)
.setValue(textInputData.value)
.setRequired(textInputData.required)
.setStyle(textInputData.style)
.toJSON(),

View File

@@ -1,17 +1,22 @@
import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js';
import { ModalBuilder, TextInputBuilder, LabelBuilder } from '../../src/index.js';
const modal = () => new ModalBuilder();
const textInput = () =>
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short),
);
const label = () =>
new LabelBuilder()
.setLabel('label')
.setTextInputComponent(new TextInputBuilder().setCustomId('text').setStyle(TextInputStyle.Short));
describe('Modals', () => {
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError();
expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError();
expect(() =>
modal().setTitle('test').setCustomId('foobar').setLabelComponents(label()).toJSON(),
).not.toThrowError();
expect(() =>
modal().setTitle('test').setCustomId('foobar').setLabelComponents(label()).toJSON(),
).not.toThrowError();
});
test('GIVEN invalid fields THEN builder does throw', () => {
@@ -21,34 +26,33 @@ describe('Modals', () => {
});
test('GIVEN valid input THEN valid JSON outputs are given', () => {
const modalData: APIModalInteractionResponseCallbackData = {
const modalData = {
title: 'title',
custom_id: 'custom id',
components: [
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.TextInput,
label: 'label',
style: TextInputStyle.Paragraph,
custom_id: 'custom id',
},
],
type: ComponentType.Label,
id: 33,
label: 'label',
description: 'description',
component: {
type: ComponentType.TextInput,
style: TextInputStyle.Paragraph,
custom_id: 'custom id',
},
},
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.TextInput,
label: 'label',
style: TextInputStyle.Paragraph,
custom_id: 'custom id',
},
],
type: ComponentType.Label,
label: 'label',
description: 'description',
component: {
type: ComponentType.TextInput,
style: TextInputStyle.Paragraph,
custom_id: 'custom id',
},
},
],
};
} satisfies APIModalInteractionResponseCallbackData;
expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData);
@@ -56,16 +60,19 @@ describe('Modals', () => {
modal()
.setTitle(modalData.title)
.setCustomId('custom id')
.setActionRows(
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
.setLabelComponents(
new LabelBuilder()
.setId(33)
.setLabel('label')
.setDescription('description')
.setTextInputComponent(new TextInputBuilder().setCustomId('custom id').setStyle(TextInputStyle.Paragraph)),
)
.addLabelComponents(
new LabelBuilder()
.setLabel('label')
.setDescription('description')
.setTextInputComponent(new TextInputBuilder().setCustomId('custom id').setStyle(TextInputStyle.Paragraph)),
)
.addActionRows([
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
])
.toJSON(),
).toEqual(modalData);
});

View File

@@ -19,7 +19,7 @@ describe('Message', () => {
test('GIVEN bad action row THEN it throws', () => {
const message = new MessageBuilder().addActionRowComponents((row) =>
row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')),
row.addTextInputComponent((input) => input.setCustomId('abc')),
);
expect(() => message.toJSON()).toThrow();
});