428 lines
13 KiB
TypeScript
428 lines
13 KiB
TypeScript
|
|
import { describe, it, expect, vi } from 'vitest'
|
||
|
|
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from '@discordeno/types'
|
||
|
|
import { createCommandHandler, ExtractCommands } from './command_parser'
|
||
|
|
import type { InteractionData } from '..'
|
||
|
|
|
||
|
|
// Test command definitions
|
||
|
|
const TEST_COMMANDS = [
|
||
|
|
{
|
||
|
|
name: 'simple',
|
||
|
|
description: 'A simple command',
|
||
|
|
type: ApplicationCommandTypes.ChatInput,
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'message',
|
||
|
|
description: 'A message to echo',
|
||
|
|
type: ApplicationCommandOptionTypes.String,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'count',
|
||
|
|
description: 'Number of times',
|
||
|
|
type: ApplicationCommandOptionTypes.Integer,
|
||
|
|
required: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'complex',
|
||
|
|
description: 'A command with subcommands',
|
||
|
|
type: ApplicationCommandTypes.ChatInput,
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'list',
|
||
|
|
description: 'List items',
|
||
|
|
type: ApplicationCommandOptionTypes.SubCommand,
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'filter',
|
||
|
|
description: 'Filter string',
|
||
|
|
type: ApplicationCommandOptionTypes.String,
|
||
|
|
required: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'create',
|
||
|
|
description: 'Create item',
|
||
|
|
type: ApplicationCommandOptionTypes.SubCommand,
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'name',
|
||
|
|
description: 'Item name',
|
||
|
|
type: ApplicationCommandOptionTypes.String,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'enabled',
|
||
|
|
description: 'Is enabled',
|
||
|
|
type: ApplicationCommandOptionTypes.Boolean,
|
||
|
|
required: false,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
] as const satisfies CreateApplicationCommand[]
|
||
|
|
|
||
|
|
describe('createCommandHandler', () => {
|
||
|
|
it('should handle a simple command with required string argument', async () => {
|
||
|
|
const simpleHandler = vi.fn()
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: simpleHandler,
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'simple',
|
||
|
|
options: [
|
||
|
|
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello world' },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello world' })
|
||
|
|
expect(notFoundHandler).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle a simple command with optional arguments', async () => {
|
||
|
|
const simpleHandler = vi.fn()
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: simpleHandler,
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'simple',
|
||
|
|
options: [
|
||
|
|
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' },
|
||
|
|
{ name: 'count', type: ApplicationCommandOptionTypes.Integer, value: 5 },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello', count: 5 })
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle subcommands correctly', async () => {
|
||
|
|
const listHandler = vi.fn()
|
||
|
|
const createHandler = vi.fn()
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: vi.fn(),
|
||
|
|
complex: {
|
||
|
|
list: listHandler,
|
||
|
|
create: createHandler,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'complex',
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'create',
|
||
|
|
type: ApplicationCommandOptionTypes.SubCommand,
|
||
|
|
options: [
|
||
|
|
{ name: 'name', type: ApplicationCommandOptionTypes.String, value: 'Test Item' },
|
||
|
|
{ name: 'enabled', type: ApplicationCommandOptionTypes.Boolean, value: true },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(createHandler).toHaveBeenCalledWith({ name: 'Test Item', enabled: true })
|
||
|
|
expect(listHandler).not.toHaveBeenCalled()
|
||
|
|
expect(notFoundHandler).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should call notFoundHandler for unknown commands', async () => {
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: vi.fn(),
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'unknown',
|
||
|
|
options: [],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should call notFoundHandler for unknown subcommands', async () => {
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: vi.fn(),
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'complex',
|
||
|
|
options: [
|
||
|
|
{
|
||
|
|
name: 'unknown',
|
||
|
|
type: ApplicationCommandOptionTypes.SubCommand,
|
||
|
|
options: [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle missing interaction data', async () => {
|
||
|
|
const notFoundHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: vi.fn(),
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler,
|
||
|
|
})
|
||
|
|
|
||
|
|
await handler(null as any)
|
||
|
|
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||
|
|
|
||
|
|
await handler({} as any)
|
||
|
|
expect(notFoundHandler).toHaveBeenCalledTimes(2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle commands without options', async () => {
|
||
|
|
const simpleHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: simpleHandler,
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler: vi.fn(),
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'simple',
|
||
|
|
// No options provided
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(simpleHandler).toHaveBeenCalledWith({})
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should ignore subcommand options when parsing top-level args', async () => {
|
||
|
|
const simpleHandler = vi.fn()
|
||
|
|
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
handler: {
|
||
|
|
simple: simpleHandler,
|
||
|
|
complex: {
|
||
|
|
list: vi.fn(),
|
||
|
|
create: vi.fn(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
notFoundHandler: vi.fn(),
|
||
|
|
})
|
||
|
|
|
||
|
|
const interactionData: InteractionData = {
|
||
|
|
name: 'simple',
|
||
|
|
options: [
|
||
|
|
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' },
|
||
|
|
// This should be ignored in parseOptions
|
||
|
|
{ name: 'subcommand', type: ApplicationCommandOptionTypes.SubCommand, options: [] },
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
await handler(interactionData)
|
||
|
|
|
||
|
|
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello' })
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('ExtractCommands type utility', () => {
|
||
|
|
it('should correctly extract command types', () => {
|
||
|
|
// Type tests - these compile-time checks ensure the types work correctly
|
||
|
|
type TestHandlers = ExtractCommands<typeof TEST_COMMANDS>
|
||
|
|
|
||
|
|
// Test that the type structure is correct
|
||
|
|
const handlers: TestHandlers = {
|
||
|
|
simple: async (args) => {
|
||
|
|
// TypeScript should know that args has message as required string and count as optional number
|
||
|
|
const message: string = args.message
|
||
|
|
const count: number | undefined = args.count
|
||
|
|
expect(message).toBeDefined()
|
||
|
|
expect(count).toBeDefined()
|
||
|
|
},
|
||
|
|
complex: {
|
||
|
|
list: async (args) => {
|
||
|
|
// TypeScript should know that args has filter as optional string
|
||
|
|
const filter: string | undefined = args.filter
|
||
|
|
expect(filter).toBeDefined()
|
||
|
|
},
|
||
|
|
create: async (args) => {
|
||
|
|
// TypeScript should know that args has name as required string and enabled as optional boolean
|
||
|
|
const name: string = args.name
|
||
|
|
const enabled: boolean | undefined = args.enabled
|
||
|
|
expect(name).toBeDefined()
|
||
|
|
expect(enabled).toBeDefined()
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// This is just to satisfy TypeScript - the real test is that the above compiles
|
||
|
|
expect(handlers).toBeDefined()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should infer types correctly without manual typing', () => {
|
||
|
|
// This test verifies that args are inferred correctly without manual type annotations
|
||
|
|
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||
|
|
notFoundHandler: async () => {},
|
||
|
|
handler: {
|
||
|
|
simple: async (args) => {
|
||
|
|
// args should be automatically typed
|
||
|
|
// This would error if args.message wasn't a string
|
||
|
|
const upper = args.message.toUpperCase()
|
||
|
|
// This would error if args.count wasn't number | undefined
|
||
|
|
const doubled = args.count ? args.count * 2 : 0
|
||
|
|
expect(upper).toBeDefined()
|
||
|
|
expect(doubled).toBeDefined()
|
||
|
|
},
|
||
|
|
complex: {
|
||
|
|
list: async (args) => {
|
||
|
|
// This would error if args.filter wasn't string | undefined
|
||
|
|
const filtered = args.filter?.toLowerCase()
|
||
|
|
expect(filtered !== undefined).toBeDefined()
|
||
|
|
},
|
||
|
|
create: async (args) => {
|
||
|
|
// This would error if types weren't correct
|
||
|
|
const nameLength = args.name.length
|
||
|
|
const isEnabled = args.enabled === true
|
||
|
|
expect(nameLength).toBeDefined()
|
||
|
|
expect(isEnabled).toBeDefined()
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(handler).toBeDefined()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle commands with no options', () => {
|
||
|
|
const EMPTY_COMMANDS = [
|
||
|
|
{
|
||
|
|
name: 'ping',
|
||
|
|
description: 'Ping command',
|
||
|
|
type: ApplicationCommandTypes.ChatInput,
|
||
|
|
},
|
||
|
|
] as const satisfies CreateApplicationCommand[]
|
||
|
|
|
||
|
|
type EmptyHandlers = ExtractCommands<typeof EMPTY_COMMANDS>
|
||
|
|
|
||
|
|
const handlers: EmptyHandlers = {
|
||
|
|
ping: async (args) => {
|
||
|
|
// args should be an empty object
|
||
|
|
expect(args).toEqual({})
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(handlers).toBeDefined()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle different option types', () => {
|
||
|
|
const TYPE_TEST_COMMANDS = [
|
||
|
|
{
|
||
|
|
name: 'types',
|
||
|
|
description: 'Test different types',
|
||
|
|
type: ApplicationCommandTypes.ChatInput,
|
||
|
|
options: [
|
||
|
|
{ name: 'str', description: 'String param', type: ApplicationCommandOptionTypes.String, required: true },
|
||
|
|
{ name: 'int', description: 'Integer param', type: ApplicationCommandOptionTypes.Integer, required: true },
|
||
|
|
{ name: 'bool', description: 'Boolean param', type: ApplicationCommandOptionTypes.Boolean, required: true },
|
||
|
|
{ name: 'user', description: 'User param', type: ApplicationCommandOptionTypes.User, required: true },
|
||
|
|
{ name: 'channel', description: 'Channel param', type: ApplicationCommandOptionTypes.Channel, required: true },
|
||
|
|
{ name: 'role', description: 'Role param', type: ApplicationCommandOptionTypes.Role, required: true },
|
||
|
|
{ name: 'num', description: 'Number param', type: ApplicationCommandOptionTypes.Number, required: true },
|
||
|
|
{ name: 'mention', description: 'Mentionable param', type: ApplicationCommandOptionTypes.Mentionable, required: true },
|
||
|
|
{ name: 'attach', description: 'Attachment param', type: ApplicationCommandOptionTypes.Attachment, required: true },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
] as const satisfies CreateApplicationCommand[]
|
||
|
|
|
||
|
|
type TypeHandlers = ExtractCommands<typeof TYPE_TEST_COMMANDS>
|
||
|
|
|
||
|
|
const handlers: TypeHandlers = {
|
||
|
|
types: async (args) => {
|
||
|
|
// Test that all types are correctly mapped
|
||
|
|
const str: string = args.str
|
||
|
|
const int: number = args.int
|
||
|
|
const bool: boolean = args.bool
|
||
|
|
const user: string = args.user
|
||
|
|
const channel: string = args.channel
|
||
|
|
const role: string = args.role
|
||
|
|
const num: number = args.num
|
||
|
|
const mention: string = args.mention
|
||
|
|
const attach: string = args.attach
|
||
|
|
|
||
|
|
expect(str).toBeDefined()
|
||
|
|
expect(int).toBeDefined()
|
||
|
|
expect(bool).toBeDefined()
|
||
|
|
expect(user).toBeDefined()
|
||
|
|
expect(channel).toBeDefined()
|
||
|
|
expect(role).toBeDefined()
|
||
|
|
expect(num).toBeDefined()
|
||
|
|
expect(mention).toBeDefined()
|
||
|
|
expect(attach).toBeDefined()
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(handlers).toBeDefined()
|
||
|
|
})
|
||
|
|
})
|