rendering to channel + simplified adapter interface
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
import type { Channel } from "../../internal/channel"
|
|
||||||
import type {
|
|
||||||
CommandInteraction,
|
|
||||||
ComponentInteraction,
|
|
||||||
} from "../../internal/interaction"
|
|
||||||
|
|
||||||
export type AdapterGenerics = {
|
|
||||||
commandReplyInit: unknown
|
|
||||||
channelInit: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Adapter<Generics extends AdapterGenerics> = {
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
addComponentInteractionListener(
|
|
||||||
listener: (interaction: ComponentInteraction) => void,
|
|
||||||
): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
createCommandInteraction(
|
|
||||||
init: Generics["commandReplyInit"],
|
|
||||||
): CommandInteraction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
createChannel(init: Generics["channelInit"]): Channel
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,35 @@
|
|||||||
import type * as Discord from "discord.js"
|
import type * as Discord from "discord.js"
|
||||||
import { raise } from "../../../helpers/raise"
|
import { raise } from "../../helpers/raise"
|
||||||
import { toUpper } from "../../../helpers/to-upper"
|
import { toUpper } from "../../helpers/to-upper"
|
||||||
import type { Channel } from "../../internal/channel"
|
import type { ComponentInteraction } from "../internal/interaction"
|
||||||
import type {
|
import type { Message, MessageOptions } from "../internal/message"
|
||||||
CommandInteraction,
|
import type { ReacordConfig, ReacordInstance } from "./reacord"
|
||||||
ComponentInteraction,
|
import { Reacord } from "./reacord"
|
||||||
} from "../../internal/interaction"
|
|
||||||
import type { Message, MessageOptions } from "../../internal/message"
|
|
||||||
import type { Adapter } from "./adapter"
|
|
||||||
|
|
||||||
type DiscordJsAdapterGenerics = {
|
export class ReacordDiscordJs extends Reacord {
|
||||||
commandReplyInit: Discord.CommandInteraction
|
constructor(client: Discord.Client, config: ReacordConfig = {}) {
|
||||||
channelInit: Discord.TextBasedChannel
|
super(config)
|
||||||
}
|
|
||||||
|
|
||||||
export class DiscordJsAdapter implements Adapter<DiscordJsAdapterGenerics> {
|
client.on("interactionCreate", (interaction) => {
|
||||||
constructor(private client: Discord.Client) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
addComponentInteractionListener(
|
|
||||||
listener: (interaction: ComponentInteraction) => void,
|
|
||||||
) {
|
|
||||||
this.client.on("interactionCreate", (interaction) => {
|
|
||||||
if (interaction.isMessageComponent()) {
|
if (interaction.isMessageComponent()) {
|
||||||
listener(createReacordComponentInteraction(interaction))
|
this.handleComponentInteraction(
|
||||||
|
createReacordComponentInteraction(interaction),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override send(channel: Discord.TextBasedChannel): ReacordInstance {
|
||||||
* @internal
|
return this.createChannelRendererInstance({
|
||||||
*/
|
send: async (options) => {
|
||||||
// eslint-disable-next-line class-methods-use-this
|
const message = await channel.send(getDiscordMessageOptions(options))
|
||||||
createCommandInteraction(
|
return createReacordMessage(message)
|
||||||
interaction: Discord.CommandInteraction,
|
},
|
||||||
): CommandInteraction {
|
})
|
||||||
return {
|
}
|
||||||
|
|
||||||
|
override reply(interaction: Discord.CommandInteraction): ReacordInstance {
|
||||||
|
return this.createCommandReplyRendererInstance({
|
||||||
type: "command",
|
type: "command",
|
||||||
id: interaction.id,
|
id: interaction.id,
|
||||||
channelId: interaction.channelId,
|
channelId: interaction.channelId,
|
||||||
@@ -55,20 +47,7 @@ export class DiscordJsAdapter implements Adapter<DiscordJsAdapterGenerics> {
|
|||||||
})
|
})
|
||||||
return createReacordMessage(message as Discord.Message)
|
return createReacordMessage(message as Discord.Message)
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
createChannel(channel: Discord.TextBasedChannel): Channel {
|
|
||||||
return {
|
|
||||||
send: async (options) => {
|
|
||||||
const message = await channel.send(getDiscordMessageOptions(options))
|
|
||||||
return createReacordMessage(message)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,33 +82,14 @@ function createReacordComponentInteraction(
|
|||||||
raise(`Unsupported component interaction type: ${interaction.type}`)
|
raise(`Unsupported component interaction type: ${interaction.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReacordMessage(message: Discord.Message): Message {
|
// TODO: this could be a part of the core library,
|
||||||
return {
|
// and also handle some edge cases, e.g. empty messages
|
||||||
edit: async (options) => {
|
|
||||||
await message.edit(getDiscordMessageOptions(options))
|
|
||||||
},
|
|
||||||
disableComponents: async () => {
|
|
||||||
for (const actionRow of message.components) {
|
|
||||||
for (const component of actionRow.components) {
|
|
||||||
component.setDisabled(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await message.edit({
|
|
||||||
components: message.components,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
delete: async () => {
|
|
||||||
await message.delete()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDiscordMessageOptions(
|
function getDiscordMessageOptions(
|
||||||
options: MessageOptions,
|
options: MessageOptions,
|
||||||
): Discord.MessageOptions {
|
): Discord.MessageOptions {
|
||||||
return {
|
return {
|
||||||
content: options.content,
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
content: options.content || null,
|
||||||
embeds: options.embeds,
|
embeds: options.embeds,
|
||||||
components: options.actionRows.map((row) => ({
|
components: options.actionRows.map((row) => ({
|
||||||
type: "ACTION_ROW",
|
type: "ACTION_ROW",
|
||||||
@@ -163,3 +123,25 @@ function getDiscordMessageOptions(
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createReacordMessage(message: Discord.Message): Message {
|
||||||
|
return {
|
||||||
|
edit: async (options) => {
|
||||||
|
await message.edit(getDiscordMessageOptions(options))
|
||||||
|
},
|
||||||
|
disableComponents: async () => {
|
||||||
|
for (const actionRow of message.components) {
|
||||||
|
for (const component of actionRow.components) {
|
||||||
|
component.setDisabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.edit({
|
||||||
|
components: message.components,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delete: async () => {
|
||||||
|
await message.delete()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
214
library/core/reacord-tester.ts
Normal file
214
library/core/reacord-tester.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
/* eslint-disable require-await */
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import { nextTick } from "node:process"
|
||||||
|
import { promisify } from "node:util"
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
import { logPretty } from "../../helpers/log-pretty"
|
||||||
|
import { omit } from "../../helpers/omit"
|
||||||
|
import { raise } from "../../helpers/raise"
|
||||||
|
import type { Channel } from "../internal/channel"
|
||||||
|
import { Container } from "../internal/container"
|
||||||
|
import type {
|
||||||
|
ButtonInteraction,
|
||||||
|
CommandInteraction,
|
||||||
|
SelectInteraction,
|
||||||
|
} from "../internal/interaction"
|
||||||
|
import type {
|
||||||
|
Message,
|
||||||
|
MessageButtonOptions,
|
||||||
|
MessageOptions,
|
||||||
|
MessageSelectOptions,
|
||||||
|
} from "../internal/message"
|
||||||
|
import type { ReacordInstance } from "./reacord"
|
||||||
|
import { Reacord } from "./reacord"
|
||||||
|
|
||||||
|
const nextTickPromise = promisify(nextTick)
|
||||||
|
|
||||||
|
export class ReacordTester extends Reacord {
|
||||||
|
private messageContainer = new Container<TestMessage>()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ maxInstances: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
get messages(): readonly TestMessage[] {
|
||||||
|
return [...this.messageContainer]
|
||||||
|
}
|
||||||
|
|
||||||
|
send(): ReacordInstance {
|
||||||
|
return this.createChannelRendererInstance(
|
||||||
|
new TestChannel(this.messageContainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply(): ReacordInstance {
|
||||||
|
return this.createCommandReplyRendererInstance(
|
||||||
|
new TestCommandInteraction(this.messageContainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async assertMessages(expected: ReturnType<this["sampleMessages"]>) {
|
||||||
|
await nextTickPromise()
|
||||||
|
expect(this.sampleMessages()).toEqual(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async assertRender(
|
||||||
|
content: ReactNode,
|
||||||
|
expected: ReturnType<this["sampleMessages"]>,
|
||||||
|
) {
|
||||||
|
const instance = this.reply()
|
||||||
|
instance.render(content)
|
||||||
|
await this.assertMessages(expected)
|
||||||
|
instance.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessages() {
|
||||||
|
logPretty(this.sampleMessages())
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleMessages() {
|
||||||
|
return this.messages.map((message) => ({
|
||||||
|
...message.options,
|
||||||
|
actionRows: message.options.actionRows.map((row) =>
|
||||||
|
row.map((component) =>
|
||||||
|
omit(component, ["customId", "onClick", "onSelect", "onSelectValue"]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
findButtonByLabel(label: string) {
|
||||||
|
for (const message of this.messageContainer) {
|
||||||
|
for (const component of message.options.actionRows.flat()) {
|
||||||
|
if (component.type === "button" && component.label === label) {
|
||||||
|
return this.createButtonActions(component, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raise(`Couldn't find button with label "${label}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
findSelectByPlaceholder(placeholder: string) {
|
||||||
|
for (const message of this.messageContainer) {
|
||||||
|
for (const component of message.options.actionRows.flat()) {
|
||||||
|
if (
|
||||||
|
component.type === "select" &&
|
||||||
|
component.placeholder === placeholder
|
||||||
|
) {
|
||||||
|
return this.createSelectActions(component, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private createButtonActions(
|
||||||
|
button: MessageButtonOptions,
|
||||||
|
message: TestMessage,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
click: () => {
|
||||||
|
this.handleComponentInteraction(
|
||||||
|
new TestButtonInteraction(button.customId, message),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSelectActions(
|
||||||
|
component: MessageSelectOptions,
|
||||||
|
message: TestMessage,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
select: (...values: string[]) => {
|
||||||
|
this.handleComponentInteraction(
|
||||||
|
new TestSelectInteraction(component.customId, message, values),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestMessage implements Message {
|
||||||
|
constructor(
|
||||||
|
public options: MessageOptions,
|
||||||
|
private container: Container<TestMessage>,
|
||||||
|
) {
|
||||||
|
container.add(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(options: MessageOptions): Promise<void> {
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableComponents(): Promise<void> {
|
||||||
|
for (const row of this.options.actionRows) {
|
||||||
|
for (const action of row) {
|
||||||
|
if (action.type === "button") {
|
||||||
|
action.disabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<void> {
|
||||||
|
this.container.remove(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestCommandInteraction implements CommandInteraction {
|
||||||
|
readonly type = "command"
|
||||||
|
readonly id = "test-command-interaction"
|
||||||
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
|
constructor(private messageContainer: Container<TestMessage>) {}
|
||||||
|
|
||||||
|
reply(messageOptions: MessageOptions): Promise<Message> {
|
||||||
|
return Promise.resolve(
|
||||||
|
new TestMessage(messageOptions, this.messageContainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||||
|
return Promise.resolve(
|
||||||
|
new TestMessage(messageOptions, this.messageContainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestButtonInteraction implements ButtonInteraction {
|
||||||
|
readonly type = "button"
|
||||||
|
readonly id = nanoid()
|
||||||
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
|
constructor(readonly customId: string, readonly message: TestMessage) {}
|
||||||
|
|
||||||
|
async update(options: MessageOptions): Promise<void> {
|
||||||
|
this.message.options = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestSelectInteraction implements SelectInteraction {
|
||||||
|
readonly type = "select"
|
||||||
|
readonly id = nanoid()
|
||||||
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly customId: string,
|
||||||
|
readonly message: TestMessage,
|
||||||
|
readonly values: string[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async update(options: MessageOptions): Promise<void> {
|
||||||
|
this.message.options = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestChannel implements Channel {
|
||||||
|
constructor(private messageContainer: Container<TestMessage>) {}
|
||||||
|
|
||||||
|
async send(messageOptions: MessageOptions): Promise<Message> {
|
||||||
|
return new TestMessage(messageOptions, this.messageContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
import type { Channel } from "../internal/channel"
|
||||||
import { ChannelMessageRenderer } from "../internal/channel-message-renderer"
|
import { ChannelMessageRenderer } from "../internal/channel-message-renderer"
|
||||||
import { CommandReplyRenderer } from "../internal/command-reply-renderer.js"
|
import { CommandReplyRenderer } from "../internal/command-reply-renderer.js"
|
||||||
|
import type {
|
||||||
|
CommandInteraction,
|
||||||
|
ComponentInteraction,
|
||||||
|
} from "../internal/interaction"
|
||||||
import { reconciler } from "../internal/reconciler.js"
|
import { reconciler } from "../internal/reconciler.js"
|
||||||
import type { Renderer } from "../internal/renderer"
|
import type { Renderer } from "../internal/renderer"
|
||||||
import type { Adapter, AdapterGenerics } from "./adapters/adapter"
|
|
||||||
|
|
||||||
export type ReacordConfig<Generics extends AdapterGenerics> = {
|
|
||||||
adapter: Adapter<Generics>
|
|
||||||
|
|
||||||
|
export type ReacordConfig = {
|
||||||
/**
|
/**
|
||||||
* The max number of active instances.
|
* The max number of active instances.
|
||||||
* When this limit is exceeded, the oldest instances will be disabled.
|
* When this limit is exceeded, the oldest instances will be disabled.
|
||||||
@@ -21,33 +23,37 @@ export type ReacordInstance = {
|
|||||||
destroy: () => void
|
destroy: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Reacord<Generics extends AdapterGenerics> {
|
export type ComponentInteractionListener = (
|
||||||
|
interaction: ComponentInteraction,
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export abstract class Reacord {
|
||||||
private renderers: Renderer[] = []
|
private renderers: Renderer[] = []
|
||||||
|
|
||||||
constructor(private readonly config: ReacordConfig<Generics>) {
|
constructor(private readonly config: ReacordConfig = {}) {}
|
||||||
config.adapter.addComponentInteractionListener((interaction) => {
|
|
||||||
|
abstract send(channel: unknown): ReacordInstance
|
||||||
|
|
||||||
|
abstract reply(commandInteraction: unknown): ReacordInstance
|
||||||
|
|
||||||
|
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
for (const renderer of this.renderers) {
|
for (const renderer of this.renderers) {
|
||||||
if (renderer.handleComponentInteraction(interaction)) return
|
if (renderer.handleComponentInteraction(interaction)) return
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get maxInstances() {
|
private get maxInstances() {
|
||||||
return this.config.maxInstances ?? 50
|
return this.config.maxInstances ?? 50
|
||||||
}
|
}
|
||||||
|
|
||||||
send(init: Generics["channelInit"]): ReacordInstance {
|
protected createChannelRendererInstance(channel: Channel) {
|
||||||
return this.createInstance(
|
return this.createInstance(new ChannelMessageRenderer(channel))
|
||||||
new ChannelMessageRenderer(this.config.adapter.createChannel(init)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reply(init: Generics["commandReplyInit"]): ReacordInstance {
|
protected createCommandReplyRendererInstance(
|
||||||
return this.createInstance(
|
commandInteraction: CommandInteraction,
|
||||||
new CommandReplyRenderer(
|
): ReacordInstance {
|
||||||
this.config.adapter.createCommandInteraction(init),
|
return this.createInstance(new CommandReplyRenderer(commandInteraction))
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createInstance(renderer: Renderer) {
|
private createInstance(renderer: Renderer) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export * from "./core/adapters/adapter"
|
|
||||||
export * from "./core/adapters/discord-js-adapter"
|
|
||||||
export * from "./core/components/action-row"
|
export * from "./core/components/action-row"
|
||||||
export * from "./core/components/button"
|
export * from "./core/components/button"
|
||||||
export * from "./core/components/embed"
|
export * from "./core/components/embed"
|
||||||
@@ -13,3 +11,5 @@ export * from "./core/components/link"
|
|||||||
export * from "./core/components/option"
|
export * from "./core/components/option"
|
||||||
export * from "./core/components/select"
|
export * from "./core/components/select"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
|
export * from "./core/reacord-discord-js"
|
||||||
|
export * from "./core/reacord-tester"
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
/* eslint-disable require-await */
|
|
||||||
import { nanoid } from "nanoid"
|
|
||||||
import { raise } from "../helpers/raise"
|
|
||||||
import type { Adapter } from "./core/adapters/adapter"
|
|
||||||
import type { Channel } from "./internal/channel"
|
|
||||||
import type {
|
|
||||||
ButtonInteraction,
|
|
||||||
CommandInteraction,
|
|
||||||
ComponentInteraction,
|
|
||||||
SelectInteraction,
|
|
||||||
} from "./internal/interaction"
|
|
||||||
import type {
|
|
||||||
Message,
|
|
||||||
MessageButtonOptions,
|
|
||||||
MessageOptions,
|
|
||||||
MessageSelectOptions,
|
|
||||||
} from "./internal/message"
|
|
||||||
|
|
||||||
type TestAdapterGenerics = {
|
|
||||||
commandReplyInit: TestCommandInteraction
|
|
||||||
channelInit: TestChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestAdapter implements Adapter<TestAdapterGenerics> {
|
|
||||||
messages: TestMessage[] = []
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static create(): Adapter<TestAdapterGenerics> & TestAdapter {
|
|
||||||
return new TestAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
private componentInteractionListener: (
|
|
||||||
interaction: ComponentInteraction,
|
|
||||||
) => void = () => {}
|
|
||||||
|
|
||||||
addComponentInteractionListener(
|
|
||||||
listener: (interaction: ComponentInteraction) => void,
|
|
||||||
): void {
|
|
||||||
this.componentInteractionListener = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
createCommandInteraction(
|
|
||||||
interaction: CommandInteraction,
|
|
||||||
): CommandInteraction {
|
|
||||||
return interaction
|
|
||||||
}
|
|
||||||
|
|
||||||
createChannel(channel: TestChannel): Channel {
|
|
||||||
return channel
|
|
||||||
}
|
|
||||||
|
|
||||||
findButtonByLabel(label: string) {
|
|
||||||
for (const message of this.messages) {
|
|
||||||
for (const component of message.options.actionRows.flat()) {
|
|
||||||
if (component.type === "button" && component.label === label) {
|
|
||||||
return this.createButtonActions(component, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
raise(`Couldn't find button with label "${label}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
findSelectByPlaceholder(placeholder: string) {
|
|
||||||
for (const message of this.messages) {
|
|
||||||
for (const component of message.options.actionRows.flat()) {
|
|
||||||
if (
|
|
||||||
component.type === "select" &&
|
|
||||||
component.placeholder === placeholder
|
|
||||||
) {
|
|
||||||
return this.createSelectActions(component, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeMessage(message: TestMessage) {
|
|
||||||
this.messages = this.messages.filter((m) => m !== message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private createButtonActions(
|
|
||||||
button: MessageButtonOptions,
|
|
||||||
message: TestMessage,
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
click: () => {
|
|
||||||
this.componentInteractionListener(
|
|
||||||
new TestButtonInteraction(button.customId, message),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createSelectActions(
|
|
||||||
component: MessageSelectOptions,
|
|
||||||
message: TestMessage,
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
select: (...values: string[]) => {
|
|
||||||
this.componentInteractionListener(
|
|
||||||
new TestSelectInteraction(component.customId, message, values),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestMessage implements Message {
|
|
||||||
constructor(public options: MessageOptions, private adapter: TestAdapter) {}
|
|
||||||
|
|
||||||
async edit(options: MessageOptions): Promise<void> {
|
|
||||||
this.options = options
|
|
||||||
}
|
|
||||||
|
|
||||||
async disableComponents(): Promise<void> {
|
|
||||||
for (const row of this.options.actionRows) {
|
|
||||||
for (const action of row) {
|
|
||||||
if (action.type === "button") {
|
|
||||||
action.disabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
|
||||||
this.adapter.removeMessage(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestCommandInteraction implements CommandInteraction {
|
|
||||||
readonly type = "command"
|
|
||||||
readonly id = "test-command-interaction"
|
|
||||||
readonly channelId = "test-channel-id"
|
|
||||||
|
|
||||||
constructor(private adapter: TestAdapter) {}
|
|
||||||
|
|
||||||
private createMesssage(messageOptions: MessageOptions): Message {
|
|
||||||
const message = new TestMessage(messageOptions, this.adapter)
|
|
||||||
this.adapter.messages.push(message)
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
reply(messageOptions: MessageOptions): Promise<Message> {
|
|
||||||
return Promise.resolve(this.createMesssage(messageOptions))
|
|
||||||
}
|
|
||||||
|
|
||||||
followUp(messageOptions: MessageOptions): Promise<Message> {
|
|
||||||
return Promise.resolve(this.createMesssage(messageOptions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestButtonInteraction implements ButtonInteraction {
|
|
||||||
readonly type = "button"
|
|
||||||
readonly id = nanoid()
|
|
||||||
readonly channelId = "test-channel-id"
|
|
||||||
|
|
||||||
constructor(readonly customId: string, readonly message: TestMessage) {}
|
|
||||||
|
|
||||||
async update(options: MessageOptions): Promise<void> {
|
|
||||||
this.message.options = options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestSelectInteraction implements SelectInteraction {
|
|
||||||
readonly type = "select"
|
|
||||||
readonly id = nanoid()
|
|
||||||
readonly channelId = "test-channel-id"
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly customId: string,
|
|
||||||
readonly message: TestMessage,
|
|
||||||
readonly values: string[],
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async update(options: MessageOptions): Promise<void> {
|
|
||||||
this.message.options = options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestChannel implements Channel {
|
|
||||||
constructor(private adapter: TestAdapter) {}
|
|
||||||
|
|
||||||
async send(messageOptions: MessageOptions): Promise<Message> {
|
|
||||||
const message = new TestMessage(messageOptions, this.adapter)
|
|
||||||
this.adapter.messages.push(message)
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
"pretty-ms": "^7.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"tsup": "^5.11.9",
|
"tsup": "^5.11.9",
|
||||||
"type-fest": "^2.8.0",
|
"type-fest": "^2.8.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Client } from "discord.js"
|
import { Client } from "discord.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { DiscordJsAdapter, Reacord } from "../library/main"
|
import { ReacordDiscordJs } from "../library/main"
|
||||||
import { createCommandHandler } from "./command-handler"
|
import { createCommandHandler } from "./command-handler"
|
||||||
import { Counter } from "./counter"
|
import { Counter } from "./counter"
|
||||||
import { FruitSelect } from "./fruit-select"
|
import { FruitSelect } from "./fruit-select"
|
||||||
@@ -10,10 +10,38 @@ const client = new Client({
|
|||||||
intents: ["GUILDS"],
|
intents: ["GUILDS"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const reacord = new Reacord({
|
const reacord = new ReacordDiscordJs(client)
|
||||||
adapter: new DiscordJsAdapter(client),
|
|
||||||
maxInstances: 2,
|
// client.on("ready", async () => {
|
||||||
})
|
// const now = new Date()
|
||||||
|
|
||||||
|
// function UptimeCounter() {
|
||||||
|
// const [uptime, setUptime] = React.useState(0)
|
||||||
|
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// const interval = setInterval(() => {
|
||||||
|
// setUptime(Date.now() - now.getTime())
|
||||||
|
// }, 5000)
|
||||||
|
// return () => clearInterval(interval)
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Embed>this bot has been running for {prettyMilliseconds(uptime)}</Embed>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const channelId = "671787605624487941"
|
||||||
|
|
||||||
|
// const channel =
|
||||||
|
// client.channels.cache.get(channelId) ||
|
||||||
|
// (await client.channels.fetch(channelId))
|
||||||
|
|
||||||
|
// if (!channel?.isText()) {
|
||||||
|
// throw new Error("channel is not text")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// reacord.send(channel).render(<UptimeCounter />)
|
||||||
|
// })
|
||||||
|
|
||||||
createCommandHandler(client, [
|
createCommandHandler(client, [
|
||||||
{
|
{
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -35,6 +35,7 @@ importers:
|
|||||||
nanoid: ^3.1.30
|
nanoid: ^3.1.30
|
||||||
nodemon: ^2.0.15
|
nodemon: ^2.0.15
|
||||||
prettier: ^2.5.1
|
prettier: ^2.5.1
|
||||||
|
pretty-ms: ^7.0.1
|
||||||
react: ^17.0.2
|
react: ^17.0.2
|
||||||
react-reconciler: ^0.26.2
|
react-reconciler: ^0.26.2
|
||||||
rxjs: ^7.5.0
|
rxjs: ^7.5.0
|
||||||
@@ -74,6 +75,7 @@ importers:
|
|||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
nodemon: 2.0.15
|
nodemon: 2.0.15
|
||||||
prettier: 2.5.1
|
prettier: 2.5.1
|
||||||
|
pretty-ms: 7.0.1
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
tsup: 5.11.9_typescript@4.5.4
|
tsup: 5.11.9_typescript@4.5.4
|
||||||
type-fest: 2.8.0
|
type-fest: 2.8.0
|
||||||
@@ -5534,6 +5536,11 @@ packages:
|
|||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/parse-ms/2.1.0:
|
||||||
|
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/parse5/6.0.1:
|
/parse5/6.0.1:
|
||||||
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5686,6 +5693,13 @@ packages:
|
|||||||
react-is: 17.0.2
|
react-is: 17.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pretty-ms/7.0.1:
|
||||||
|
resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
parse-ms: 2.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/progress/2.0.3:
|
/progress/2.0.3:
|
||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { ReacordTester } from "../library/core/reacord-tester"
|
||||||
import { ActionRow, Button, Select } from "../library/main"
|
import { ActionRow, Button, Select } from "../library/main"
|
||||||
import { setupReacordTesting } from "./setup-testing"
|
|
||||||
|
|
||||||
const { assertRender } = setupReacordTesting()
|
const testing = new ReacordTester()
|
||||||
|
|
||||||
test("action row", async () => {
|
test("action row", async () => {
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Button label="outside button" onClick={() => {}} />
|
<Button label="outside button" onClick={() => {}} />
|
||||||
<ActionRow>
|
<ActionRow>
|
||||||
|
|||||||
2
test/discord-js.test.tsx
Normal file
2
test/discord-js.test.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
test.todo("discord js integration")
|
||||||
|
export {}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { ReacordTester } from "../library/core/reacord-tester"
|
||||||
import {
|
import {
|
||||||
Embed,
|
Embed,
|
||||||
EmbedAuthor,
|
EmbedAuthor,
|
||||||
@@ -8,14 +9,13 @@ import {
|
|||||||
EmbedThumbnail,
|
EmbedThumbnail,
|
||||||
EmbedTitle,
|
EmbedTitle,
|
||||||
} from "../library/main"
|
} from "../library/main"
|
||||||
import { setupReacordTesting } from "./setup-testing"
|
|
||||||
|
|
||||||
const { assertRender } = setupReacordTesting()
|
const testing = new ReacordTester()
|
||||||
|
|
||||||
test("kitchen sink", async () => {
|
test("kitchen sink", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed color={0xfe_ee_ef}>
|
<Embed color={0xfe_ee_ef}>
|
||||||
<EmbedAuthor name="author" iconUrl="https://example.com/author.png" />
|
<EmbedAuthor name="author" iconUrl="https://example.com/author.png" />
|
||||||
@@ -75,7 +75,7 @@ test("kitchen sink", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("author variants", async () => {
|
test("author variants", async () => {
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedAuthor iconUrl="https://example.com/author.png">
|
<EmbedAuthor iconUrl="https://example.com/author.png">
|
||||||
@@ -110,7 +110,7 @@ test("author variants", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("field variants", async () => {
|
test("field variants", async () => {
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedField name="field name" value="field value" />
|
<EmbedField name="field name" value="field value" />
|
||||||
@@ -157,7 +157,7 @@ test("field variants", async () => {
|
|||||||
test("footer variants", async () => {
|
test("footer variants", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedFooter text="footer text" />
|
<EmbedFooter text="footer text" />
|
||||||
@@ -213,7 +213,7 @@ test("footer variants", async () => {
|
|||||||
test("embed props", async () => {
|
test("embed props", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await assertRender(
|
await testing.assertRender(
|
||||||
<Embed
|
<Embed
|
||||||
title="title text"
|
title="title text"
|
||||||
description="description text"
|
description="description text"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { ReacordTester } from "../library/core/reacord-tester"
|
||||||
import { Link } from "../library/main"
|
import { Link } from "../library/main"
|
||||||
import { setupReacordTesting } from "./setup-testing"
|
|
||||||
|
|
||||||
const { assertRender } = setupReacordTesting()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
test("link", async () => {
|
test("link", async () => {
|
||||||
await assertRender(
|
await tester.assertRender(
|
||||||
<>
|
<>
|
||||||
<Link url="https://example.com/">link text</Link>
|
<Link url="https://example.com/">link text</Link>
|
||||||
<Link label="link text" url="https://example.com/" />
|
<Link label="link text" url="https://example.com/" />
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Button, Embed, EmbedField, EmbedTitle } from "../library/main"
|
import {
|
||||||
import { TestCommandInteraction } from "../library/testing"
|
Button,
|
||||||
import { setupReacordTesting } from "./setup-testing"
|
Embed,
|
||||||
|
EmbedField,
|
||||||
|
EmbedTitle,
|
||||||
|
ReacordTester,
|
||||||
|
} from "../library/main"
|
||||||
|
|
||||||
test("rendering behavior", async () => {
|
test("rendering behavior", async () => {
|
||||||
const { reacord, adapter, assertMessages } = setupReacordTesting()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = reacord.reply(new TestCommandInteraction(adapter))
|
const reply = tester.reply()
|
||||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||||
|
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 0",
|
content: "count: 0",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -35,8 +39,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("show embed").click()
|
tester.findButtonByLabel("show embed").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 0",
|
content: "count: 0",
|
||||||
embeds: [{ title: "the counter" }],
|
embeds: [{ title: "the counter" }],
|
||||||
@@ -62,8 +66,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("clicc").click()
|
tester.findButtonByLabel("clicc").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 1",
|
content: "count: 1",
|
||||||
embeds: [
|
embeds: [
|
||||||
@@ -94,8 +98,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("clicc").click()
|
tester.findButtonByLabel("clicc").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
embeds: [
|
embeds: [
|
||||||
@@ -126,8 +130,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("hide embed").click()
|
tester.findButtonByLabel("hide embed").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -153,8 +157,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("clicc").click()
|
tester.findButtonByLabel("clicc").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -180,8 +184,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("deactivate").click()
|
tester.findButtonByLabel("deactivate").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -210,8 +214,8 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
adapter.findButtonByLabel("clicc").click()
|
tester.findButtonByLabel("clicc").click()
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -242,9 +246,9 @@ test("rendering behavior", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("delete", async () => {
|
test("delete", async () => {
|
||||||
const { reacord, adapter, assertMessages } = setupReacordTesting()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = reacord.reply(new TestCommandInteraction(adapter))
|
const reply = tester.reply()
|
||||||
reply.render(
|
reply.render(
|
||||||
<>
|
<>
|
||||||
some text
|
some text
|
||||||
@@ -253,7 +257,7 @@ test("delete", async () => {
|
|||||||
</>,
|
</>,
|
||||||
)
|
)
|
||||||
|
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "some text",
|
content: "some text",
|
||||||
embeds: [{ description: "some embed" }],
|
embeds: [{ description: "some embed" }],
|
||||||
@@ -264,7 +268,7 @@ test("delete", async () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
reply.destroy()
|
reply.destroy()
|
||||||
await assertMessages([])
|
await tester.assertMessages([])
|
||||||
})
|
})
|
||||||
|
|
||||||
// test multiple instances that can be updated independently,
|
// test multiple instances that can be updated independently,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { jest } from "@jest/globals"
|
import { jest } from "@jest/globals"
|
||||||
import React, { useState } from "react"
|
import React, { useState } from "react"
|
||||||
import { Button, Option, Select } from "../library/main"
|
import { Button, Option, ReacordTester, Select } from "../library/main"
|
||||||
import { setupReacordTesting } from "./setup-testing"
|
|
||||||
|
|
||||||
const { adapter, reply, assertRender, assertMessages } = setupReacordTesting()
|
|
||||||
|
|
||||||
test("single select", async () => {
|
test("single select", async () => {
|
||||||
|
const tester = new ReacordTester()
|
||||||
const onSelect = jest.fn()
|
const onSelect = jest.fn()
|
||||||
|
|
||||||
function TestSelect() {
|
function TestSelect() {
|
||||||
@@ -30,7 +28,7 @@ test("single select", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function assertSelect(values: string[], disabled = false) {
|
async function assertSelect(values: string[], disabled = false) {
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -54,23 +52,26 @@ test("single select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reply = tester.reply()
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
reply.render(<TestSelect />)
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
adapter.findSelectByPlaceholder("choose one").select("2")
|
tester.findSelectByPlaceholder("choose one").select("2")
|
||||||
await assertSelect(["2"])
|
await assertSelect(["2"])
|
||||||
expect(onSelect).toHaveBeenCalledWith({ values: ["2"] })
|
expect(onSelect).toHaveBeenCalledWith({ values: ["2"] })
|
||||||
|
|
||||||
adapter.findButtonByLabel("disable").click()
|
tester.findButtonByLabel("disable").click()
|
||||||
await assertSelect(["2"], true)
|
await assertSelect(["2"], true)
|
||||||
|
|
||||||
adapter.findSelectByPlaceholder("choose one").select("1")
|
tester.findSelectByPlaceholder("choose one").select("1")
|
||||||
await assertSelect(["2"], true)
|
await assertSelect(["2"], true)
|
||||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("multiple select", async () => {
|
test("multiple select", async () => {
|
||||||
|
const tester = new ReacordTester()
|
||||||
const onSelect = jest.fn()
|
const onSelect = jest.fn()
|
||||||
|
|
||||||
function TestSelect() {
|
function TestSelect() {
|
||||||
@@ -91,7 +92,7 @@ test("multiple select", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function assertSelect(values: string[]) {
|
async function assertSelect(values: string[]) {
|
||||||
await assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -115,31 +116,34 @@ test("multiple select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reply = tester.reply()
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
reply.render(<TestSelect />)
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
adapter.findSelectByPlaceholder("select").select("1", "3")
|
tester.findSelectByPlaceholder("select").select("1", "3")
|
||||||
await assertSelect(expect.arrayContaining(["1", "3"]))
|
await assertSelect(expect.arrayContaining(["1", "3"]))
|
||||||
expect(onSelect).toHaveBeenCalledWith({
|
expect(onSelect).toHaveBeenCalledWith({
|
||||||
values: expect.arrayContaining(["1", "3"]),
|
values: expect.arrayContaining(["1", "3"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.findSelectByPlaceholder("select").select("2")
|
tester.findSelectByPlaceholder("select").select("2")
|
||||||
await assertSelect(expect.arrayContaining(["2"]))
|
await assertSelect(expect.arrayContaining(["2"]))
|
||||||
expect(onSelect).toHaveBeenCalledWith({
|
expect(onSelect).toHaveBeenCalledWith({
|
||||||
values: expect.arrayContaining(["2"]),
|
values: expect.arrayContaining(["2"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.findSelectByPlaceholder("select").select()
|
tester.findSelectByPlaceholder("select").select()
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledWith({ values: [] })
|
expect(onSelect).toHaveBeenCalledWith({ values: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("optional onSelect + unknown value", async () => {
|
test("optional onSelect + unknown value", async () => {
|
||||||
reply.render(<Select placeholder="select" />)
|
const tester = new ReacordTester()
|
||||||
adapter.findSelectByPlaceholder("select").select("something")
|
tester.reply().render(<Select placeholder="select" />)
|
||||||
await assertMessages([
|
tester.findSelectByPlaceholder("select").select("something")
|
||||||
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { nextTick } from "node:process"
|
|
||||||
import { promisify } from "node:util"
|
|
||||||
import type { ReactNode } from "react"
|
|
||||||
import { logPretty } from "../helpers/log-pretty"
|
|
||||||
import { omit } from "../helpers/omit"
|
|
||||||
import { Reacord } from "../library/main"
|
|
||||||
import { TestAdapter, TestCommandInteraction } from "../library/testing"
|
|
||||||
|
|
||||||
const nextTickPromise = promisify(nextTick)
|
|
||||||
|
|
||||||
export function setupReacordTesting() {
|
|
||||||
const adapter = TestAdapter.create()
|
|
||||||
const reacord = new Reacord({ adapter })
|
|
||||||
const reply = reacord.reply(new TestCommandInteraction(adapter))
|
|
||||||
|
|
||||||
async function assertMessages(expected: ReturnType<typeof sampleMessages>) {
|
|
||||||
await nextTickPromise() // wait for the render to complete
|
|
||||||
expect(sampleMessages(adapter)).toEqual(expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertRender(
|
|
||||||
content: ReactNode,
|
|
||||||
expected: ReturnType<typeof sampleMessages>,
|
|
||||||
) {
|
|
||||||
reply.render(content)
|
|
||||||
await assertMessages(expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
function logMessages() {
|
|
||||||
logPretty(sampleMessages(adapter))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
reacord,
|
|
||||||
adapter,
|
|
||||||
reply,
|
|
||||||
assertMessages,
|
|
||||||
assertRender,
|
|
||||||
logMessages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sampleMessages(adapter: TestAdapter) {
|
|
||||||
return adapter.messages.map((message) => ({
|
|
||||||
...message.options,
|
|
||||||
actionRows: message.options.actionRows.map((row) =>
|
|
||||||
row.map((component) =>
|
|
||||||
omit(component, ["customId", "onClick", "onSelect", "onSelectValue"]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
7
todo.md
7
todo.md
@@ -1,6 +1,6 @@
|
|||||||
# core features
|
# core features
|
||||||
|
|
||||||
- [ ] render to channel
|
- [x] render to channel
|
||||||
- [x] render to interaction
|
- [x] render to interaction
|
||||||
- [ ] ephemeral messages
|
- [ ] ephemeral messages
|
||||||
- [x] message content
|
- [x] message content
|
||||||
@@ -43,5 +43,6 @@
|
|||||||
- [ ] max instance count per guild
|
- [ ] max instance count per guild
|
||||||
- [ ] max instance count per channel
|
- [ ] max instance count per channel
|
||||||
- [ ] uncontrolled select
|
- [ ] uncontrolled select
|
||||||
- [ ] single class/helper function for testing `ReacordTester`
|
- [x] single class/helper function for testing `ReacordTester`
|
||||||
- [ ] some failsafes and fallbacks in DJS adapter
|
- [ ] handle deletion outside of reacord
|
||||||
|
- [ ] for more easily writing adapters, address discord API nuances at the reacord level instead of the adapter level. the goal being that adapters can just take the objects and send them to discord
|
||||||
|
|||||||
Reference in New Issue
Block a user