mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 18:43:31 +01:00
feat: @discordjs/structures (#10900)
* chore: init /structures * feat: base structure * feat: initial structures design attempt * refactor(Structure): use unknown to store in kData * feat(Structure): add Invite refactor(Structure): patch to _patch * refactor: symbol names and override location * fix: don't possibly return 0 if discord borks Co-authored-by: Synbulat Biishev <signin@syjalo.dev> * refactor: use getter value instead of api Co-authored-by: Synbulat Biishev <signin@syjalo.dev> * refactor: cache createdTimestamp value Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> * docs: better docs for what's done so far * feat: add Mixin * refactor(User): remove bitfield getters and add displayName * feat(structures): add Connection * feat(structures): add Channel base * refactor(Mixin): trace prototype chain, allow construction * fix(structures): fix mixin behavior * fix(structures): data optimization call behavior from perf testing * feat: channel mixins * chore: update deps * feat: channels and mixins * chore: more typeguard tests * fix: tests and some other issues * feat: add ChannelWebhookMixin * fix: more tests * chore: tests and docs * chore: docs * fix: remove unneccessary omitted * chore: apply code suggestions * refactor: change how extended invite works * fix: type imports * Apply suggestions from code review Co-authored-by: Almeida <github@almeidx.dev> * fix: tests * chore: add jsdoc * refactor: apply code suggestions * fix: don't instantiate sub-structures * fix: don't do null default twice * chore: use formatters, add _cache * chore: lockfile * chore: move MixinTypes to declaratiion file * fix: tests * fix: don't include source d.ts files for docs * feat: bitfields * feat: more bitfields * refactor: remove DirectoryChannel structure * chore: apply suggestions from code review * chore: remove unused import * refactor: use symbol for mixin toJSON, remove _ prefix * chore: apply suggestions from code review * refactor: remove bitfield casts * refactor: remove special case for threadchannel types * fix: apply code review suggestions * refactor: bitfields always store bigint * fix: tests * chore: apply suggestions from code review * fix: lint * refactor: conditional structuredClone * Apply suggestions from code review Co-authored-by: ckohen <chaikohen@gmail.com> * fix: code review errors * fix: lint * chore: bump dtypes * Update packages/structures/cliff.toml Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * docs: link to VideoQualityMode * chore: typo in comment * chore: small nits in docs links * chore: small nits * docs: forgot one * chore: update template * chore: typos and things * chore: apply suggestions from code review * fix: tests and typeguards * chore: don't clone appliedTags * refactor: use a symbol for patch method * fix: add missing readonly * chore: remove todo comment * refactor: use symbol for clone * fix: add constraint to DataType * chore: apply suggestions * fix: dtypes bump * chore: fix comment * chore: add todo comment * chore: mark bitfield as todo chore: mark bit field as todo and edit readme --------- Co-authored-by: ckohen <chaikohen@gmail.com> Co-authored-by: Synbulat Biishev <signin@syjalo.dev> Co-authored-by: Almeida <github@almeidx.dev> Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
98
packages/structures/__tests__/Mixin.test.ts
Normal file
98
packages/structures/__tests__/Mixin.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { kData, kPatch } from '../src/utils/symbols.js';
|
||||
import type { APIData } from './mixinClasses.js';
|
||||
import { Base, Mixed, MixedWithExtended } from './mixinClasses.js';
|
||||
|
||||
describe('Mixin function', () => {
|
||||
const data: APIData = {
|
||||
id: '1',
|
||||
property1: 23,
|
||||
};
|
||||
|
||||
test('Mixed class has all getters', () => {
|
||||
const instance = new Mixed(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.property1).toBe(data.property1);
|
||||
expect(instance.property2).toBe(data.property2);
|
||||
});
|
||||
|
||||
test('Mixed class has all methods', () => {
|
||||
const instance = new Mixed(data);
|
||||
expect(instance.getId()).toBe(data.id);
|
||||
expect(instance.getProperty1()).toBe(data.property1);
|
||||
expect(instance.getProperty2()).toBe(data.property2);
|
||||
expect(instance.getProperties()).toEqual({
|
||||
property1: data.property1,
|
||||
property2: data.property2,
|
||||
});
|
||||
});
|
||||
|
||||
test('Mixed with extended class has all getters', () => {
|
||||
const instance = new MixedWithExtended(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.property1).toBe(data.property1);
|
||||
expect(instance.property2).toBe(data.property2);
|
||||
expect(instance.isExtended).toBe(true);
|
||||
});
|
||||
|
||||
test('Mixed with extended class has all methods', () => {
|
||||
const instance = new MixedWithExtended(data);
|
||||
expect(instance.getId()).toBe(data.id);
|
||||
expect(instance.getProperty1()).toBe(data.property1);
|
||||
expect(instance.getProperty2()).toBe(data.property2);
|
||||
expect(instance.getProperties()).toEqual({
|
||||
property1: data.property1,
|
||||
property2: data.property2,
|
||||
});
|
||||
});
|
||||
|
||||
test('Mixed class calls construct methods on construct', () => {
|
||||
const instance1 = new Mixed(data);
|
||||
const instance2 = new MixedWithExtended(data);
|
||||
expect(instance1.constructCalled).toBe(true);
|
||||
expect(instance2.constructCalled).toBe(true);
|
||||
});
|
||||
|
||||
test('Mixed class respects mixin data optimizations', () => {
|
||||
expect(typeof Object.getOwnPropertyDescriptor(Mixed.DataTemplate, 'mixinOptimize')?.set).toBe('function');
|
||||
const missingOptimizedInstance = new Mixed(data);
|
||||
const alreadyOptimizedInstance = new Mixed({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||
const baseOptimizedInstance = new Base({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||
|
||||
expect(missingOptimizedInstance.baseOptimize).toBe(null);
|
||||
expect(missingOptimizedInstance.mixinOptimize).toBe(null);
|
||||
// Setters pass this
|
||||
expect('baseOptimize' in missingOptimizedInstance[kData]).toBe(true);
|
||||
expect('mixinOptimize' in missingOptimizedInstance[kData]).toBe(true);
|
||||
expect(missingOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||
expect(missingOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||
|
||||
expect(alreadyOptimizedInstance.baseOptimize).toBe(true);
|
||||
expect(alreadyOptimizedInstance.mixinOptimize).toBe(true);
|
||||
// Setters pass this
|
||||
expect('baseOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||
expect('mixinOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||
expect(alreadyOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||
expect(alreadyOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||
expect(alreadyOptimizedInstance.toJSON()).toEqual({ ...data, mixinOptimize: 'true', baseOptimize: 'true' });
|
||||
|
||||
alreadyOptimizedInstance[kPatch]({ mixinOptimize: '', baseOptimize: '' });
|
||||
|
||||
expect(alreadyOptimizedInstance.baseOptimize).toBe(false);
|
||||
expect(alreadyOptimizedInstance.mixinOptimize).toBe(false);
|
||||
// Setters pass this
|
||||
expect('baseOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||
expect('mixinOptimize' in alreadyOptimizedInstance[kData]).toBe(true);
|
||||
expect(alreadyOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||
expect(alreadyOptimizedInstance[kData].mixinOptimize).toBeUndefined();
|
||||
|
||||
// Ensure mixin optimizations don't happen on base (ie overwritten DataTemplate)
|
||||
expect(baseOptimizedInstance.baseOptimize).toBe(true);
|
||||
expect('mixinOptimize' in baseOptimizedInstance).toBe(false);
|
||||
// Setters pass this
|
||||
expect('baseOptimize' in baseOptimizedInstance[kData]).toBe(true);
|
||||
expect('mixinOptimize' in baseOptimizedInstance[kData]).toBe(true);
|
||||
expect(baseOptimizedInstance[kData].baseOptimize).toBeUndefined();
|
||||
expect(baseOptimizedInstance[kData].mixinOptimize).toBe('true');
|
||||
});
|
||||
});
|
||||
65
packages/structures/__tests__/Structure.test.ts
Normal file
65
packages/structures/__tests__/Structure.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, test, expect, beforeEach } from 'vitest';
|
||||
import { DataTemplatePropertyName, OptimizeDataPropertyName, Structure } from '../src/Structure.js';
|
||||
import { kData, kPatch } from '../src/utils/symbols.js';
|
||||
|
||||
describe('Base Structure', () => {
|
||||
const data = { test: true, patched: false, removed: true };
|
||||
let struct: Structure<typeof data>;
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error Structure constructor is protected
|
||||
struct = new Structure(data);
|
||||
// @ts-expect-error Structure.DataTemplate is protected
|
||||
Structure.DataTemplate = {};
|
||||
});
|
||||
|
||||
test('Data reference is not identical (clone via Object.assign)', () => {
|
||||
expect(struct[kData]).not.toBe(data);
|
||||
expect(struct[kData]).toEqual(data);
|
||||
});
|
||||
|
||||
test('Remove properties via template (constructor)', () => {
|
||||
// @ts-expect-error Structure.DataTemplate is protected
|
||||
Structure.DataTemplate = { set removed(_) {} };
|
||||
// @ts-expect-error Structure constructor is protected
|
||||
const templatedStruct: Structure<typeof data> = new Structure(data);
|
||||
expect(templatedStruct[kData].removed).toBe(undefined);
|
||||
// Setters still exist and pass "in" test unfortunately
|
||||
expect('removed' in templatedStruct[kData]).toBe(true);
|
||||
expect(templatedStruct[kData]).toEqual({ test: true, patched: false });
|
||||
});
|
||||
|
||||
test('patch clones data and updates in place', () => {
|
||||
const dataBefore = struct[kData];
|
||||
const patched = struct[kPatch]({ patched: true });
|
||||
expect(patched[kData].patched).toBe(true);
|
||||
// Patch in place
|
||||
expect(struct[kData]).toBe(patched[kData]);
|
||||
// Clones
|
||||
expect(dataBefore.patched).toBe(false);
|
||||
expect(dataBefore).not.toBe(patched[kData]);
|
||||
});
|
||||
|
||||
test('Remove properties via template ([kPatch])', () => {
|
||||
// @ts-expect-error Structure.DataTemplate is protected
|
||||
Structure.DataTemplate = { set removed(_) {} };
|
||||
// @ts-expect-error Structure constructor is protected
|
||||
const templatedStruct: Structure<typeof data> = new Structure(data);
|
||||
templatedStruct[kPatch]({ removed: false });
|
||||
expect(templatedStruct[kData].removed).toBe(undefined);
|
||||
// Setters still exist and pass "in" test unfortunately
|
||||
expect('removed' in templatedStruct[kData]).toBe(true);
|
||||
expect(templatedStruct[kData]).toEqual({ test: true, patched: false });
|
||||
});
|
||||
|
||||
test('toJSON clones but retains data equality', () => {
|
||||
const json = struct.toJSON();
|
||||
expect(json).not.toBe(data);
|
||||
expect(json).not.toBe(struct[kData]);
|
||||
expect(struct[kData]).toEqual(json);
|
||||
});
|
||||
|
||||
test("XPropertyName variable matches the actual property's names", () => {
|
||||
expect(Structure[DataTemplatePropertyName]).toStrictEqual({});
|
||||
expect(struct[OptimizeDataPropertyName]).toBeTypeOf('function');
|
||||
});
|
||||
});
|
||||
741
packages/structures/__tests__/channels.test.ts
Normal file
741
packages/structures/__tests__/channels.test.ts
Normal file
@@ -0,0 +1,741 @@
|
||||
import type {
|
||||
APIAnnouncementThreadChannel,
|
||||
APIDMChannel,
|
||||
APIGroupDMChannel,
|
||||
APIGuildCategoryChannel,
|
||||
APIGuildForumChannel,
|
||||
APIGuildMediaChannel,
|
||||
APIGuildStageVoiceChannel,
|
||||
APIGuildVoiceChannel,
|
||||
APINewsChannel,
|
||||
APIPrivateThreadChannel,
|
||||
APIPublicThreadChannel,
|
||||
APITextChannel,
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
ForumLayoutType,
|
||||
SortOrderType,
|
||||
ChannelType,
|
||||
OverwriteType,
|
||||
ThreadAutoArchiveDuration,
|
||||
VideoQualityMode,
|
||||
ChannelFlags,
|
||||
} from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
AnnouncementChannel,
|
||||
AnnouncementThreadChannel,
|
||||
CategoryChannel,
|
||||
DMChannel,
|
||||
ForumChannel,
|
||||
ForumTag,
|
||||
GroupDMChannel,
|
||||
MediaChannel,
|
||||
PermissionOverwrite,
|
||||
PrivateThreadChannel,
|
||||
PublicThreadChannel,
|
||||
StageChannel,
|
||||
TextChannel,
|
||||
ThreadMetadata,
|
||||
VoiceChannel,
|
||||
} from '../src/index.js';
|
||||
import { kData } from '../src/utils/symbols.js';
|
||||
|
||||
describe('text channel', () => {
|
||||
const data: APITextChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildText,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
last_message_id: '3',
|
||||
last_pin_timestamp: '2020-10-10T13:50:17.209Z',
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
rate_limit_per_user: 9,
|
||||
topic: 'hello',
|
||||
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||
default_thread_rate_limit_per_user: 30,
|
||||
};
|
||||
|
||||
test('TextChannel has all properties', () => {
|
||||
const instance = new TextChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance.lastPinTimestamp).toBe(Date.parse(data.last_pin_timestamp!));
|
||||
expect(instance.lastPinAt?.toISOString()).toBe(data.last_pin_timestamp);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||
expect(instance.topic).toBe(data.topic);
|
||||
expect(instance.type).toBe(ChannelType.GuildText);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new TextChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
|
||||
test('PermissionOverwrite sub-structure', () => {
|
||||
const instances = data.permission_overwrites?.map((overwrite) => new PermissionOverwrite(overwrite));
|
||||
expect(instances?.map((overwrite) => overwrite.toJSON())).toEqual(data.permission_overwrites);
|
||||
expect(instances?.[0]?.allow?.toJSON()).toBe(data.permission_overwrites?.[0]?.allow);
|
||||
expect(instances?.[0]?.deny?.toJSON()).toBe(data.permission_overwrites?.[0]?.deny);
|
||||
expect(instances?.[0]?.id).toBe(data.permission_overwrites?.[0]?.id);
|
||||
expect(instances?.[0]?.type).toBe(data.permission_overwrites?.[0]?.type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('announcement channel', () => {
|
||||
const data: APINewsChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildAnnouncement,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
last_message_id: '3',
|
||||
last_pin_timestamp: null,
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
rate_limit_per_user: 9,
|
||||
topic: 'hello',
|
||||
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||
default_thread_rate_limit_per_user: 30,
|
||||
};
|
||||
|
||||
test('AnnouncementChannel has all properties', () => {
|
||||
const instance = new AnnouncementChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance.lastPinTimestamp).toBe(null);
|
||||
expect(instance.lastPinAt).toBe(data.last_pin_timestamp);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||
expect(instance.topic).toBe(data.topic);
|
||||
expect(instance.type).toBe(ChannelType.GuildAnnouncement);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new AnnouncementChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('category channel', () => {
|
||||
const data: APIGuildCategoryChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildCategory,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('CategoryChannel has all properties', () => {
|
||||
const instance = new CategoryChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.type).toBe(ChannelType.GuildCategory);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new CategoryChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(false);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DM channel', () => {
|
||||
const dataNoRecipients: APIDMChannel = {
|
||||
id: '1',
|
||||
type: ChannelType.DM,
|
||||
last_message_id: '3',
|
||||
last_pin_timestamp: '2020-10-10T13:50:17.209Z',
|
||||
name: null,
|
||||
};
|
||||
|
||||
const data = {
|
||||
...dataNoRecipients,
|
||||
recipients: [
|
||||
{
|
||||
avatar: '123',
|
||||
discriminator: '0',
|
||||
global_name: 'tester',
|
||||
id: '1',
|
||||
username: 'test',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('DMChannel has all properties', () => {
|
||||
const instance = new DMChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance.lastPinTimestamp).toBe(Date.parse(data.last_pin_timestamp!));
|
||||
expect(instance.lastPinAt?.toISOString()).toBe(data.last_pin_timestamp);
|
||||
expect(instance[kData].recipients).toEqual(data.recipients);
|
||||
expect(instance.type).toBe(ChannelType.DM);
|
||||
expect(instance.url).toBe('https://discord.com/channels/@me/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('DMChannel with no recipients', () => {
|
||||
const instance = new DMChannel(dataNoRecipients);
|
||||
expect(instance[kData].recipients).toEqual(dataNoRecipients.recipients);
|
||||
expect(instance.toJSON()).toEqual(dataNoRecipients);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new DMChannel(data);
|
||||
expect(instance.isDMBased()).toBe(true);
|
||||
expect(instance.isGuildBased()).toBe(false);
|
||||
expect(instance.isPermissionCapable()).toBe(false);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupDM channel', () => {
|
||||
const data: APIGroupDMChannel = {
|
||||
id: '1',
|
||||
type: ChannelType.GroupDM,
|
||||
last_message_id: '3',
|
||||
name: 'name',
|
||||
recipients: [
|
||||
{
|
||||
avatar: '123',
|
||||
discriminator: '0',
|
||||
global_name: 'tester',
|
||||
id: '1',
|
||||
username: 'test',
|
||||
},
|
||||
],
|
||||
last_pin_timestamp: null,
|
||||
application_id: '34',
|
||||
icon: 'abc',
|
||||
managed: true,
|
||||
owner_id: '567',
|
||||
};
|
||||
|
||||
test('GroupDMChannel has all properties', () => {
|
||||
const instance = new GroupDMChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance[kData].recipients).toEqual(data.recipients);
|
||||
expect(instance.applicationId).toBe(data.application_id);
|
||||
expect(instance.managed).toBe(data.managed);
|
||||
expect(instance.ownerId).toBe(data.owner_id);
|
||||
expect(instance.type).toBe(ChannelType.GroupDM);
|
||||
expect(instance.icon).toBe(data.icon);
|
||||
expect(instance.url).toBe('https://discord.com/channels/@me/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new GroupDMChannel(data);
|
||||
expect(instance.isDMBased()).toBe(true);
|
||||
expect(instance.isGuildBased()).toBe(false);
|
||||
expect(instance.isPermissionCapable()).toBe(false);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forum channel', () => {
|
||||
const dataNoTags: Omit<APIGuildForumChannel, 'available_tags'> = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildForum,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
topic: 'hello',
|
||||
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||
default_thread_rate_limit_per_user: 30,
|
||||
default_forum_layout: ForumLayoutType.GalleryView,
|
||||
default_reaction_emoji: {
|
||||
emoji_id: '159',
|
||||
emoji_name: null,
|
||||
},
|
||||
default_sort_order: SortOrderType.LatestActivity,
|
||||
};
|
||||
const data: APIGuildForumChannel = {
|
||||
...dataNoTags,
|
||||
available_tags: [
|
||||
{
|
||||
name: 'emoji',
|
||||
emoji_name: '😀',
|
||||
moderated: false,
|
||||
id: '789',
|
||||
emoji_id: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('ForumChannel has all properties', () => {
|
||||
const instance = new ForumChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.defaultForumLayout).toBe(data.default_forum_layout);
|
||||
expect(instance.defaultReactionEmoji).toBe(data.default_reaction_emoji);
|
||||
expect(instance.defaultSortOrder).toBe(data.default_sort_order);
|
||||
expect(instance[kData].available_tags).toEqual(data.available_tags);
|
||||
expect(instance.topic).toBe(data.topic);
|
||||
expect(instance.type).toBe(ChannelType.GuildForum);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new ForumChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(false);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(true);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
|
||||
test('ForumTag has all properties', () => {
|
||||
const instances = data.available_tags.map((tag) => new ForumTag(tag));
|
||||
expect(instances.map((tag) => tag.toJSON())).toEqual(data.available_tags);
|
||||
expect(instances[0]?.id).toBe(data.available_tags[0]?.id);
|
||||
expect(instances[0]?.emojiId).toBe(data.available_tags[0]?.emoji_id);
|
||||
expect(instances[0]?.emojiName).toBe(data.available_tags[0]?.emoji_name);
|
||||
expect(instances[0]?.name).toBe(data.available_tags[0]?.name);
|
||||
expect(instances[0]?.moderated).toBe(data.available_tags[0]?.moderated);
|
||||
expect(instances[0]?.emoji).toBe(data.available_tags[0]?.emoji_name);
|
||||
});
|
||||
|
||||
test('omitted property from ForumChannel', () => {
|
||||
const instance = new ForumChannel(dataNoTags);
|
||||
expect(instance.toJSON()).toEqual(dataNoTags);
|
||||
});
|
||||
});
|
||||
|
||||
describe('media channel', () => {
|
||||
const data: APIGuildMediaChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildMedia,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
topic: 'hello',
|
||||
default_auto_archive_duration: ThreadAutoArchiveDuration.OneHour,
|
||||
default_thread_rate_limit_per_user: 30,
|
||||
available_tags: [
|
||||
{
|
||||
name: 'emoji',
|
||||
emoji_name: null,
|
||||
moderated: false,
|
||||
id: '789',
|
||||
emoji_id: '444',
|
||||
},
|
||||
],
|
||||
default_reaction_emoji: {
|
||||
emoji_id: '159',
|
||||
emoji_name: null,
|
||||
},
|
||||
default_sort_order: SortOrderType.LatestActivity,
|
||||
};
|
||||
|
||||
test('MediaChannel has all properties', () => {
|
||||
const instance = new MediaChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.defaultAutoArchiveDuration).toBe(data.default_auto_archive_duration);
|
||||
expect(instance.defaultThreadRateLimitPerUser).toBe(data.default_thread_rate_limit_per_user);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance[kData].available_tags).toEqual(data.available_tags);
|
||||
expect(instance.topic).toBe(data.topic);
|
||||
expect(instance.type).toBe(ChannelType.GuildMedia);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new MediaChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(false);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(true);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
|
||||
test('ForumTag has all properties', () => {
|
||||
const instances = data.available_tags.map((tag) => new ForumTag(tag));
|
||||
expect(instances.map((tag) => tag.toJSON())).toEqual(data.available_tags);
|
||||
expect(instances[0]?.emoji).toBe(`<:_:${data.available_tags[0]?.emoji_id}>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('voice channel', () => {
|
||||
const data: APIGuildVoiceChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildVoice,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
last_message_id: '3',
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
rate_limit_per_user: 9,
|
||||
bitrate: 7,
|
||||
rtc_region: 'somewhere',
|
||||
user_limit: 100,
|
||||
video_quality_mode: VideoQualityMode.Full,
|
||||
};
|
||||
|
||||
test('VoiceChannel has all properties', () => {
|
||||
const instance = new VoiceChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.bitrate).toBe(data.bitrate);
|
||||
expect(instance.rtcRegion).toBe(data.rtc_region);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance.videoQualityMode).toBe(data.video_quality_mode);
|
||||
expect(instance.userLimit).toBe(data.user_limit);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||
expect(instance.type).toBe(ChannelType.GuildVoice);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new VoiceChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(true);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage channel', () => {
|
||||
const data: APIGuildStageVoiceChannel = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.GuildStageVoice,
|
||||
position: 0,
|
||||
guild_id: '2',
|
||||
last_message_id: '3',
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
permission_overwrites: [
|
||||
{
|
||||
allow: '123',
|
||||
deny: '456',
|
||||
type: OverwriteType.Member,
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
rate_limit_per_user: 9,
|
||||
bitrate: 7,
|
||||
rtc_region: 'somewhere',
|
||||
user_limit: 100,
|
||||
video_quality_mode: VideoQualityMode.Full,
|
||||
};
|
||||
|
||||
test('StageChannel has all properties', () => {
|
||||
const instance = new StageChannel(data);
|
||||
expect(instance.id).toBe(data.id);
|
||||
expect(instance.name).toBe(data.name);
|
||||
expect(instance.position).toBe(data.position);
|
||||
expect(instance.bitrate).toBe(data.bitrate);
|
||||
expect(instance.rtcRegion).toBe(data.rtc_region);
|
||||
expect(instance.flags?.toJSON()).toBe(data.flags);
|
||||
expect(instance.guildId).toBe(data.guild_id);
|
||||
expect(instance.lastMessageId).toBe(data.last_message_id);
|
||||
expect(instance.videoQualityMode).toBe(data.video_quality_mode);
|
||||
expect(instance.nsfw).toBe(data.nsfw);
|
||||
expect(instance.parentId).toBe(data.parent_id);
|
||||
expect(instance[kData].permission_overwrites).toEqual(data.permission_overwrites);
|
||||
expect(instance.rateLimitPerUser).toBe(data.rate_limit_per_user);
|
||||
expect(instance.type).toBe(ChannelType.GuildStageVoice);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
});
|
||||
|
||||
test('type guards', () => {
|
||||
const instance = new StageChannel(data);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(true);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(false);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(true);
|
||||
expect(instance.isWebhookCapable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('thread channels', () => {
|
||||
const dataNoTags: Omit<APIPublicThreadChannel, 'applied_tags'> = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: ChannelType.PublicThread,
|
||||
guild_id: '2',
|
||||
last_message_id: '3',
|
||||
last_pin_timestamp: null,
|
||||
nsfw: true,
|
||||
parent_id: '4',
|
||||
rate_limit_per_user: 9,
|
||||
};
|
||||
|
||||
const dataPublic: APIPublicThreadChannel = {
|
||||
...dataNoTags,
|
||||
applied_tags: ['567'],
|
||||
};
|
||||
|
||||
const dataAnnounce: APIAnnouncementThreadChannel = {
|
||||
...dataPublic,
|
||||
thread_metadata: {
|
||||
archive_timestamp: '2024-09-08T12:01:02.345Z',
|
||||
archived: false,
|
||||
auto_archive_duration: ThreadAutoArchiveDuration.ThreeDays,
|
||||
locked: true,
|
||||
},
|
||||
flags: ChannelFlags.Pinned,
|
||||
type: ChannelType.AnnouncementThread,
|
||||
};
|
||||
|
||||
const dataPrivate: APIPrivateThreadChannel = {
|
||||
...dataPublic,
|
||||
thread_metadata: {
|
||||
...dataAnnounce.thread_metadata!,
|
||||
create_timestamp: '2023-01-02T15:13:11.987Z',
|
||||
invitable: true,
|
||||
},
|
||||
type: ChannelType.PrivateThread,
|
||||
};
|
||||
|
||||
test('PublicThreadChannel has all properties', () => {
|
||||
const instance = new PublicThreadChannel(dataPublic);
|
||||
expect(instance.id).toBe(dataPublic.id);
|
||||
expect(instance.name).toBe(dataPublic.name);
|
||||
expect(instance.flags?.toJSON()).toBe(dataPublic.flags);
|
||||
expect(instance.guildId).toBe(dataPublic.guild_id);
|
||||
expect(instance.lastMessageId).toBe(dataPublic.last_message_id);
|
||||
expect(instance.nsfw).toBe(dataPublic.nsfw);
|
||||
expect(instance.parentId).toBe(dataPublic.parent_id);
|
||||
expect(instance.rateLimitPerUser).toBe(dataPublic.rate_limit_per_user);
|
||||
expect(instance.type).toBe(ChannelType.PublicThread);
|
||||
expect(instance.appliedTags).toEqual(dataPublic.applied_tags);
|
||||
expect(instance.memberCount).toBe(dataPublic.member_count);
|
||||
expect(instance.messageCount).toBe(dataPublic.message_count);
|
||||
expect(instance.totalMessageSent).toBe(dataPublic.total_message_sent);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(dataPublic);
|
||||
});
|
||||
|
||||
test('type guards PublicThread', () => {
|
||||
const instance = new PublicThreadChannel(dataPublic);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(false);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(true);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
|
||||
test('PrivateThreadChannel has all properties', () => {
|
||||
const instance = new PrivateThreadChannel(dataPrivate);
|
||||
expect(instance.id).toBe(dataPrivate.id);
|
||||
expect(instance.name).toBe(dataPrivate.name);
|
||||
expect(instance.flags?.toJSON()).toBe(dataPrivate.flags);
|
||||
expect(instance.guildId).toBe(dataPrivate.guild_id);
|
||||
expect(instance.lastMessageId).toBe(dataPrivate.last_message_id);
|
||||
expect(instance.nsfw).toBe(dataPrivate.nsfw);
|
||||
expect(instance.parentId).toBe(dataPrivate.parent_id);
|
||||
expect(instance.rateLimitPerUser).toBe(dataPrivate.rate_limit_per_user);
|
||||
expect(instance[kData].thread_metadata).toEqual(dataPrivate.thread_metadata);
|
||||
expect(instance.type).toBe(ChannelType.PrivateThread);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(dataPrivate);
|
||||
});
|
||||
|
||||
test('type guards PrivateThread', () => {
|
||||
const instance = new PrivateThreadChannel(dataPrivate);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(false);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(true);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
|
||||
test('AnnouncementThreadChannel has all properties', () => {
|
||||
const instance = new AnnouncementThreadChannel(dataAnnounce);
|
||||
expect(instance.id).toBe(dataAnnounce.id);
|
||||
expect(instance.name).toBe(dataAnnounce.name);
|
||||
expect(instance.flags?.toJSON()).toBe(dataAnnounce.flags);
|
||||
expect(instance.guildId).toBe(dataAnnounce.guild_id);
|
||||
expect(instance.lastMessageId).toBe(dataAnnounce.last_message_id);
|
||||
expect(instance.nsfw).toBe(dataAnnounce.nsfw);
|
||||
expect(instance.parentId).toBe(dataAnnounce.parent_id);
|
||||
expect(instance.rateLimitPerUser).toBe(dataAnnounce.rate_limit_per_user);
|
||||
expect(instance[kData].thread_metadata).toEqual(dataAnnounce.thread_metadata);
|
||||
expect(instance.type).toBe(ChannelType.AnnouncementThread);
|
||||
expect(instance.url).toBe('https://discord.com/channels/2/1');
|
||||
expect(instance.toJSON()).toEqual(dataAnnounce);
|
||||
});
|
||||
|
||||
test('type guards AnnouncementThread', () => {
|
||||
const instance = new AnnouncementThreadChannel(dataAnnounce);
|
||||
expect(instance.isDMBased()).toBe(false);
|
||||
expect(instance.isGuildBased()).toBe(true);
|
||||
expect(instance.isPermissionCapable()).toBe(false);
|
||||
expect(instance.isTextBased()).toBe(true);
|
||||
expect(instance.isThread()).toBe(true);
|
||||
expect(instance.isThreadOnly()).toBe(false);
|
||||
expect(instance.isVoiceBased()).toBe(false);
|
||||
expect(instance.isWebhookCapable()).toBe(false);
|
||||
});
|
||||
|
||||
test('omitted property from PublicThread', () => {
|
||||
const instance = new PublicThreadChannel(dataNoTags);
|
||||
expect(instance.toJSON()).toEqual(dataNoTags);
|
||||
expect(instance.appliedTags).toBe(null);
|
||||
});
|
||||
|
||||
test('ThreadMetadata has all properties', () => {
|
||||
const instance = new ThreadMetadata(dataPrivate.thread_metadata!);
|
||||
expect(instance.toJSON()).toEqual(dataPrivate.thread_metadata);
|
||||
expect(instance.archived).toBe(dataPrivate.thread_metadata?.archived);
|
||||
expect(instance.archivedAt?.toISOString()).toBe(dataPrivate.thread_metadata?.archive_timestamp);
|
||||
expect(instance.archivedTimestamp).toBe(Date.parse(dataPrivate.thread_metadata!.archive_timestamp));
|
||||
expect(instance.createdAt?.toISOString()).toBe(dataPrivate.thread_metadata?.create_timestamp);
|
||||
expect(instance.createdTimestamp).toBe(Date.parse(dataPrivate.thread_metadata!.create_timestamp!));
|
||||
expect(instance.autoArchiveDuration).toBe(dataPrivate.thread_metadata?.auto_archive_duration);
|
||||
expect(instance.invitable).toBe(dataPrivate.thread_metadata?.invitable);
|
||||
expect(instance.locked).toBe(dataPrivate.thread_metadata?.locked);
|
||||
});
|
||||
});
|
||||
90
packages/structures/__tests__/invite.test.ts
Normal file
90
packages/structures/__tests__/invite.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { APIExtendedInvite, APIInvite } from 'discord-api-types/v10';
|
||||
import { InviteTargetType, InviteType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { Invite } from '../src/index.js';
|
||||
import { kPatch } from '../src/utils/symbols.js';
|
||||
|
||||
describe('Invite', () => {
|
||||
const dataNoCode: Omit<APIInvite, 'code'> = {
|
||||
type: InviteType.Guild,
|
||||
channel: null,
|
||||
approximate_member_count: 15,
|
||||
approximate_presence_count: 35,
|
||||
target_type: InviteTargetType.EmbeddedApplication,
|
||||
};
|
||||
|
||||
const data: APIInvite = {
|
||||
...dataNoCode,
|
||||
code: '123',
|
||||
};
|
||||
|
||||
const dataExtended: APIExtendedInvite = {
|
||||
...data,
|
||||
created_at: '2020-10-10T13:50:17.209Z',
|
||||
max_age: 12,
|
||||
max_uses: 34,
|
||||
temporary: false,
|
||||
uses: 5,
|
||||
};
|
||||
|
||||
test('Invite has all properties', () => {
|
||||
const instance = new Invite(data);
|
||||
expect(instance.type).toBe(data.type);
|
||||
expect(instance.code).toBe(data.code);
|
||||
expect(instance.createdAt).toBe(null);
|
||||
expect(instance.createdTimestamp).toBe(null);
|
||||
expect(instance.maxAge).toBe(undefined);
|
||||
expect(instance.maxUses).toBe(undefined);
|
||||
expect(instance.approximateMemberCount).toBe(data.approximate_member_count);
|
||||
expect(instance.approximatePresenceCount).toBe(data.approximate_presence_count);
|
||||
expect(instance.targetType).toBe(data.target_type);
|
||||
expect(instance.temporary).toBe(undefined);
|
||||
expect(instance.uses).toBe(undefined);
|
||||
expect(instance.expiresTimestamp).toBe(null);
|
||||
expect(instance.expiresAt).toBe(null);
|
||||
expect(instance.url).toBe('https://discord.gg/123');
|
||||
expect(instance.toJSON()).toEqual(data);
|
||||
expect(`${instance}`).toBe('https://discord.gg/123');
|
||||
expect(instance.valueOf()).toBe(data.code);
|
||||
});
|
||||
|
||||
test('extended Invite has all properties', () => {
|
||||
const instance = new Invite(dataExtended);
|
||||
expect(instance.type).toBe(data.type);
|
||||
expect(instance.code).toBe(dataExtended.code);
|
||||
expect(instance.createdAt?.toISOString()).toBe(dataExtended.created_at);
|
||||
expect(instance.createdTimestamp).toBe(Date.parse(dataExtended.created_at));
|
||||
expect(instance.maxAge).toBe(dataExtended.max_age);
|
||||
expect(instance.maxUses).toBe(dataExtended.max_uses);
|
||||
expect(instance.approximateMemberCount).toBe(dataExtended.approximate_member_count);
|
||||
expect(instance.approximatePresenceCount).toBe(dataExtended.approximate_presence_count);
|
||||
expect(instance.targetType).toBe(dataExtended.target_type);
|
||||
expect(instance.temporary).toBe(dataExtended.temporary);
|
||||
expect(instance.uses).toBe(dataExtended.uses);
|
||||
expect(instance.expiresTimestamp).toStrictEqual(Date.parse('2020-10-10T13:50:29.209Z'));
|
||||
expect(instance.expiresAt).toStrictEqual(new Date('2020-10-10T13:50:29.209Z'));
|
||||
expect(instance.url).toBe('https://discord.gg/123');
|
||||
expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||
});
|
||||
|
||||
test('Invite with omitted properties', () => {
|
||||
const instance = new Invite(dataNoCode);
|
||||
expect(instance.toJSON()).toEqual(dataNoCode);
|
||||
expect(instance.url).toBe(null);
|
||||
expect(instance.code).toBe(undefined);
|
||||
expect(`${instance}`).toBe('');
|
||||
expect(instance.valueOf()).toEqual(Object.prototype.valueOf.apply(instance));
|
||||
});
|
||||
|
||||
test('Invite with expiration', () => {
|
||||
const instance = new Invite({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||
expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
|
||||
});
|
||||
|
||||
test('Patching Invite works in place', () => {
|
||||
const instance1 = new Invite(data);
|
||||
const instance2 = instance1[kPatch]({ max_age: 34 });
|
||||
expect(instance1.toJSON()).not.toEqual(data);
|
||||
expect(instance2).toBe(instance1);
|
||||
});
|
||||
});
|
||||
132
packages/structures/__tests__/mixinClasses.ts
Normal file
132
packages/structures/__tests__/mixinClasses.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Mixin } from '../src/Mixin.js';
|
||||
import type { MixinTypes } from '../src/MixinTypes.d.ts';
|
||||
import { Structure } from '../src/Structure.js';
|
||||
import { kData, kMixinConstruct, kMixinToJSON, kPatch } from '../src/utils/symbols.js';
|
||||
|
||||
export interface APIData {
|
||||
baseOptimize?: string;
|
||||
id: string;
|
||||
mixinOptimize?: string;
|
||||
property1?: number;
|
||||
property2?: boolean;
|
||||
}
|
||||
|
||||
export class Base<Omitted extends keyof APIData | '' = ''> extends Structure<APIData, Omitted> {
|
||||
public static override readonly DataTemplate = {
|
||||
set baseOptimize(_: unknown) {},
|
||||
};
|
||||
|
||||
public baseOptimize: boolean | null = null;
|
||||
|
||||
public constructor(data: APIData) {
|
||||
super(data);
|
||||
this.optimizeData(data);
|
||||
}
|
||||
|
||||
public override [kPatch](data: Partial<APIData>) {
|
||||
super[kPatch](data);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override optimizeData(data: Partial<APIData>) {
|
||||
if ('baseOptimize' in data) {
|
||||
this.baseOptimize = Boolean(data.baseOptimize);
|
||||
}
|
||||
}
|
||||
|
||||
public get id() {
|
||||
return this[kData].id;
|
||||
}
|
||||
|
||||
public getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public override toJSON() {
|
||||
const data = super.toJSON();
|
||||
if (this.baseOptimize) {
|
||||
data.baseOptimize = String(this.baseOptimize);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MixinProperty1<Omitted extends keyof APIData | '' = ''> extends Base<Omitted> {
|
||||
mixinOptimize: boolean | null;
|
||||
}
|
||||
export class MixinProperty1 {
|
||||
public static readonly DataTemplate = {
|
||||
set mixinOptimize(_: unknown) {},
|
||||
};
|
||||
|
||||
public [kMixinConstruct]() {
|
||||
this.mixinOptimize = null;
|
||||
}
|
||||
|
||||
public optimizeData(data: Partial<APIData>) {
|
||||
if ('mixinOptimize' in data) {
|
||||
this.mixinOptimize = Boolean(data.mixinOptimize);
|
||||
}
|
||||
}
|
||||
|
||||
public get property1() {
|
||||
return this[kData].property1;
|
||||
}
|
||||
|
||||
public getProperty1() {
|
||||
return this.property1;
|
||||
}
|
||||
|
||||
protected [kMixinToJSON](data: Partial<APIData>) {
|
||||
if (this.mixinOptimize) {
|
||||
data.mixinOptimize = String(this.mixinOptimize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MixinProperty2<Omitted extends keyof APIData | '' = ''> extends Base<Omitted> {
|
||||
constructCalled: boolean;
|
||||
}
|
||||
export class MixinProperty2 {
|
||||
public [kMixinConstruct]() {
|
||||
this.constructCalled = true;
|
||||
}
|
||||
|
||||
public get property2() {
|
||||
return this[kData].property2;
|
||||
}
|
||||
|
||||
public getProperty2() {
|
||||
return this.property2;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedMixinProperty2 extends MixinProperty2 {
|
||||
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
||||
public get isExtended() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Mixed extends MixinTypes<Base, [MixinProperty1, MixinProperty2]> {}
|
||||
export class Mixed extends Base {
|
||||
public getProperties() {
|
||||
return { property1: this.property1, property2: this.property2 };
|
||||
}
|
||||
}
|
||||
|
||||
Mixin(Mixed, [MixinProperty1, MixinProperty2]);
|
||||
|
||||
export interface MixedWithExtended extends MixinTypes<Base, [MixinProperty1, ExtendedMixinProperty2]> {}
|
||||
export class MixedWithExtended extends Base {
|
||||
public getProperties() {
|
||||
return {
|
||||
property1: this.property1,
|
||||
property2: this.property2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Intentionally don't directly mix Property 2
|
||||
Mixin(MixedWithExtended, [MixinProperty1, ExtendedMixinProperty2]);
|
||||
40
packages/structures/__tests__/types/Mixin.test-d.ts
Normal file
40
packages/structures/__tests__/types/Mixin.test-d.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { expectNotType, expectType } from 'tsd';
|
||||
import { expectTypeOf } from 'vitest';
|
||||
import type { MixinTypes } from '../../src/MixinTypes.d.ts';
|
||||
import type { kMixinConstruct } from '../../src/utils/symbols.js';
|
||||
import type { MixinProperty1, Base, MixinProperty2 } from '../mixinClasses.js';
|
||||
|
||||
declare const extendsNoOmit: Omit<MixinProperty1, keyof Base | typeof kMixinConstruct>;
|
||||
declare const extendsOmitProperty1: Omit<MixinProperty1<'property1'>, keyof Base | typeof kMixinConstruct>;
|
||||
declare const extendsBothNoOmit: Omit<MixinProperty1 & MixinProperty2, keyof Base | typeof kMixinConstruct>;
|
||||
declare const extendsBothOmitProperty1: Omit<
|
||||
MixinProperty1<'property1'> & MixinProperty2<'property1'>,
|
||||
keyof Base | typeof kMixinConstruct
|
||||
>;
|
||||
declare const extendsBothOmitBoth: Omit<
|
||||
MixinProperty1<'property1'> & MixinProperty2<'property2'>,
|
||||
keyof Base | typeof kMixinConstruct
|
||||
>;
|
||||
|
||||
expectType<MixinTypes<Base, [MixinProperty1]>>(extendsNoOmit);
|
||||
expectType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>]>>(extendsOmitProperty1);
|
||||
expectNotType<MixinTypes<Base, [MixinProperty1]>>(extendsOmitProperty1);
|
||||
expectNotType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>]>>(extendsNoOmit);
|
||||
|
||||
expectType<MixinTypes<Base, [MixinProperty1, MixinProperty2]>>(extendsBothNoOmit);
|
||||
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
|
||||
expectType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothOmitProperty1);
|
||||
expectNotType<MixinTypes<Base, [MixinProperty1, MixinProperty2]>>(extendsBothOmitProperty1);
|
||||
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
|
||||
expectNotType<MixinTypes<Base<'property1'>, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothNoOmit);
|
||||
|
||||
// Earlier mixins in the list must specify all properties because of the way merging works
|
||||
expectType<
|
||||
MixinTypes<Base<'property1' | 'property2'>, [MixinProperty1<'property1' | 'property2'>, MixinProperty2<'property2'>]>
|
||||
>(extendsBothOmitBoth);
|
||||
|
||||
expectTypeOf<MixinTypes<Base<'property1'>, [MixinProperty1]>>().toBeNever();
|
||||
// @ts-expect-error Shouldn't be able to assign non identical omits
|
||||
expectTypeOf<MixinTypes<Base, [MixinProperty1<'property1'>]>>()
|
||||
// Separate line so ts-expect-error doesn't match this ever
|
||||
.toBeNever();
|
||||
79
packages/structures/__tests__/types/channels.test-d.ts
Normal file
79
packages/structures/__tests__/types/channels.test-d.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { ChannelType, GuildChannelType, GuildTextChannelType, ThreadChannelType } from 'discord-api-types/v10';
|
||||
import { expectNever, expectType } from 'tsd';
|
||||
import type { Channel } from '../../src/index.js';
|
||||
|
||||
declare const channel: Channel;
|
||||
|
||||
if (channel.isGuildBased()) {
|
||||
expectType<string>(channel.guildId);
|
||||
expectType<GuildChannelType>(channel.type);
|
||||
|
||||
if (channel.isDMBased()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isPermissionCapable()) {
|
||||
expectType<Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType>>(channel.type);
|
||||
}
|
||||
|
||||
if (channel.isTextBased()) {
|
||||
expectType<GuildTextChannelType>(channel.type);
|
||||
}
|
||||
|
||||
if (channel.isWebhookCapable()) {
|
||||
expectType<ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType>>(
|
||||
channel.type,
|
||||
);
|
||||
}
|
||||
|
||||
if (channel.isThread()) {
|
||||
expectType<ThreadChannelType>(channel.type);
|
||||
}
|
||||
|
||||
if (channel.isThreadOnly()) {
|
||||
expectType<ChannelType.GuildForum | ChannelType.GuildMedia>(channel.type);
|
||||
}
|
||||
|
||||
if (channel.isVoiceBased()) {
|
||||
expectType<ChannelType.GuildStageVoice | ChannelType.GuildVoice>(channel.type);
|
||||
if (!channel.isTextBased()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (!channel.isWebhookCapable()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.isDMBased()) {
|
||||
expectType<ChannelType.DM | ChannelType.GroupDM>(channel.type);
|
||||
|
||||
if (channel.isGuildBased()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isPermissionCapable()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isWebhookCapable()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isVoiceBased()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isThread()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isThreadOnly()) {
|
||||
expectNever(channel);
|
||||
}
|
||||
|
||||
if (channel.isTextBased()) {
|
||||
expectType<ChannelType.DM | ChannelType.GroupDM>(channel.type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user