Create list effects command #412

Merged
Vylpes merged 15 commits from feature/379-list-effects into develop 2024-12-07 22:32:20 +00:00
10 changed files with 644 additions and 0 deletions

View file

@ -0,0 +1,33 @@
import {ButtonInteraction} from "discord.js";
import {ButtonEvent} from "../type/buttonEvent";
import EffectHelper from "../helpers/EffectHelper";
export default class Effects extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const action = interaction.customId.split(" ")[1];
switch (action) {
case "list":
await this.List(interaction);
break;
}
}
private async List(interaction: ButtonInteraction) {
const pageOption = interaction.customId.split(" ")[2];
const page = Number(pageOption);
if (!page) {
await interaction.reply("Page option is not a valid number");
return;
}
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
await interaction.update({
embeds: [ result.embed ],
components: [ result.row ],
});
}
}

45
src/commands/effects.ts Normal file
View file

@ -0,0 +1,45 @@
import {CommandInteraction, SlashCommandBuilder} from "discord.js";
import {Command} from "../type/command";
import EffectHelper from "../helpers/EffectHelper";
export default class Effects extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("effects")
.setDescription("Effects")
.addSubcommand(x => x
.setName("list")
.setDescription("List all effects I have")
.addNumberOption(x => x
.setName("page")
.setDescription("The page number")
.setMinValue(1)));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case "list":
await this.List(interaction);
break;
}
}
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 ],
});
}
}

View file

@ -57,4 +57,18 @@ export default class UserEffect extends AppBaseEntity {
return single;
}
public static async FetchAllByUserIdPaginated(userId: string, page: number = 0, itemsPerPage: number = 10): Promise<[UserEffect[], number]> {
const repository = AppDataSource.getRepository(UserEffect);
const query = await repository.createQueryBuilder("effect")
.where("effect.UserId = :userId", { userId })
.where("effect.Unused > 0")
.orderBy("effect.Name", "ASC")
.skip(page * itemsPerPage)
.take(itemsPerPage)
.getManyAndCount();
return query;
}
}

View file

@ -1,4 +1,6 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
import UserEffect from "../database/entities/app/UserEffect";
import EmbedColours from "../constants/EmbedColours";
export default class EffectHelper {
public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
@ -46,4 +48,49 @@ export default class EffectHelper {
return true;
}
public static async GenerateEffectEmbed(userId: string, page: number): Promise<{
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
}> {
const itemsPerPage = 10;
const query = await UserEffect.FetchAllByUserIdPaginated(userId, page - 1, itemsPerPage);
const effects = query[0];
const count = query[1];
const totalPages = count > 0 ? Math.ceil(count / itemsPerPage) : 1;
let description = "*none*";
if (effects.length > 0) {
description = effects.map(x => `${x.Name} x${x.Unused}`).join("\n");
}
const embed = new EmbedBuilder()
.setTitle("Effects")
.setDescription(description)
.setColor(EmbedColours.Ok)
.setFooter({ text: `Page ${page} of ${totalPages}` });
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`effects list ${page - 1}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == 1),
new ButtonBuilder()
.setCustomId(`effects list ${page + 1}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == totalPages),
);
return {
embed,
row,
};
}
}

View file

