Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add components to /builders (#7195)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
- Loading branch information
1 parent
37a22e0
commit 2bb40fd
Showing
11 changed files
with
777 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { APIActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9'; | ||
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src'; | ||
|
||
describe('Action Row Components', () => { | ||
describe('Assertion Tests', () => { | ||
test('GIVEN valid components THEN do not throw', () => { | ||
expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError(); | ||
expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError(); | ||
}); | ||
|
||
test('GIVEN valid JSON input THEN valid JSON output is given', () => { | ||
const actionRowData: APIActionRowComponent = { | ||
type: ComponentType.ActionRow, | ||
components: [ | ||
{ | ||
type: ComponentType.Button, | ||
label: 'button', | ||
style: ButtonStyle.Primary, | ||
custom_id: 'test', | ||
}, | ||
{ | ||
type: ComponentType.Button, | ||
label: 'link', | ||
style: ButtonStyle.Link, | ||
url: 'https://google.com', | ||
}, | ||
{ | ||
type: ComponentType.SelectMenu, | ||
placeholder: 'test', | ||
custom_id: 'test', | ||
options: [ | ||
{ | ||
label: 'option', | ||
value: 'option', | ||
}, | ||
], | ||
}, | ||
], | ||
}; | ||
|
||
expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData); | ||
expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); | ||
expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); | ||
// @ts-expect-error | ||
expect(() => createComponent({ type: 42, components: [] })).toThrowError(); | ||
}); | ||
test('GIVEN valid builder options THEN valid JSON output is given', () => { | ||
const rowWithButtonData: APIActionRowComponent = { | ||
type: ComponentType.ActionRow, | ||
components: [ | ||
{ | ||
type: ComponentType.Button, | ||
label: 'test', | ||
custom_id: '123', | ||
style: ButtonStyle.Primary, | ||
}, | ||
], | ||
}; | ||
|
||
const rowWithSelectMenuData: APIActionRowComponent = { | ||
type: ComponentType.ActionRow, | ||
components: [ | ||
{ | ||
type: ComponentType.SelectMenu, | ||
custom_id: '1234', | ||
options: [ | ||
{ | ||
label: 'one', | ||
value: 'one', | ||
}, | ||
{ | ||
label: 'two', | ||
value: 'two', | ||
}, | ||
], | ||
max_values: 10, | ||
min_values: 12, | ||
}, | ||
], | ||
}; | ||
|
||
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); | ||
const selectMenu = new SelectMenuComponent() | ||
.setCustomId('1234') | ||
.setMaxValues(10) | ||
.setMinValues(12) | ||
.setOptions([ | ||
new SelectMenuOption().setLabel('one').setValue('one'), | ||
new SelectMenuOption().setLabel('two').setValue('two'), | ||
]); | ||
|
||
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData); | ||
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { | ||
APIButtonComponentWithCustomId, | ||
APIButtonComponentWithURL, | ||
ButtonStyle, | ||
ComponentType, | ||
} from 'discord-api-types/v9'; | ||
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions'; | ||
import { ButtonComponent } from '../../src/components/Button'; | ||
|
||
const buttonComponent = () => new ButtonComponent(); | ||
|
||
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 not 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(() => { | ||
const button = buttonComponent() | ||
.setCustomId('custom') | ||
.setStyle(ButtonStyle.Primary) | ||
.setDisabled(true) | ||
.setEmoji({ name: 'test' }); | ||
|
||
button.toJSON(); | ||
}).not.toThrowError(); | ||
|
||
expect(() => buttonComponent().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(); | ||
}).toThrowError(); | ||
|
||
expect(() => { | ||
// @ts-expect-error | ||
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'); | ||
button.toJSON(); | ||
}).toThrowError(); | ||
|
||
expect(() => { | ||
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test'); | ||
button.toJSON(); | ||
}).toThrowError(); | ||
|
||
expect(() => buttonComponent().setStyle(24)).toThrowError(); | ||
expect(() => buttonComponent().setLabel(longStr)).toThrowError(); | ||
// @ts-expect-error | ||
expect(() => buttonComponent().setDisabled(0)).toThrowError(); | ||
// @ts-expect-error | ||
expect(() => buttonComponent().setEmoji('foo')).toThrowError(); | ||
|
||
expect(() => buttonComponent().setURL('foobar')).toThrowError(); | ||
}); | ||
|
||
test('GiVEN valid input THEN valid JSON outputs are given', () => { | ||
const interactionData: APIButtonComponentWithCustomId = { | ||
type: ComponentType.Button, | ||
custom_id: 'test', | ||
label: 'test', | ||
style: ButtonStyle.Primary, | ||
disabled: true, | ||
}; | ||
|
||
expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData); | ||
|
||
expect( | ||
buttonComponent() | ||
.setCustomId(interactionData.custom_id) | ||
.setLabel(interactionData.label) | ||
.setStyle(interactionData.style) | ||
.setDisabled(interactionData.disabled) | ||
.toJSON(), | ||
).toEqual(interactionData); | ||
|
||
const linkData: APIButtonComponentWithURL = { | ||
type: ComponentType.Button, | ||
label: 'test', | ||
style: ButtonStyle.Link, | ||
disabled: true, | ||
url: 'https://google.com', | ||
}; | ||
|
||
expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData); | ||
|
||
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url)); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9'; | ||
import { SelectMenuComponent, SelectMenuOption } from '../../src/index'; | ||
|
||
const selectMenu = () => new SelectMenuComponent(); | ||
const selectMenuOption = () => new SelectMenuOption(); | ||
|
||
const longStr = | ||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; | ||
|
||
describe('Button Components', () => { | ||
describe('Assertion Tests', () => { | ||
test('GIVEN valid inputs THEN Select Menu does not throw', () => { | ||
expect(() => selectMenu().setCustomId('foo')).not.toThrowError(); | ||
expect(() => selectMenu().setMaxValues(10)).not.toThrowError(); | ||
expect(() => selectMenu().setMinValues(3)).not.toThrowError(); | ||
expect(() => selectMenu().setDisabled(true)).not.toThrowError(); | ||
expect(() => selectMenu().setPlaceholder('description')).not.toThrowError(); | ||
|
||
const option = selectMenuOption() | ||
.setLabel('test') | ||
.setValue('test') | ||
.setDefault(true) | ||
.setEmoji({ name: 'test' }) | ||
.setDescription('description'); | ||
expect(() => selectMenu().addOptions(option)).not.toThrowError(); | ||
expect(() => selectMenu().setOptions([option])).not.toThrowError(); | ||
}); | ||
|
||
test('GIVEN invalid inputs THEN Select Menu does throw', () => { | ||
expect(() => selectMenu().setCustomId(longStr)).toThrowError(); | ||
expect(() => selectMenu().setMaxValues(30)).toThrowError(); | ||
expect(() => selectMenu().setMinValues(-20)).toThrowError(); | ||
// @ts-expect-error | ||
expect(() => selectMenu().setDisabled(0)).toThrowError(); | ||
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError(); | ||
|
||
expect(() => { | ||
selectMenuOption() | ||
.setLabel(longStr) | ||
.setValue(longStr) | ||
// @ts-expect-error | ||
.setDefault(-1) | ||
// @ts-expect-error | ||
.setEmoji({ name: 1 }) | ||
.setDescription(longStr); | ||
}).toThrowError(); | ||
}); | ||
|
||
test('GIVEN valid JSON input THEN valid JSON history is correct', () => { | ||
const selectMenuOptionData: APISelectMenuOption = { | ||
label: 'test', | ||
value: 'test', | ||
emoji: { name: 'test' }, | ||
default: true, | ||
description: 'test', | ||
}; | ||
|
||
const selectMenuData: APISelectMenuComponent = { | ||
type: ComponentType.SelectMenu, | ||
custom_id: 'test', | ||
max_values: 10, | ||
min_values: 3, | ||
disabled: true, | ||
options: [selectMenuOptionData], | ||
placeholder: 'test', | ||
}; | ||
|
||
expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData); | ||
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; | ||
import type { ButtonComponent, SelectMenuComponent } from '..'; | ||
import type { Component } from './Component'; | ||
import { createComponent } from './Components'; | ||
|
||
export type ActionRowComponent = ButtonComponent | SelectMenuComponent; | ||
|
||
// TODO: Add valid form component types | ||
|
||
/** | ||
* Represents an action row component | ||
*/ | ||
export class ActionRow<T extends ActionRowComponent> implements Component { | ||
public readonly components: T[] = []; | ||
public readonly type = ComponentType.ActionRow; | ||
|
||
public constructor(data?: APIActionRowComponent) { | ||
this.components = (data?.components.map(createComponent) ?? []) as T[]; | ||
} | ||
|
||
/** | ||
* Adds components to this action row. | ||
* @param components The components to add to this action row. | ||
* @returns | ||
*/ | ||
public addComponents(...components: T[]) { | ||
this.components.push(...components); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets the components in this action row | ||
* @param components The components to set this row to | ||
*/ | ||
public setComponents(components: T[]) { | ||
Reflect.set(this, 'components', [...components]); | ||
return this; | ||
} | ||
|
||
public toJSON(): APIActionRowComponent { | ||
return { | ||
...this, | ||
components: this.components.map((component) => component.toJSON()), | ||
}; | ||
} | ||
} |
Oops, something went wrong.