From 6acdad3bd8408ab732bf66212856b296f9f3b9eb Mon Sep 17 00:00:00 2001 From: almeidx Date: Fri, 28 Feb 2025 19:40:41 +0000 Subject: [PATCH 1/3] fix: poll builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed validations - Added missing documentation - Removed redundant code - Consistency™️ --- .../builders/src/messages/poll/Assertions.ts | 4 ++-- packages/builders/src/messages/poll/Poll.ts | 2 +- .../builders/src/messages/poll/PollAnswer.ts | 19 +++++++++++++++---- .../src/messages/poll/PollAnswerMedia.ts | 11 +++++++---- .../builders/src/messages/poll/PollMedia.ts | 6 ++++++ .../src/messages/poll/PollQuestion.ts | 5 ++++- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/builders/src/messages/poll/Assertions.ts b/packages/builders/src/messages/poll/Assertions.ts index 0c0c065c6c6d..769737b432a3 100644 --- a/packages/builders/src/messages/poll/Assertions.ts +++ b/packages/builders/src/messages/poll/Assertions.ts @@ -6,14 +6,14 @@ export const pollQuestionPredicate = z.object({ text: z.string().min(1).max(300) export const pollAnswerMediaPredicate = z.object({ text: z.string().min(1).max(55), - emoji: emojiPredicate.nullish(), + emoji: emojiPredicate.optional(), }); export const pollAnswerPredicate = z.object({ poll_media: pollAnswerMediaPredicate }); export const pollPredicate = z.object({ question: pollQuestionPredicate, - answers: z.array(pollAnswerPredicate).max(10), + answers: z.array(pollAnswerPredicate).min(1).max(10), duration: z.number().min(1).max(768).optional(), allow_multiselect: z.boolean().optional(), layout_type: z.nativeEnum(PollLayoutType).optional(), diff --git a/packages/builders/src/messages/poll/Poll.ts b/packages/builders/src/messages/poll/Poll.ts index 2d5e3cf8aac7..837c9e6fdb18 100644 --- a/packages/builders/src/messages/poll/Poll.ts +++ b/packages/builders/src/messages/poll/Poll.ts @@ -161,7 +161,7 @@ export class PollBuilder implements JSONEncodable { * @param updater - The function to update the question with */ public updateQuestion(updater: (builder: PollQuestionBuilder) => void): this { - updater((this.data.question ??= new PollQuestionBuilder())); + updater(this.data.question); return this; } diff --git a/packages/builders/src/messages/poll/PollAnswer.ts b/packages/builders/src/messages/poll/PollAnswer.ts index 536a8781c98c..071ed97fd8ae 100644 --- a/packages/builders/src/messages/poll/PollAnswer.ts +++ b/packages/builders/src/messages/poll/PollAnswer.ts @@ -9,8 +9,16 @@ export interface PollAnswerData extends Omit> = {}) { this.data = { ...structuredClone(data), @@ -35,8 +43,9 @@ export class PollAnswerBuilder { * * @param updater - The function to update the media with */ - public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void) { - updater((this.data.poll_media ??= new PollAnswerMediaBuilder())); + public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void): this { + updater(this.data.poll_media); + return this; } /** @@ -47,10 +56,12 @@ export class PollAnswerBuilder { * @param validationOverride - Force validation to run/not run regardless of your global preference */ public toJSON(validationOverride?: boolean): Omit { + const { poll_media, ...rest } = this.data; + const data = { - ...structuredClone(this.data), + ...structuredClone(rest), // Disable validation because the pollAnswerPredicate below will validate this as well - poll_media: this.data.poll_media?.toJSON(false), + poll_media: poll_media.toJSON(false), }; validate(pollAnswerPredicate, data, validationOverride); diff --git a/packages/builders/src/messages/poll/PollAnswerMedia.ts b/packages/builders/src/messages/poll/PollAnswerMedia.ts index 2420d375c68c..7899efa718a0 100644 --- a/packages/builders/src/messages/poll/PollAnswerMedia.ts +++ b/packages/builders/src/messages/poll/PollAnswerMedia.ts @@ -4,11 +4,11 @@ import { pollAnswerMediaPredicate } from './Assertions.js'; import { PollMediaBuilder } from './PollMedia.js'; /** - * A builder that creates API-compatible JSON data for poll answers. + * A builder that creates API-compatible JSON data for the media of a poll answer. */ export class PollAnswerMediaBuilder extends PollMediaBuilder { /** - * Sets the emoji for this poll answer. + * Sets the emoji for this poll answer media. * * @param emoji - The emoji to use */ @@ -18,18 +18,21 @@ export class PollAnswerMediaBuilder extends PollMediaBuilder { } /** - * Clears the emoji for this poll answer. + * Clears the emoji for this poll answer media. */ public clearEmoji(): this { this.data.emoji = undefined; return this; } + /** + * {@inheritDoc PollMediaBuilder.toJSON} + */ public override toJSON(validationOverride?: boolean): APIPollMedia { const clone = structuredClone(this.data); validate(pollAnswerMediaPredicate, clone, validationOverride); - return clone as APIPollMedia; + return clone; } } diff --git a/packages/builders/src/messages/poll/PollMedia.ts b/packages/builders/src/messages/poll/PollMedia.ts index b0ba25f305d0..879a10b0588c 100644 --- a/packages/builders/src/messages/poll/PollMedia.ts +++ b/packages/builders/src/messages/poll/PollMedia.ts @@ -1,6 +1,12 @@ import type { APIPollMedia } from 'discord-api-types/v10'; +/** + * The base poll media builder that contains common symbols for poll media builders. + */ export abstract class PollMediaBuilder { + /** + * The API data associated with this poll media. + */ protected readonly data: Partial; /** diff --git a/packages/builders/src/messages/poll/PollQuestion.ts b/packages/builders/src/messages/poll/PollQuestion.ts index 8d9fbf9a5008..ad0c9923240e 100644 --- a/packages/builders/src/messages/poll/PollQuestion.ts +++ b/packages/builders/src/messages/poll/PollQuestion.ts @@ -7,11 +7,14 @@ import { PollMediaBuilder } from './PollMedia.js'; * A builder that creates API-compatible JSON data for a poll question. */ export class PollQuestionBuilder extends PollMediaBuilder { + /** + * {@inheritDoc PollMediaBuilder.toJSON} + */ public override toJSON(validationOverride?: boolean): Omit { const clone = structuredClone(this.data); validate(pollQuestionPredicate, clone, validationOverride); - return clone as Omit; + return clone; } } From 2fb1ac0c4860a1077addab334d6f85fbb1fc4d48 Mon Sep 17 00:00:00 2001 From: almeidx Date: Fri, 28 Feb 2025 19:50:50 +0000 Subject: [PATCH 2/3] fix: tests --- .../builders/__tests__/messages/poll.test.ts | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/builders/__tests__/messages/poll.test.ts b/packages/builders/__tests__/messages/poll.test.ts index 0101d5452ea8..fdf88a067072 100644 --- a/packages/builders/__tests__/messages/poll.test.ts +++ b/packages/builders/__tests__/messages/poll.test.ts @@ -1,4 +1,4 @@ -import { PollLayoutType } from 'discord-api-types/v10'; +import { PollLayoutType, type RESTAPIPoll } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { PollAnswerMediaBuilder, PollBuilder, PollQuestionBuilder } from '../../src/index.js'; @@ -7,22 +7,33 @@ const dummyData = { text: '.', }, answers: [], -}; +} satisfies RESTAPIPoll; + +const dummyDataWithAnswer = { + ...dummyData, + answers: [ + { + poll_media: { + text: '.', + }, + }, + ], +} satisfies RESTAPIPoll; describe('Poll', () => { describe('Poll question', () => { test('GIVEN a poll with pre-defined question text THEN return valid toJSON data', () => { - const poll = new PollBuilder({ question: { text: 'foo' } }); + const poll = new PollBuilder({ ...dummyDataWithAnswer, question: { text: 'foo' } }); - expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } }); + expect(poll.toJSON()).toStrictEqual({ ...dummyDataWithAnswer, question: { text: 'foo' } }); }); test('GIVEN a poll with question text THEN return valid toJSON data', () => { - const poll = new PollBuilder(); + const poll = new PollBuilder(dummyDataWithAnswer); poll.setQuestion({ text: 'foo' }); - expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } }); + expect(poll.toJSON()).toStrictEqual({ ...dummyDataWithAnswer, question: { text: 'foo' } }); }); test('GIVEN a poll with invalid question THEN throws error', () => { @@ -32,21 +43,21 @@ describe('Poll', () => { describe('Poll duration', () => { test('GIVEN a poll with pre-defined duration THEN return valid toJSON data', () => { - const poll = new PollBuilder({ duration: 1, ...dummyData }); + const poll = new PollBuilder({ duration: 1, ...dummyDataWithAnswer }); - expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyDataWithAnswer }); }); test('GIVEN a poll with duration THEN return valid toJSON data', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); poll.setDuration(1); - expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyDataWithAnswer }); }); test('GIVEN a poll with invalid duration THEN throws error', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); expect(() => poll.setDuration(999).toJSON()).toThrowError(); }); @@ -54,21 +65,21 @@ describe('Poll', () => { describe('Poll layout type', () => { test('GIVEN a poll with pre-defined layout type THEN return valid toJSON data', () => { - const poll = new PollBuilder({ layout_type: PollLayoutType.Default, ...dummyData }); + const poll = new PollBuilder({ ...dummyDataWithAnswer, layout_type: PollLayoutType.Default }); - expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyDataWithAnswer }); }); test('GIVEN a poll with layout type THEN return valid toJSON data', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); poll.setLayoutType(PollLayoutType.Default); - expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyDataWithAnswer }); }); test('GIVEN a poll with invalid layout type THEN throws error', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); // @ts-expect-error Invalid layout type expect(() => poll.setLayoutType(-1).toJSON()).toThrowError(); @@ -77,21 +88,21 @@ describe('Poll', () => { describe('Poll multi select', () => { test('GIVEN a poll with pre-defined multi select enabled THEN return valid toJSON data', () => { - const poll = new PollBuilder({ allow_multiselect: true, ...dummyData }); + const poll = new PollBuilder({ allow_multiselect: true, ...dummyDataWithAnswer }); - expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyDataWithAnswer }); }); test('GIVEN a poll with multi select enabled THEN return valid toJSON data', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); poll.setMultiSelect(); - expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData }); + expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyDataWithAnswer }); }); test('GIVEN a poll with invalid multi select value THEN throws error', () => { - const poll = new PollBuilder(dummyData); + const poll = new PollBuilder(dummyDataWithAnswer); // @ts-expect-error Invalid multi-select value expect(() => poll.setMultiSelect('string').toJSON()).toThrowError(); From ae229ae47711fe7844c468c7d72d5afe661c65be Mon Sep 17 00:00:00 2001 From: almeidx Date: Sat, 1 Mar 2025 12:28:21 +0000 Subject: [PATCH 3/3] feat: missing answers test --- packages/builders/__tests__/messages/poll.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/builders/__tests__/messages/poll.test.ts b/packages/builders/__tests__/messages/poll.test.ts index fdf88a067072..b8819e34ceac 100644 --- a/packages/builders/__tests__/messages/poll.test.ts +++ b/packages/builders/__tests__/messages/poll.test.ts @@ -110,6 +110,12 @@ describe('Poll', () => { }); describe('Poll answers', () => { + test('GIVEN a poll without answers THEN throws error', () => { + const poll = new PollBuilder(dummyData); + + expect(() => poll.toJSON()).toThrowError(); + }); + test('GIVEN a poll with pre-defined answer THEN returns valid toJSON data', () => { const poll = new PollBuilder({ ...dummyData,