Skip to content

Commit

Permalink
feat: add missing v13 component methods (#7466)
Browse files Browse the repository at this point in the history
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
  • Loading branch information
4 people committed Feb 18, 2022
1 parent 395a68f commit f7257f0
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 76 deletions.
72 changes: 38 additions & 34 deletions packages/builders/__tests__/components/actionRow.test.ts
@@ -1,6 +1,40 @@
import { APIActionRowComponent, APIMessageComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';

const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.Button,
label: 'test',
custom_id: '123',
style: ButtonStyle.Primary,
},
],
};

const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
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,
},
],
};

describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
Expand Down Expand Up @@ -45,40 +79,6 @@ describe('Action Row Components', () => {
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.Button,
label: 'test',
custom_id: '123',
style: ButtonStyle.Primary,
},
],
};

const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
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')
Expand All @@ -92,5 +92,9 @@ describe('Action Row Components', () => {
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
});
test('Given JSON data THEN builder is equal to it and itself', () => {
expect(new ActionRow(rowWithSelectMenuData).equals(rowWithSelectMenuData)).toBeTruthy();
expect(new ActionRow(rowWithButtonData).equals(new ActionRow(rowWithButtonData))).toBeTruthy();
});
});
});
12 changes: 12 additions & 0 deletions packages/builders/__tests__/components/button.test.ts
Expand Up @@ -142,5 +142,17 @@ describe('Button Components', () => {

expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
});
test('Given JSON data THEN builder is equal to it and itself', () => {
const buttonData: APIButtonComponentWithCustomId = {
type: ComponentType.Button,
custom_id: 'test',
label: 'test',
style: ButtonStyle.Primary,
disabled: true,
};

expect(new ButtonComponent(buttonData).equals(buttonData)).toBeTruthy();
expect(new ButtonComponent(buttonData).equals(new ButtonComponent(buttonData))).toBeTruthy();
});
});
});
52 changes: 29 additions & 23 deletions packages/builders/__tests__/components/selectMenu.test.ts
Expand Up @@ -7,7 +7,29 @@ const selectMenuOption = () => new SelectMenuOption();
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';

describe('Button Components', () => {
const selectMenuOptionData: APISelectMenuOption = {
label: 'test',
value: 'test',
emoji: { name: 'test' },
default: true,
description: 'test',
};

const selectMenuDataWithoutOptions = {
type: ComponentType.SelectMenu,
custom_id: 'test',
max_values: 10,
min_values: 3,
disabled: true,
placeholder: 'test',
} as const;

const selectMenuData: APISelectMenuComponent = {
...selectMenuDataWithoutOptions,
options: [selectMenuOptionData],
};

describe('Select Menu Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
Expand All @@ -24,6 +46,7 @@ describe('Button Components', () => {
.setDescription('description');
expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
});

test('GIVEN invalid inputs THEN Select Menu does throw', () => {
Expand All @@ -47,34 +70,17 @@ describe('Button Components', () => {
});

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 selectMenuDataWithoutOptions = {
type: ComponentType.SelectMenu,
custom_id: 'test',
max_values: 10,
min_values: 3,
disabled: true,
placeholder: 'test',
} as const;

const selectMenuData: APISelectMenuComponent = {
...selectMenuDataWithoutOptions,
options: [selectMenuOptionData],
};

expect(
new SelectMenuComponent(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOption(selectMenuOptionData))
.toJSON(),
).toEqual(selectMenuData);
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});

test('Given JSON data THEN builder is equal to it and itself', () => {
expect(new SelectMenuComponent(selectMenuData).equals(selectMenuData)).toBeTruthy();
expect(new SelectMenuComponent(selectMenuData).equals(new SelectMenuComponent(selectMenuData))).toBeTruthy();
});
});
});
9 changes: 5 additions & 4 deletions packages/builders/__tests__/messages/embed.test.ts
Expand Up @@ -115,10 +115,8 @@ describe('Embed', () => {
});

test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setColor(0xff0000);

expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new Embed().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new Embed().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
});

test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
Expand All @@ -133,6 +131,9 @@ describe('Embed', () => {

// @ts-expect-error
expect(() => embed.setColor('RED')).toThrowError();
// @ts-expect-error
expect(() => embed.setColor([42, 36])).toThrowError();
expect(() => embed.setColor([42, 36, 1000])).toThrowError();
});
});

Expand Down
1 change: 1 addition & 0 deletions packages/builders/package.json
Expand Up @@ -53,6 +53,7 @@
"dependencies": {
"@sindresorhus/is": "^4.4.0",
"discord-api-types": "^0.27.0",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
Expand Down
11 changes: 11 additions & 0 deletions packages/builders/src/components/ActionRow.ts
Expand Up @@ -2,6 +2,7 @@ import { type APIActionRowComponent, ComponentType, APIMessageComponent } from '
import type { ButtonComponent, SelectMenuComponent } from '..';
import { Component } from './Component';
import { createComponent } from './Components';
import isEqual from 'fast-deep-equal';

export type MessageComponent = ActionRowComponent | ActionRow;

Expand Down Expand Up @@ -46,4 +47,14 @@ export class ActionRow<T extends ActionRowComponent = ActionRowComponent> extend
components: this.components.map((component) => component.toJSON()),
};
}