@ -7,6 +7,7 @@ import AllBalance from "./commands/allbalance";
import Balance from "./commands/balance";
import Daily from "./commands/daily";
import Drop from "./commands/drop";
import Effects from "./commands/effects";
import Gdrivesync from "./commands/gdrivesync";
import Give from "./commands/give";
import Id from "./commands/id";
@ -25,6 +26,7 @@ import Droprarity from "./commands/stage/droprarity";
// Button Event Imports
import Claim from "./buttonEvents/Claim";
import EffectsButtonEvent from "./buttonEvents/Effects";
import InventoryButtonEvent from "./buttonEvents/Inventory";
import MultidropButtonEvent from "./buttonEvents/Multidrop";
import Reroll from "./buttonEvents/Reroll";
@ -44,6 +46,7 @@ export default class Registry {
CoreClient.RegisterCommand("balance", new Balance());
CoreClient.RegisterCommand("daily", new Daily());
CoreClient.RegisterCommand("drop", new Drop());
CoreClient.RegisterCommand("effects", new Effects());
CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
CoreClient.RegisterCommand("give", new Give());
CoreClient.RegisterCommand("id", new Id());
@ -63,6 +66,7 @@ export default class Registry {
public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent("claim", new Claim());
CoreClient.RegisterButtonEvent("effects", new EffectsButtonEvent());
CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
CoreClient.RegisterButtonEvent("reroll", new Reroll());

View file

@ -0,0 +1,127 @@
import {ButtonInteraction} from "discord.js";
import Effects from "../../src/buttonEvents/Effects";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("execute", () => {
describe("GIVEN action in custom id is list", () => {
const interaction = {
customId: "effects list",
} as unknown as ButtonInteraction;
let listSpy: jest.SpyInstance;
beforeAll(async () => {
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT list function to be called", () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(listSpy).toHaveBeenCalledWith(interaction);
});
});
});
describe("List", () => {
let interaction: ButtonInteraction;
const embed = {
name: "Embed",
};
const row = {
name: "Row",
};
beforeEach(() => {
interaction = {
customId: "effects list",
user: {
id: "userId",
},
update: jest.fn(),
reply: jest.fn(),
} as unknown as ButtonInteraction;
});
describe("GIVEN page is a valid number", () => {
beforeEach(async () => {
interaction.customId += " 1";
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to be called", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
test("EXPECT interaction to be updated", () => {
expect(interaction.update).toHaveBeenCalledTimes(1);
expect(interaction.update).toHaveBeenCalledWith({
embeds: [ embed ],
components: [ row ],
});
});
});
describe("GIVEN page in custom id is not supplied", () => {
beforeEach(async () => {
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT interaction to be replied with error", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
});
test("EXPECT interaction to not be updated", () => {
expect(interaction.update).not.toHaveBeenCalled();
});
});
describe("GIVEN page in custom id is not a number", () => {
beforeEach(async () => {
interaction.customId += " test";
EffectHelper.GenerateEffectEmbed = jest.fn()
.mockResolvedValue({
embed,
row,
});
const effects = new Effects();
await effects.execute(interaction);
});
test("EXPECT interaction to be replied with error", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number");
});
test("EXPECT interaction to not be updated", () => {
expect(interaction.update).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`constructor 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,
},
],
"type": 1,
}
`;

View file

@ -0,0 +1,164 @@
import {ChatInputCommandInteraction} from "discord.js";
import Effects from "../../src/commands/effects";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("constructor", () => {
let effects: Effects;
beforeEach(() => {
effects = new Effects();
});
test("EXPECT CommandBuilder to be defined", () => {
expect(effects.CommandBuilder).toMatchSnapshot();
});
});
describe("execute", () => {
describe("GIVEN interaction is not a chat input command", () => {
let interaction: ChatInputCommandInteraction;
let listSpy: jest.SpyInstance;
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(false),
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT isChatInputCommand to have been called", () => {
expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1);
});
test("EXPECT nothing to happen", () => {
expect(listSpy).not.toHaveBeenCalled();
});
});
describe("GIVEN subcommand is list", () => {
let interaction: ChatInputCommandInteraction;
let listSpy: jest.SpyInstance;
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("list"),
},
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
listSpy = jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
await effects.execute(interaction);
});
test("EXPECT subcommand function to be called", () => {
expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1);
});
test("EXPECT list function to be called", () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(listSpy).toHaveBeenCalledWith(interaction);
});
});
});
describe("List", () => {
const effects: Effects = new Effects();
let interaction: ChatInputCommandInteraction;
const embed = {
name: "embed",
};
const row = {
name: "row",
};
beforeEach(async () => {
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("list"),
},
reply: jest.fn(),
user: {
id: "userId",
},
} as unknown as ChatInputCommandInteraction;
const effects = new Effects();
EffectHelper.GenerateEffectEmbed = jest.fn().mockReturnValue({
embed,
row,
});
jest.spyOn(effects as unknown as {"List": () => object}, "List")
.mockImplementation();
});
describe("GIVEN page option is supplied", () => {
describe("AND page is a valid number", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce({
value: "2",
});
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 2);
});
test("EXPECT interaction to have been replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith({
embeds: [ embed ],
components: [ row ],
});
});
});
describe("AND page is not a valid number", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce({
value: "test",
});
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
});
});
describe("GIVEN page option is not supplied", () => {
beforeEach(async () => {
interaction.options.get = jest.fn().mockReturnValueOnce(undefined);
await effects.execute(interaction);
});
test("EXPECT EffectHelper.GenerateEffectEmbed to have been called with page of 1", () => {
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledTimes(1);
expect(EffectHelper.GenerateEffectEmbed).toHaveBeenCalledWith("userId", 1);
});
});
});

