diff --git a/src/commands/effects.ts b/src/commands/effects.ts index 98727b9..cd6d1d4 100644 --- a/src/commands/effects.ts +++ b/src/commands/effects.ts @@ -1,10 +1,10 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import { Command } from "../type/command"; -import EffectHelper from "../helpers/EffectHelper"; -import { EffectDetails } from "../constants/EffectDetails"; -import TimeLengthInput from "../helpers/TimeLengthInput"; -import EmbedColours from "../constants/EmbedColours"; +import { EffectChoices } from "../constants/EffectDetails"; import AppLogger from "../client/appLogger"; +import List from "./effects/List"; +import Use from "./effects/Use"; +import Buy from "./effects/Buy"; export default class Effects extends Command { constructor() { @@ -27,9 +27,19 @@ export default class Effects extends Command { .setName("id") .setDescription("The effect id to use") .setRequired(true) - .setChoices([ - { name: "Unclaimed Chance Up", value: "unclaimed" }, - ]))); + .setChoices(EffectChoices))) + .addSubcommand(x => x + .setName("buy") + .setDescription("Buy more effects") + .addStringOption(y => y + .setName("id") + .setDescription("The effect id to buy") + .setRequired(true) + .setChoices(EffectChoices)) + .addNumberOption(y => y + .setName("quantity") + .setDescription("The amount to buy") + .setMinValue(1))); } public override async execute(interaction: CommandInteraction) { @@ -39,80 +49,16 @@ export default class Effects extends Command { switch (subcommand) { case "list": - await this.List(interaction); + await List(interaction); break; case "use": - await this.Use(interaction); + await Use(interaction); break; + case "buy": + await Buy(interaction); + break; + default: + AppLogger.LogError("Commands/Effects", `Invalid subcommand: ${subcommand}`); } } - - private async List(interaction: CommandInteraction) { - const pageOption = interaction.options.get("page"); - - const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1; - - const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page); - - await interaction.reply({ - embeds: [ result.embed ], - components: [ result.row ], - }); - } - - private async Use(interaction: CommandInteraction) { - const id = interaction.options.get("id", true).value!.toString(); - - const effectDetail = EffectDetails.get(id); - - if (!effectDetail) { - AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`); - - await interaction.reply("Unable to find effect!"); - return; - } - - const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id); - - if (!canUseEffect) { - await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown"); - return; - } - - const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration); - - const embed = new EmbedBuilder() - .setTitle("Effect Confirmation") - .setDescription("Would you like to use this effect?") - .setColor(EmbedColours.Ok) - .addFields([ - { - name: "Effect", - value: effectDetail.friendlyName, - inline: true, - }, - { - name: "Length", - value: timeLengthInput.GetLengthShort(), - inline: true, - }, - ]); - - const row = new ActionRowBuilder() - .addComponents([ - new ButtonBuilder() - .setLabel("Confirm") - .setCustomId(`effects use confirm ${effectDetail.id}`) - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setLabel("Cancel") - .setCustomId(`effects use cancel ${effectDetail.id}`) - .setStyle(ButtonStyle.Danger), - ]); - - await interaction.reply({ - embeds: [ embed ], - components: [ row ], - }); - } } diff --git a/src/commands/effects/Buy.ts b/src/commands/effects/Buy.ts new file mode 100644 index 0000000..e26b585 --- /dev/null +++ b/src/commands/effects/Buy.ts @@ -0,0 +1,4 @@ +import { CommandInteraction } from "discord.js"; + +export default async function Buy(interaction: CommandInteraction) { +} \ No newline at end of file diff --git a/src/commands/effects/List.ts b/src/commands/effects/List.ts new file mode 100644 index 0000000..cc91321 --- /dev/null +++ b/src/commands/effects/List.ts @@ -0,0 +1,15 @@ +import { CommandInteraction } from "discord.js"; +import EffectHelper from "../../helpers/EffectHelper"; + +export default async function List(interaction: CommandInteraction) { + const pageOption = interaction.options.get("page"); + + const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1; + + const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page); + + await interaction.reply({ + embeds: [ result.embed ], + components: [ result.row ], + }); +} \ No newline at end of file diff --git a/src/commands/effects/Use.ts b/src/commands/effects/Use.ts new file mode 100644 index 0000000..9f72ae0 --- /dev/null +++ b/src/commands/effects/Use.ts @@ -0,0 +1,62 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder } from "discord.js"; +import { EffectDetails } from "../../constants/EffectDetails"; +import AppLogger from "../../client/appLogger"; +import EffectHelper from "../../helpers/EffectHelper"; +import TimeLengthInput from "../../helpers/TimeLengthInput"; +import EmbedColours from "../../constants/EmbedColours"; + +export default async function Use(interaction: CommandInteraction) { + const id = interaction.options.get("id", true).value!.toString(); + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`); + + await interaction.reply("Unable to find effect!"); + return; + } + + const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id); + + if (!canUseEffect) { + await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown"); + return; + } + + const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration); + + const embed = new EmbedBuilder() + .setTitle("Effect Confirmation") + .setDescription("Would you like to use this effect?") + .setColor(EmbedColours.Ok) + .addFields([ + { + name: "Effect", + value: effectDetail.friendlyName, + inline: true, + }, + { + name: "Length", + value: timeLengthInput.GetLengthShort(), + inline: true, + }, + ]); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setLabel("Confirm") + .setCustomId(`effects use confirm ${effectDetail.id}`) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setLabel("Cancel") + .setCustomId(`effects use cancel ${effectDetail.id}`) + .setStyle(ButtonStyle.Danger), + ]); + + await interaction.reply({ + embeds: [ embed ], + components: [ row ], + }); +} \ No newline at end of file diff --git a/src/constants/EffectDetails.ts b/src/constants/EffectDetails.ts index 4b84dad..9d1f2b6 100644 --- a/src/constants/EffectDetails.ts +++ b/src/constants/EffectDetails.ts @@ -17,3 +17,7 @@ class EffectDetail { export const EffectDetails = new Map([ [ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ], ]); + +export const EffectChoices = [ + { name: "Unclaimed Chance Up", value: "unclaimed" }, +]; diff --git a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts new file mode 100644 index 0000000..26818b3 --- /dev/null +++ b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts @@ -0,0 +1,12 @@ +import { CommandInteraction } from "../../__types__/discord.js"; + +export default function GenerateCommandInteractionMock(options?: { + subcommand?: string, +}): CommandInteraction { + return { + isChatInputCommand: jest.fn().mockReturnValue(true), + options: { + getSubcommand: jest.fn().mockReturnValue(options?.subcommand), + }, + }; +} \ No newline at end of file diff --git a/tests/__types__/discord.js.ts b/tests/__types__/discord.js.ts index 6506b1d..e1f5961 100644 --- a/tests/__types__/discord.js.ts +++ b/tests/__types__/discord.js.ts @@ -14,4 +14,11 @@ export type ButtonInteraction = { id: string, } | null, customId: string, +} + +export type CommandInteraction = { + isChatInputCommand: jest.Func, + options: { + getSubcommand: jest.Func, + }, } \ No newline at end of file diff --git a/tests/commands/__snapshots__/effects.test.ts.snap b/tests/commands/__snapshots__/effects.test.ts.snap new file mode 100644 index 0000000..474b505 --- /dev/null +++ b/tests/commands/__snapshots__/effects.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EXPECT CommandBuilder to be defined 1`] = ` +{ + "contexts": undefined, + "default_member_permissions": undefined, + "default_permission": undefined, + "description": "Effects", + "description_localizations": undefined, + "dm_permission": undefined, + "integration_types": undefined, + "name": "effects", + "name_localizations": undefined, + "nsfw": undefined, + "options": [ + { + "description": "List all effects I have", + "description_localizations": undefined, + "name": "list", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": undefined, + "description": "The page number", + "description_localizations": undefined, + "max_value": undefined, + "min_value": 1, + "name": "page", + "name_localizations": undefined, + "required": false, + "type": 10, + }, + ], + "type": 1, + }, + { + "description": "Use an effect in your inventory", + "description_localizations": undefined, + "name": "use", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": [ + { + "name": "Unclaimed Chance Up", + "name_localizations": undefined, + "value": "unclaimed", + }, + ], + "description": "The effect id to use", + "description_localizations": undefined, + "max_length": undefined, + "min_length": undefined, + "name": "id", + "name_localizations": undefined, + "required": true, + "type": 3, + }, + ], + "type": 1, + }, + { + "description": "Buy more effects", + "description_localizations": undefined, + "name": "buy", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": [ + { + "name": "Unclaimed Chance Up", + "name_localizations": undefined, + "value": "unclaimed", + }, + ], + "description": "The effect id to buy", + "description_localizations": undefined, + "max_length": undefined, + "min_length": undefined, + "name": "id", + "name_localizations": undefined, + "required": true, + "type": 3, + }, + { + "autocomplete": undefined, + "choices": undefined, + "description": "The amount to buy", + "description_localizations": undefined, + "max_value": undefined, + "min_value": 1, + "name": "quantity", + "name_localizations": undefined, + "required": false, + "type": 10, + }, + ], + "type": 1, + }, + ], + "type": 1, +} +`; diff --git a/tests/commands/effects.test.ts b/tests/commands/effects.test.ts new file mode 100644 index 0000000..33f612d --- /dev/null +++ b/tests/commands/effects.test.ts @@ -0,0 +1,105 @@ +import Effects from "../../src/commands/effects"; +import List from "../../src/commands/effects/List"; +import Use from "../../src/commands/effects/Use"; +import Buy from "../../src/commands/effects/Buy"; +import AppLogger from "../../src/client/appLogger"; +import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock"; +import { CommandInteraction } from "discord.js"; + +jest.mock("../../src/commands/effects/List"); +jest.mock("../../src/commands/effects/Use"); +jest.mock("../../src/commands/effects/Buy"); +jest.mock("../../src/client/appLogger"); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +test("EXPECT CommandBuilder to be defined", async () => { + // Act + const effects = new Effects(); + + // Assert + expect(effects.CommandBuilder).toMatchSnapshot(); +}); + +describe("execute", () => { + test("GIVEN interaction subcommand is list, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "list", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(List).toHaveBeenCalledTimes(1); + expect(List).toHaveBeenCalledWith(interaction); + + expect(Use).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is use, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "use", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(Use).toHaveBeenCalledTimes(1); + expect(Use).toHaveBeenCalledWith(interaction); + + expect(List).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is buy, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "buy", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(Buy).toHaveBeenCalledTimes(1); + expect(Buy).toHaveBeenCalledWith(interaction); + + expect(List).not.toHaveBeenCalled(); + expect(Use).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is invalid, EXPECT error logged", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "invalid", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Commands/Effects", "Invalid subcommand: invalid"); + + expect(List).not.toHaveBeenCalled(); + expect(Use).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file