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: PollBuilder #10324

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions packages/builders/__tests__/messages/poll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { PollLayoutType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { PollAnswerMediaBuilder, PollBuilder, PollQuestionBuilder } from '../../src/index.js';

const dummyData = {
question: {
text: '.',
},
answers: [],
};

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' } });

expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
});

test('GIVEN a poll with question text THEN return valid toJSON data', () => {
const poll = new PollBuilder();

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

expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
});

test('GIVEN a poll with invalid question THEN throws error', () => {
expect(() => new PollQuestionBuilder().setText('.'.repeat(301)).toJSON()).toThrowError();
});
});

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

expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
});

test('GIVEN a poll with duration THEN return valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

poll.setDuration(1);

expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
});

test('GIVEN a poll with invalid duration THEN throws error', () => {
const poll = new PollBuilder(dummyData);

expect(() => poll.setDuration(999).toJSON()).toThrowError();
});
});

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 });

expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
});

test('GIVEN a poll with layout type THEN return valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

poll.setLayoutType(PollLayoutType.Default);

expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
});

test('GIVEN a poll with invalid layout type THEN throws error', () => {
const poll = new PollBuilder(dummyData);

// @ts-expect-error Invalid layout type
expect(() => poll.setLayoutType(-1).toJSON()).toThrowError();
});
});

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 });

expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
});

test('GIVEN a poll with multi select enabled THEN return valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

poll.setMultiSelect();

expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
});

test('GIVEN a poll with invalid multi select value THEN throws error', () => {
const poll = new PollBuilder(dummyData);

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

describe('Poll answers', () => {
test('GIVEN a poll with pre-defined answer THEN returns valid toJSON data', () => {
const poll = new PollBuilder({
...dummyData,
answers: [{ poll_media: { text: 'foo' } }],
});
expect(poll.toJSON()).toStrictEqual({
...dummyData,
answers: [{ poll_media: { text: 'foo' } }],
});
});

test('GIVEN a poll using PollBuilder#addAnswers THEN returns valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

poll.addAnswers({ poll_media: { text: 'foo' } });
poll.addAnswers([{ poll_media: { text: 'foo' } }]);

expect(poll.toJSON()).toStrictEqual({
...dummyData,
answers: [{ poll_media: { text: 'foo' } }, { poll_media: { text: 'foo' } }],
});
});

test('GIVEN a poll using PollBuilder#spliceAnswers THEN returns valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

poll.addAnswers({ poll_media: { text: 'foo' } }, { poll_media: { text: 'bar' } });

expect(poll.spliceAnswers(0, 1).toJSON()).toStrictEqual({
...dummyData,
answers: [{ poll_media: { text: 'bar' } }],
});
});

test('GIVEN a poll using PollBuilder#spliceAnswers THEN returns valid toJSON data 2', () => {
const poll = new PollBuilder(dummyData);

poll.addAnswers(...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } })));

expect(() =>
poll.spliceAnswers(0, 3, ...Array.from({ length: 2 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).not.toThrowError();
});

test('GIVEN a poll using PollBuilder#spliceAnswers that adds additional answers resulting in answers > 10 THEN throws error', () => {
const poll = new PollBuilder();

poll.addAnswers(...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } })));

expect(() =>
poll.spliceAnswers(0, 3, ...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).toThrowError();
});

test('GIVEN a poll using PollBuilder#setAnswers THEN returns valid toJSON data', () => {
const poll = new PollBuilder(dummyData);

expect(() =>
poll.setAnswers(...Array.from({ length: 10 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).not.toThrowError();
expect(() =>
poll.setAnswers(Array.from({ length: 10 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).not.toThrowError();
});

test('GIVEN a poll using PollBuilder#setAnswers that sets more than 10 answers THEN throws error', () => {
const poll = new PollBuilder(dummyData);

expect(() =>
poll.setAnswers(...Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).toThrowError();
expect(() =>
poll.setAnswers(Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).toThrowError();
});

describe('GIVEN invalid answer amount THEN throws error', () => {
test('1', () => {
const poll = new PollBuilder(dummyData);

expect(() =>
poll.addAnswers(...Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
).toThrowError();
});
});

describe('GIVEN invalid answer THEN throws error', () => {
test('2', () => {
const poll = new PollBuilder().setQuestion({ text: '.' });

// @ts-expect-error Invalid answer
expect(() => poll.addAnswers({}).toJSON()).toThrowError();
});
});

describe('GIVEN invalid answer text length THEN throws error', () => {
test('3', () => {
expect(() => new PollAnswerMediaBuilder().setText('.'.repeat(56)).toJSON()).toThrowError();
});
});

describe('GIVEN invalid answer text THEN throws error', () => {
test('4', () => {
expect(() => new PollAnswerMediaBuilder().setText('').toJSON()).toThrowError();
});
});

describe('GIVEN invalid answer emoji THEN throws error', () => {
test('5', () => {
// @ts-expect-error Invalid emoji
expect(() => new PollAnswerMediaBuilder().setEmoji('').toJSON()).toThrowError();
});
});
});
});
8 changes: 8 additions & 0 deletions packages/builders/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,16 @@ export * from './messages/embed/EmbedAuthor.js';
export * from './messages/embed/EmbedField.js';
export * from './messages/embed/EmbedFooter.js';

export * from './messages/poll/Assertions.js';
export * from './messages/poll/Poll.js';
export * from './messages/poll/PollAnswer.js';
export * from './messages/poll/PollAnswerMedia.js';
export * from './messages/poll/PollMedia.js';
export * from './messages/poll/PollQuestion.js';

export * from './util/componentUtil.js';
export * from './util/normalizeArray.js';
export * from './util/resolveBuilder.js';
export * from './util/validation.js';

export * from './Assertions.js';
Expand Down
20 changes: 20 additions & 0 deletions packages/builders/src/messages/poll/Assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PollLayoutType } from 'discord-api-types/v10';
import { z } from 'zod';
import { emojiPredicate } from '../../components/Assertions';

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(),
});

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

export const pollPredicate = z.object({
question: pollQuestionPredicate,
answers: z.array(pollAnswerPredicate).max(10),
duration: z.number().min(1).max(768).optional(),
allow_multiselect: z.boolean().optional(),
layout_type: z.nativeEnum(PollLayoutType).optional(),
});
Loading
Loading