kitchen sink test

This commit is contained in:
MapleLeaf
2021-12-26 12:00:22 -06:00
parent d5815cbec6
commit d10618e3c1
9 changed files with 2303 additions and 154 deletions

13
jest.config.js Normal file
View 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

View File

@@ -17,9 +17,9 @@
"lint": "eslint --ext js,ts,tsx .", "lint": "eslint --ext js,ts,tsx .",
"lint-fix": "pnpm lint -- --fix", "lint-fix": "pnpm lint -- --fix",
"format": "prettier --write .", "format": "prettier --write .",
"test": "vitest", "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"test-watch": "vitest --watch", "test-watch": "pnpm test -- --watch",
"coverage": "vitest --coverage", "coverage": "pnpm test -- --coverage",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"playground": "nodemon --exec esmo --ext ts,tsx ./playground/main.tsx" "playground": "nodemon --exec esmo --ext ts,tsx ./playground/main.tsx"
}, },
@@ -42,11 +42,15 @@
}, },
"devDependencies": { "devDependencies": {
"@itsmapleleaf/configs": "^1.1.2", "@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/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0", "@typescript-eslint/parser": "^5.8.0",
"c8": "^7.10.0", "c8": "^7.10.0",
"discord.js": "^13.4.0", "discord.js": "^13.4.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "latest",
"esbuild-jest": "^0.5.0",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0", "eslint-import-resolver-typescript": "^2.5.0",
@@ -56,13 +60,14 @@
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^39.0.0", "eslint-plugin-unicorn": "^39.0.0",
"esmo": "^0.13.0", "esmo": "^0.13.0",
"jest": "^27.4.5",
"lodash-es": "^4.17.21",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"react": "^17.0.2", "react": "^17.0.2",
"tsup": "^5.11.9", "tsup": "^5.11.9",
"typescript": "^4.5.4", "type-fest": "^2.8.0",
"vite": "^2.7.6", "typescript": "^4.5.4"
"vitest": "^0.0.108"
}, },
"resolutions": { "resolutions": {
"esbuild": "latest" "esbuild": "latest"

1974
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

View File

@@ -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)])
}

View File

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

View File

@@ -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
View 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
View 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)
}