Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add missing v13 component methods #7466

Merged
merged 10 commits into from
Feb 18, 2022
72 changes: 38 additions & 34 deletions packages/builders/__tests__/components/actionRow.test.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
});
});
});
12 changes: 8 additions & 4 deletions packages/builders/__tests__/messages/embed.test.ts
Original file line number Diff line number Diff line change
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 @@ -134,6 +132,12 @@ describe('Embed', () => {
// @ts-expect-error
expect(() => embed.setColor('RED')).toThrowError();
});

test('GIVEN an embed with a valid color THEN does not throw error', () => {
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
const embed = new Embed();
expect(() => embed.setColor([42, 36, 100])).not.toThrowError();
expect(() => embed.setColor(0xffffff)).not.toThrowError();
});
});

describe('Embed Timestamp', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/builders/package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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) {
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
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()),
});
}
}
14 changes: 9 additions & 5 deletions packages/builders/src/components/Component.ts
Original file line number Diff line number Diff line change
@@ -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,17 +14,15 @@ 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
*/
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
public abstract toJSON(): APIMessageComponent;
public abstract equals(other: Component | APIActionRowComponentTypes): boolean;
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved

public constructor(data: DataType) {
this.data = data;
Expand Down
8 changes: 8 additions & 0 deletions packages/builders/src/components/button/UnsafeButton.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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';
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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()),
});
}
}
7 changes: 6 additions & 1 deletion packages/builders/src/messages/embed/Assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export const authorNamePredicate = fieldNamePredicate.nullable();

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

export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
export const colorPredicate = z
.number()
.gte(0)
.lte(0xffffff)
.nullable()
.or(z.tuple([z.number(), z.number(), z.number()]));
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
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