From ace834b274def854adf8395967757bc24dc2de94 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:17:50 +0000 Subject: [PATCH] fix: Adjust label predicates and fix buttons emoji/label behaviour (#11317) * fix: buttons with custom id * fix: button labels * fix: also add refinement to link buttons --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../__tests__/components/button.test.ts | 28 +++++++++++- .../__tests__/components/selectMenu.test.ts | 9 ++-- .../builders/src/components/Assertions.ts | 44 +++++++++++-------- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index fc0c86c2a..85319bec8 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -5,7 +5,13 @@ import { type APIButtonComponentWithURL, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js'; +import { + PrimaryButtonBuilder, + PremiumButtonBuilder, + LinkButtonBuilder, + DangerButtonBuilder, + SecondaryButtonBuilder, +} from '../../src/index.js'; const longStr = 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; @@ -25,6 +31,26 @@ describe('Button Components', () => { button.toJSON(); }).not.toThrowError(); + expect(() => { + const button = new SecondaryButtonBuilder().setCustomId('custom').setLabel('a'.repeat(80)); + button.toJSON(); + }).not.toThrowError(); + + expect(() => { + const button = new DangerButtonBuilder().setCustomId('custom').setEmoji({ name: 'ok' }); + button.toJSON(); + }).not.toThrowError(); + + expect(() => { + const button = new LinkButtonBuilder().setURL('https://discord.js.org').setLabel('a'.repeat(80)); + button.toJSON(); + }).not.toThrowError(); + + expect(() => { + const button = new LinkButtonBuilder().setURL('https://discord.js.org').setEmoji({ name: 'ok' }); + button.toJSON(); + }).not.toThrowError(); + expect(() => { const button = new PremiumButtonBuilder().setSKUId('123456789012345678'); button.toJSON(); diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 544bf79a5..34bd60289 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -7,6 +7,9 @@ const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' }); const selectMenuOption = () => new StringSelectMenuOptionBuilder(); const longStr = 'a'.repeat(256); +const selectMenuOptionLabelAboveLimit = 'a'.repeat(101); +const selectMenuOptionValueAboveLimit = 'a'.repeat(101); +const selectMenuOptionDescriptionAboveLimit = 'a'.repeat(101); const selectMenuOptionData: APISelectMenuOption = { label: 'test', @@ -196,13 +199,13 @@ describe('Select Menu Components', () => { expect(() => { selectMenuOption() - .setLabel(longStr) - .setValue(longStr) + .setLabel(selectMenuOptionLabelAboveLimit) + .setValue(selectMenuOptionValueAboveLimit) // @ts-expect-error: Invalid default value .setDefault(-1) // @ts-expect-error: Invalid emoji .setEmoji({ name: 1 }) - .setDescription(longStr) + .setDescription(selectMenuOptionDescriptionAboveLimit) .toJSON(); }).toThrowError(); }); diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 864a27d56..36f18202e 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -2,8 +2,6 @@ import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } f import { z } from 'zod'; import { idPredicate, customIdPredicate, snowflakePredicate } from '../Assertions.js'; -const labelPredicate = z.string().min(1).max(80); - export const emojiPredicate = z .strictObject({ id: snowflakePredicate.optional(), @@ -19,23 +17,33 @@ const buttonPredicateBase = z.strictObject({ disabled: z.boolean().optional(), }); -const buttonCustomIdPredicateBase = buttonPredicateBase.extend({ - custom_id: customIdPredicate, - emoji: emojiPredicate.optional(), - label: labelPredicate, -}); +const buttonLabelPredicate = z.string().min(1).max(80); -const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }); -const buttonSecondaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Secondary) }); -const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }); -const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }); +const buttonCustomIdPredicateBase = buttonPredicateBase + .extend({ + custom_id: customIdPredicate, + emoji: emojiPredicate.optional(), + label: buttonLabelPredicate.optional(), + }) + .refine((data) => data.emoji !== undefined || data.label !== undefined, { + message: 'Buttons with a custom id must have either an emoji or a label.', + }); -const buttonLinkPredicate = buttonPredicateBase.extend({ - style: z.literal(ButtonStyle.Link), - url: z.url({ protocol: /^(?:https?|discord)$/ }).max(512), - emoji: emojiPredicate.optional(), - label: labelPredicate, -}); +const buttonPrimaryPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Primary) }); +const buttonSecondaryPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Secondary) }); +const buttonSuccessPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Success) }); +const buttonDangerPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Danger) }); + +const buttonLinkPredicate = buttonPredicateBase + .extend({ + style: z.literal(ButtonStyle.Link), + url: z.url({ protocol: /^(?:https?|discord)$/ }).max(512), + emoji: emojiPredicate.optional(), + label: buttonLabelPredicate.optional(), + }) + .refine((data) => data.emoji !== undefined || data.label !== undefined, { + message: 'Link buttons must have either an emoji or a label.', + }); const buttonPremiumPredicate = buttonPredicateBase.extend({ style: z.literal(ButtonStyle.Premium), @@ -92,7 +100,7 @@ export const selectMenuRolePredicate = selectMenuBasePredicate.extend({ }); export const selectMenuStringOptionPredicate = z.object({ - label: labelPredicate, + label: z.string().min(1).max(100), value: z.string().min(1).max(100), description: z.string().min(1).max(100).optional(), emoji: emojiPredicate.optional(),