View file

@ -1,3 +1,4 @@
import {ActionRowBuilder, ButtonBuilder, EmbedBuilder} from "discord.js";
import UserEffect from "../../src/database/entities/app/UserEffect";
import EffectHelper from "../../src/helpers/EffectHelper";
@ -279,3 +280,101 @@ describe("HasEffect", () => {
});
});
});
describe("GenerateEffectEmbed", () => {
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[],
0,
]);
await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT UserEffect.FetchAllByUserIdPaginated to be called", () => {
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledTimes(1);
expect(UserEffect.FetchAllByUserIdPaginated).toHaveBeenCalledWith("userId", 0, 10);
});
describe("GIVEN there are no effects returned", () => {
let result: {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
};
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[],
0,
]);
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT result returned", () => {
expect(result).toMatchSnapshot();
});
});
describe("GIVEN there are effects returned", () => {
let result: {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
};
beforeEach(async () => {
UserEffect.FetchAllByUserIdPaginated = jest.fn()
.mockResolvedValue([
[
{
Name: "name",
Unused: 1,
},
],
1,
]);
result = await EffectHelper.GenerateEffectEmbed("userId", 1);
});
test("EXPECT result returned", () => {
expect(result).toMatchSnapshot();
});
describe("AND it is the first page", () => {
beforeEach(async () => {
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
});
test("EXPECT Previous button to be disabled", () => {
const button = result.row.components[0].data as unknown as {
label: string,
disabled: boolean
};
expect(button).toBeDefined();
expect(button.label).toBe("Previous");
expect(button.disabled).toBe(true);
});
});
describe("AND it is the last page", () => {
beforeEach(async () => {
result = await EffectHelper.GenerateEffectEmbed("userId", 1)
});
test("EXPECT Next button to be disabled", () => {
const button = result.row.components[1].data as unknown as {
label: string,
disabled: boolean
};
expect(button).toBeDefined();
expect(button.label).toBe("Next");
expect(button.disabled).toBe(true);
});
});
});
});

View file

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GenerateEffectEmbed GIVEN there are effects returned EXPECT result returned 1`] = `
{
"embed": {
"color": 3166394,
"description": "name x1",
"footer": {
"icon_url": undefined,
"text": "Page 1 of 1",
},
"title": "Effects",
},
"row": {
"components": [
{
"custom_id": "effects list 0",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "effects list 2",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
}
`;
exports[`GenerateEffectEmbed GIVEN there are no effects returned EXPECT result returned 1`] = `
{
"embed": {
"color": 3166394,
"description": "*none*",
"footer": {
"icon_url": undefined,
"text": "Page 1 of 1",
},
"title": "Effects",
},
"row": {
"components": [
{
"custom_id": "effects list 0",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "effects list 2",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
}
`;