Skip to content

Commit d1f56ff

Browse files
authored
fix: poll builders (#10783)
* fix: poll builders - Fixed validations - Added missing documentation - Removed redundant code - Consistency™️ * fix: tests * feat: missing answers test
1 parent 88bfeaa commit d1f56ff

File tree

7 files changed

+73
-33
lines changed

7 files changed

+73
-33
lines changed

packages/builders/__tests__/messages/poll.test.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PollLayoutType } from 'discord-api-types/v10';
1+
import { PollLayoutType, type RESTAPIPoll } from 'discord-api-types/v10';
22
import { describe, test, expect } from 'vitest';
33
import { PollAnswerMediaBuilder, PollBuilder, PollQuestionBuilder } from '../../src/index.js';
44

@@ -7,22 +7,33 @@ const dummyData = {
77
text: '.',
88
},
99
answers: [],
10-
};
10+
} satisfies RESTAPIPoll;
11+
12+
const dummyDataWithAnswer = {
13+
...dummyData,
14+
answers: [
15+
{
16+
poll_media: {
17+
text: '.',
18+
},
19+
},
20+
],
21+
} satisfies RESTAPIPoll;
1122

1223
describe('Poll', () => {
1324
describe('Poll question', () => {
1425
test('GIVEN a poll with pre-defined question text THEN return valid toJSON data', () => {
15-
const poll = new PollBuilder({ question: { text: 'foo' } });
26+
const poll = new PollBuilder({ ...dummyDataWithAnswer, question: { text: 'foo' } });
1627

17-
expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
28+
expect(poll.toJSON()).toStrictEqual({ ...dummyDataWithAnswer, question: { text: 'foo' } });
1829
});
1930

2031
test('GIVEN a poll with question text THEN return valid toJSON data', () => {
21-
const poll = new PollBuilder();
32+
const poll = new PollBuilder(dummyDataWithAnswer);
2233

2334
poll.setQuestion({ text: 'foo' });
2435

25-
expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
36+
expect(poll.toJSON()).toStrictEqual({ ...dummyDataWithAnswer, question: { text: 'foo' } });
2637
});
2738

2839
test('GIVEN a poll with invalid question THEN throws error', () => {
@@ -32,43 +43,43 @@ describe('Poll', () => {
3243

3344
describe('Poll duration', () => {
3445
test('GIVEN a poll with pre-defined duration THEN return valid toJSON data', () => {
35-
const poll = new PollBuilder({ duration: 1, ...dummyData });
46+
const poll = new PollBuilder({ duration: 1, ...dummyDataWithAnswer });
3647

37-
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
48+
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyDataWithAnswer });
3849
});
3950

4051
test('GIVEN a poll with duration THEN return valid toJSON data', () => {
41-
const poll = new PollBuilder(dummyData);
52+
const poll = new PollBuilder(dummyDataWithAnswer);
4253

4354
poll.setDuration(1);
4455

45-
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
56+
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyDataWithAnswer });
4657
});
4758

4859
test('GIVEN a poll with invalid duration THEN throws error', () => {
49-
const poll = new PollBuilder(dummyData);
60+
const poll = new PollBuilder(dummyDataWithAnswer);
5061

5162
expect(() => poll.setDuration(999).toJSON()).toThrowError();
5263
});
5364
});
5465

5566
describe('Poll layout type', () => {
5667
test('GIVEN a poll with pre-defined layout type THEN return valid toJSON data', () => {
57-
const poll = new PollBuilder({ layout_type: PollLayoutType.Default, ...dummyData });
68+
const poll = new PollBuilder({ ...dummyDataWithAnswer, layout_type: PollLayoutType.Default });
5869

59-
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
70+
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyDataWithAnswer });
6071
});
6172

6273
test('GIVEN a poll with layout type THEN return valid toJSON data', () => {
63-
const poll = new PollBuilder(dummyData);
74+
const poll = new PollBuilder(dummyDataWithAnswer);
6475

6576
poll.setLayoutType(PollLayoutType.Default);
6677

67-
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
78+
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyDataWithAnswer });
6879
});
6980

7081
test('GIVEN a poll with invalid layout type THEN throws error', () => {
71-
const poll = new PollBuilder(dummyData);
82+
const poll = new PollBuilder(dummyDataWithAnswer);
7283

7384
// @ts-expect-error Invalid layout type
7485
expect(() => poll.setLayoutType(-1).toJSON()).toThrowError();
@@ -77,28 +88,34 @@ describe('Poll', () => {
7788

7889
describe('Poll multi select', () => {
7990
test('GIVEN a poll with pre-defined multi select enabled THEN return valid toJSON data', () => {
80-
const poll = new PollBuilder({ allow_multiselect: true, ...dummyData });
91+
const poll = new PollBuilder({ allow_multiselect: true, ...dummyDataWithAnswer });
8192

82-
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
93+
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyDataWithAnswer });
8394
});
8495

8596
test('GIVEN a poll with multi select enabled THEN return valid toJSON data', () => {
86-
const poll = new PollBuilder(dummyData);
97+
const poll = new PollBuilder(dummyDataWithAnswer);
8798

8899
poll.setMultiSelect();
89100

90-
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
101+
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyDataWithAnswer });
91102
});
92103

93104
test('GIVEN a poll with invalid multi select value THEN throws error', () => {
94-
const poll = new PollBuilder(dummyData);
105+
const poll = new PollBuilder(dummyDataWithAnswer);
95106

96107
// @ts-expect-error Invalid multi-select value
97108
expect(() => poll.setMultiSelect('string').toJSON()).toThrowError();
98109
});
99110
});
100111

101112
describe('Poll answers', () => {
113+
test('GIVEN a poll without answers THEN throws error', () => {
114+
const poll = new PollBuilder(dummyData);
115+
116+
expect(() => poll.toJSON()).toThrowError();
117+
});
118+
102119
test('GIVEN a poll with pre-defined answer THEN returns valid toJSON data', () => {
103120
const poll = new PollBuilder({
104121
...dummyData,

packages/builders/src/messages/poll/Assertions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ export const pollQuestionPredicate = z.object({ text: z.string().min(1).max(300)
66

77
export const pollAnswerMediaPredicate = z.object({
88
text: z.string().min(1).max(55),
9-
emoji: emojiPredicate.nullish(),
9+
emoji: emojiPredicate.optional(),
1010
});
1111

1212
export const pollAnswerPredicate = z.object({ poll_media: pollAnswerMediaPredicate });
1313

1414
export const pollPredicate = z.object({
1515
question: pollQuestionPredicate,
16-
answers: z.array(pollAnswerPredicate).max(10),
16+
answers: z.array(pollAnswerPredicate).min(1).max(10),
1717
duration: z.number().min(1).max(768).optional(),
1818
allow_multiselect: z.boolean().optional(),
1919
layout_type: z.nativeEnum(PollLayoutType).optional(),

packages/builders/src/messages/poll/Poll.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class PollBuilder implements JSONEncodable<RESTAPIPoll> {
161161
* @param updater - The function to update the question with
162162
*/
163163
public updateQuestion(updater: (builder: PollQuestionBuilder) => void): this {
164-
updater((this.data.question ??= new PollQuestionBuilder()));
164+
updater(this.data.question);
165165
return this;
166166
}
167167

packages/builders/src/messages/poll/PollAnswer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ export interface PollAnswerData extends Omit<APIPollAnswer, 'answer_id' | 'poll_
99
}
1010

1111
export class PollAnswerBuilder {
12+
/**
13+
* The API data associated with this poll answer.
14+
*/
1215
protected readonly data: PollAnswerData;
1316

17+
/**
18+
* Creates a new poll answer from API data.
19+
*
20+
* @param data - The API data to create this poll answer with
21+
*/
1422
public constructor(data: Partial<Omit<APIPollAnswer, 'answer_id'>> = {}) {
1523
this.data = {
1624
...structuredClone(data),
@@ -35,8 +43,9 @@ export class PollAnswerBuilder {
3543
*
3644
* @param updater - The function to update the media with
3745
*/
38-
public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void) {
39-
updater((this.data.poll_media ??= new PollAnswerMediaBuilder()));
46+
public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void): this {
47+
updater(this.data.poll_media);
48+
return this;
4049
}
4150

4251
/**
@@ -47,10 +56,12 @@ export class PollAnswerBuilder {
4756
* @param validationOverride - Force validation to run/not run regardless of your global preference
4857
*/
4958
public toJSON(validationOverride?: boolean): Omit<APIPollAnswer, 'answer_id'> {
59+
const { poll_media, ...rest } = this.data;
60+
5061
const data = {
51-
...structuredClone(this.data),
62+
...structuredClone(rest),
5263
// Disable validation because the pollAnswerPredicate below will validate this as well
53-
poll_media: this.data.poll_media?.toJSON(false),
64+
poll_media: poll_media.toJSON(false),
5465
};
5566

5667
validate(pollAnswerPredicate, data, validationOverride);

packages/builders/src/messages/poll/PollAnswerMedia.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { pollAnswerMediaPredicate } from './Assertions.js';
44
import { PollMediaBuilder } from './PollMedia.js';
55

66
/**
7-
* A builder that creates API-compatible JSON data for poll answers.
7+
* A builder that creates API-compatible JSON data for the media of a poll answer.
88
*/
99
export class PollAnswerMediaBuilder extends PollMediaBuilder {
1010
/**
11-
* Sets the emoji for this poll answer.
11+
* Sets the emoji for this poll answer media.
1212
*
1313
* @param emoji - The emoji to use
1414
*/
@@ -18,18 +18,21 @@ export class PollAnswerMediaBuilder extends PollMediaBuilder {
1818
}
1919

2020
/**
21-
* Clears the emoji for this poll answer.
21+
* Clears the emoji for this poll answer media.
2222
*/
2323
public clearEmoji(): this {
2424
this.data.emoji = undefined;
2525
return this;
2626
}
2727

28+
/**
29+
* {@inheritDoc PollMediaBuilder.toJSON}
30+
*/
2831
public override toJSON(validationOverride?: boolean): APIPollMedia {
2932
const clone = structuredClone(this.data);
3033

3134
validate(pollAnswerMediaPredicate, clone, validationOverride);
3235

33-
return clone as APIPollMedia;
36+
return clone;
3437
}
3538
}

packages/builders/src/messages/poll/PollMedia.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { APIPollMedia } from 'discord-api-types/v10';
22

3+
/**
4+
* The base poll media builder that contains common symbols for poll media builders.
5+
*/
36
export abstract class PollMediaBuilder {
7+
/**
8+
* The API data associated with this poll media.
9+
*/
410
protected readonly data: Partial<APIPollMedia>;
511

612
/**

packages/builders/src/messages/poll/PollQuestion.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { PollMediaBuilder } from './PollMedia.js';
77
* A builder that creates API-compatible JSON data for a poll question.
88
*/
99
export class PollQuestionBuilder extends PollMediaBuilder {
10+
/**
11+
* {@inheritDoc PollMediaBuilder.toJSON}
12+
*/
1013
public override toJSON(validationOverride?: boolean): Omit<APIPollMedia, 'emoji'> {
1114
const clone = structuredClone(this.data);
1215

1316
validate(pollQuestionPredicate, clone, validationOverride);
1417

15-
return clone as Omit<APIPollMedia, 'emoji'>;
18+
return clone;
1619
}
1720
}

0 commit comments

Comments
 (0)