public equals(other: APIActionRowComponent<APIMessageComponent> | ActionRow) {
if (other instanceof ActionRow) {
return isEqual(other.data, this.data) && isEqual(other.components, this.components);
}
return isEqual(other, {
...this.data,
components: this.components.map((component) => component.toJSON()),
});
}
}
15 changes: 10 additions & 5 deletions packages/builders/src/components/Component.ts
@@ -1,5 +1,11 @@
import type { JSONEncodable } from '../util/jsonEncodable';
import type { APIBaseComponent, APIMessageComponent, ComponentType } from 'discord-api-types/v9';
import type {
APIActionRowComponentTypes,
APIBaseComponent,
APIMessageComponent,
ComponentType,
} from 'discord-api-types/v9';
import type { Equatable } from '../util/equatable';

/**
* Represents a discord component
Expand All @@ -8,18 +14,17 @@ export abstract class Component<
DataType extends Partial<APIBaseComponent<ComponentType>> & {
type: ComponentType;
} = APIBaseComponent<ComponentType>,
> implements JSONEncodable<APIMessageComponent>
> implements JSONEncodable<APIMessageComponent>, Equatable<Component | APIActionRowComponentTypes>
{
/**
* The API data associated with this component
*/
protected readonly data: DataType;

/**
* Converts this component to an API-compatible JSON object
*/
public abstract toJSON(): APIMessageComponent;

public abstract equals(other: Component | APIActionRowComponentTypes): boolean;

public constructor(data: DataType) {
this.data = data;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/builders/src/components/button/UnsafeButton.ts
Expand Up @@ -7,6 +7,7 @@ import {
type APIButtonComponentWithCustomId,
} from 'discord-api-types/v9';
import { Component } from '../Component';
import isEqual from 'fast-deep-equal';

/**
* Represents a non-validated button component
Expand Down Expand Up @@ -118,4 +119,11 @@ export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent>
...this.data,
} as APIButtonComponent;
}

public equals(other: APIButtonComponent | UnsafeButtonComponent) {
if (other instanceof UnsafeButtonComponent) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
}
31 changes: 26 additions & 5 deletions packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts
@@ -1,6 +1,7 @@
import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
import { Component } from '../Component';
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
import isEqual from 'fast-deep-equal';

/**
* Represents a non-validated select menu component
Expand Down Expand Up @@ -101,17 +102,27 @@ export class UnsafeSelectMenuComponent extends Component<
* @param options The options to add to this select menu
* @returns
*/
public addOptions(...options: UnsafeSelectMenuOption[]) {
this.options.push(...options);
public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
this.options.push(
...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
),
);
return this;
}

/**
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(...options: UnsafeSelectMenuOption[]) {
this.options.splice(0, this.options.length, ...options);
public setOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
this.options.splice(
0,
this.options.length,
...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
),
);
return this;
}

Expand All @@ -122,4 +133,14 @@ export class UnsafeSelectMenuComponent extends Component<
options: this.options.map((o) => o.toJSON()),
} as APISelectMenuComponent;
}

public equals(other: APISelectMenuComponent | UnsafeSelectMenuComponent): boolean {
if (other instanceof UnsafeSelectMenuComponent) {
return isEqual(other.data, this.data) && isEqual(other.options, this.options);
}
return isEqual(other, {
...this.data,
options: this.options.map((o) => o.toJSON()),
});
}
}
9 changes: 8 additions & 1 deletion packages/builders/src/messages/embed/Assertions.ts
Expand Up @@ -25,7 +25,14 @@ export const authorNamePredicate = fieldNamePredicate.nullable();

export const urlPredicate = z.string().url().nullish();

export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
export const RGBPredicate = z.number().int().gte(0).lte(255);
export const colorPredicate = z
.number()
.int()
.gte(0)
.lte(0xffffff)
.nullable()
.or(z.tuple([RGBPredicate, RGBPredicate, RGBPredicate]));

export const descriptionPredicate = z.string().min(1).max(4096).nullable();

Expand Down
4 changes: 2 additions & 2 deletions packages/builders/src/messages/embed/Embed.ts
Expand Up @@ -13,7 +13,7 @@ import {
urlPredicate,
validateFieldLength,
} from './Assertions';
import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmbed';
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbed } from './UnsafeEmbed';

/**
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
Expand Down Expand Up @@ -48,7 +48,7 @@ export class Embed extends UnsafeEmbed {
return super.setAuthor(options);
}

public override setColor(color: number | null): this {
public override setColor(color: number | RGBTuple | null): this {
// Data assertions
return super.setColor(colorPredicate.parse(color));
}
Expand Down

0 comments on commit f7257f0

Please sign in to comment.