kitchen sink test
This commit is contained in:
13
jest.config.js
Normal file
13
jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
const config = {
|
||||
transform: {
|
||||
"^.+\\.[jt]sx?$": ["esbuild-jest", { format: "esm", sourcemap: true }],
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
||||
moduleNameMapper: {
|
||||
"^(\\.\\.?/.+)\\.jsx?$": "$1",
|
||||
},
|
||||
verbose: true,
|
||||
cache: false,
|
||||
}
|
||||
export default config
|
||||
17
package.json
17
package.json
@@ -17,9 +17,9 @@
|
||||
"lint": "eslint --ext js,ts,tsx .",
|
||||
"lint-fix": "pnpm lint -- --fix",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest",
|
||||
"test-watch": "vitest --watch",
|
||||
"coverage": "vitest --coverage",
|
||||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
||||
"test-watch": "pnpm test -- --watch",
|
||||
"coverage": "pnpm test -- --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"playground": "nodemon --exec esmo --ext ts,tsx ./playground/main.tsx"
|
||||
},
|
||||
@@ -42,11 +42,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@itsmapleleaf/configs": "^1.1.2",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/lodash-es": "^4.17.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"c8": "^7.10.0",
|
||||
"discord.js": "^13.4.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "latest",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
@@ -56,13 +60,14 @@
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unicorn": "^39.0.0",
|
||||
"esmo": "^0.13.0",
|
||||
"jest": "^27.4.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"tsup": "^5.11.9",
|
||||
"typescript": "^4.5.4",
|
||||
"vite": "^2.7.6",
|
||||
"vitest": "^0.0.108"
|
||||
"type-fest": "^2.8.0",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"esbuild": "latest"
|
||||
|
||||
1974
pnpm-lock.yaml
generated
1974
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
21
src/helpers/retry-with-timeout.ts
Normal file
21
src/helpers/retry-with-timeout.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
|
||||
const maxTime = 500
|
||||
const waitPeriod = 50
|
||||
|
||||
export async function retryWithTimeout<T>(
|
||||
callback: () => Promise<T> | T,
|
||||
): Promise<T> {
|
||||
const startTime = Date.now()
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await callback()
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > maxTime) {
|
||||
throw error
|
||||
}
|
||||
await setTimeout(waitPeriod)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { rejectAfter } from "./reject-after.js"
|
||||
import type { MaybePromise } from "./types.js"
|
||||
import { waitFor } from "./wait-for.js"
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function waitForWithTimeout(
|
||||
condition: () => MaybePromise<boolean>,
|
||||
timeout = 1000,
|
||||
errorMessage = `timed out after ${timeout}ms`,
|
||||
) {
|
||||
return Promise.race([waitFor(condition), rejectAfter(timeout, errorMessage)])
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import type { MaybePromise } from "./types.js"
|
||||
|
||||
export async function waitFor(condition: () => MaybePromise<boolean>) {
|
||||
while (!(await condition())) {
|
||||
await setTimeout(100)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import "dotenv/config"
|
||||
import { test } from "vitest"
|
||||
import { getEnvironmentValue } from "./helpers/get-environment-value.js"
|
||||
|
||||
const testBotToken = getEnvironmentValue("TEST_BOT_TOKEN")
|
||||
const testChannelId = getEnvironmentValue("TEST_CHANNEL_ID")
|
||||
|
||||
test.todo("reacord")
|
||||
107
src/test-adapter.ts
Normal file
107
src/test-adapter.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { nanoid } from "nanoid"
|
||||
import type { Adapter } from "./adapter"
|
||||
import { raise } from "./helpers/raise"
|
||||
import type {
|
||||
ButtonInteraction,
|
||||
CommandInteraction,
|
||||
ComponentInteraction,
|
||||
} from "./interaction"
|
||||
import type { Message, MessageButtonOptions, MessageOptions } from "./message"
|
||||
|
||||
export class TestAdapter implements Adapter<{}> {
|
||||
readonly messages: TestMessage[] = []
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private componentInteractionListener: (
|
||||
interaction: ComponentInteraction,
|
||||
) => void = () => {}
|
||||
|
||||
addComponentInteractionListener(
|
||||
listener: (interaction: ComponentInteraction) => void,
|
||||
): void {
|
||||
this.componentInteractionListener = listener
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
createCommandInteraction(
|
||||
interaction: CommandInteraction,
|
||||
): CommandInteraction {
|
||||
return interaction
|
||||
}
|
||||
|
||||
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}"`)
|
||||
}
|
||||
|
||||
private createButtonActions(
|
||||
button: MessageButtonOptions,
|
||||
message: TestMessage,
|
||||
) {
|
||||
return {
|
||||
click: () => {
|
||||
this.componentInteractionListener(
|
||||
new TestButtonInteraction(button.customId, message),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestMessage implements Message {
|
||||
constructor(public options: MessageOptions) {}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
}
|
||||
297
test/kitchen-sink.test.tsx
Normal file
297
test/kitchen-sink.test.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { nextTick } from "node:process"
|
||||
import { promisify } from "node:util"
|
||||
import * as React from "react"
|
||||
import { omit } from "../src/helpers/omit"
|
||||
import { Button, Embed, EmbedField, EmbedTitle, Reacord } from "../src/main"
|
||||
import { TestAdapter, TestCommandInteraction } from "../src/test-adapter"
|
||||
|
||||
const nextTickPromise = promisify(nextTick)
|
||||
|
||||
test("kitchen-sink", async () => {
|
||||
const adapter = new TestAdapter()
|
||||
const reacord = new Reacord({ adapter })
|
||||
|
||||
const reply = reacord.createCommandReply(new TestCommandInteraction(adapter))
|
||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 0",
|
||||
embeds: [],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "show embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("show embed").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 0",
|
||||
embeds: [{ title: "the counter" }],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "hide embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("clicc").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 1",
|
||||
embeds: [
|
||||
{
|
||||
title: "the counter",
|
||||
fields: [{ name: "is it even?", value: "no" }],
|
||||
},
|
||||
],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "hide embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("clicc").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 2",
|
||||
embeds: [
|
||||
{
|
||||
title: "the counter",
|
||||
fields: [{ name: "is it even?", value: "yes" }],
|
||||
},
|
||||
],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "hide embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("hide embed").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 2",
|
||||
embeds: [],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "show embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("clicc").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 3",
|
||||
embeds: [],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "show embed",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("deactivate").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 3",
|
||||
embeds: [],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "show embed",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
adapter.findButtonByLabel("clicc").click()
|
||||
await assertMessages(adapter, [
|
||||
{
|
||||
content: "count: 3",
|
||||
embeds: [],
|
||||
actionRows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
style: "primary",
|
||||
label: "clicc",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "secondary",
|
||||
label: "show embed",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
style: "danger",
|
||||
label: "deactivate",
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
function KitchenSinkCounter(props: { onDeactivate: () => void }) {
|
||||
const [count, setCount] = React.useState(0)
|
||||
const [embedVisible, setEmbedVisible] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
count: {count}
|
||||
{embedVisible && (
|
||||
<Embed>
|
||||
<EmbedTitle>the counter</EmbedTitle>
|
||||
{count > 0 && (
|
||||
<EmbedField name="is it even?">
|
||||
{count % 2 === 0 ? "yes" : "no"}
|
||||
</EmbedField>
|
||||
)}
|
||||
</Embed>
|
||||
)}
|
||||
{embedVisible && (
|
||||
<Button label="hide embed" onClick={() => setEmbedVisible(false)} />
|
||||
)}
|
||||
<Button
|
||||
style="primary"
|
||||
label="clicc"
|
||||
onClick={() => setCount(count + 1)}
|
||||
/>
|
||||
{!embedVisible && (
|
||||
<Button label="show embed" onClick={() => setEmbedVisible(true)} />
|
||||
)}
|
||||
<Button style="danger" label="deactivate" onClick={props.onDeactivate} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function extractMessageDataSample(adapter: TestAdapter) {
|
||||
return adapter.messages.map((message) => ({
|
||||
...message.options,
|
||||
actionRows: message.options.actionRows.map((row) =>
|
||||
row.map((component) => omit(component, ["customId"])),
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
async function assertMessages(
|
||||
adapter: TestAdapter,
|
||||
expected: ReturnType<typeof extractMessageDataSample>,
|
||||
) {
|
||||
await nextTickPromise()
|
||||
expect(extractMessageDataSample(adapter)).toEqual(expected)
|
||||
}
|
||||
Reference in New Issue
Block a user