diff --git a/packages/bot/src/functions/general/commands/vaga.spec.ts b/packages/bot/src/functions/general/commands/vaga.spec.ts index cc5261018..c6dcfa86a 100644 --- a/packages/bot/src/functions/general/commands/vaga.spec.ts +++ b/packages/bot/src/functions/general/commands/vaga.spec.ts @@ -78,8 +78,20 @@ function makeInteraction( : Promise.reject(new Error('timeout')), ), } - const reply = jest.fn(() => Promise.resolve(message)) + // descricao is now collected via a modal (paragraph text input), not a + // plain string option — the modal submission is its own interaction + // that owns the reply/editReply for the preview + button flow. + const modalReply = jest.fn(() => Promise.resolve(message)) + const modalSubmit = { + user: { id: 'u1' }, + fields: { + getTextInputValue: (name: string) => opts[name] ?? '', + }, + reply: modalReply, + editReply: jest.fn(), + } const interaction = { + id: 'interaction-1', user: { id: 'u1', tag: 'mod#1' }, guild: { id: 'g1', @@ -98,10 +110,19 @@ function makeInteraction( options: { getString: (name: string) => opts[name] ?? null, }, - reply, + showModal: jest.fn(), + awaitModalSubmit: jest.fn(() => Promise.resolve(modalSubmit)), + reply: jest.fn(), editReply: jest.fn(), } - return { interaction, reply, send, update, message } + return { + interaction, + reply: modalReply, + send, + update, + message, + modalSubmit, + } } describe('/vaga command', () => { @@ -185,4 +206,82 @@ describe('/vaga command', () => { }) expect(send).not.toHaveBeenCalled() }) + + it('shows a modal for descricao before doing any lookups', async () => { + const { interaction } = makeInteraction(base, { + customId: 'vaga_cancel', + }) + await vaga.execute({ + interaction: interaction as never, + client: {} as never, + }) + expect(interaction.showModal).toHaveBeenCalledTimes(1) + expect(interaction.awaitModalSubmit).toHaveBeenCalledTimes(1) + }) + + it('preserves real line breaks from the modal in the published message', async () => { + const multiline = { + ...base, + descricao: '- Requisito um\n- Requisito dois\n- Requisito três', + } + const { interaction, send } = makeInteraction(multiline, { + customId: 'vaga_publish', + }) + await vaga.execute({ + interaction: interaction as never, + client: {} as never, + }) + const sent = send.mock.calls[0][0] as { content: string } + expect(sent.content).toContain( + '- Requisito um\n- Requisito dois\n- Requisito três', + ) + }) + + it('does nothing further when the modal submission times out', async () => { + const { interaction, send } = makeInteraction(base) + interaction.awaitModalSubmit = jest.fn(() => + Promise.reject(new Error('time')), + ) + await vaga.execute({ + interaction: interaction as never, + client: {} as never, + }) + expect(send).not.toHaveBeenCalled() + }) + + it('scopes the modal filter to this invocation, rejecting a stale/concurrent submission (regression for cross-invocation mixing)', async () => { + const { interaction } = makeInteraction(base, { + customId: 'vaga_cancel', + }) + await vaga.execute({ + interaction: interaction as never, + client: {} as never, + }) + const [{ filter }] = interaction.awaitModalSubmit.mock.calls[0] as [ + { + filter: (i: { + user: { id: string } + customId: string + }) => boolean + }, + ] + const thisCustomId = `vaga_descricao_${interaction.id}` + + // Same user, this invocation's modal — accepted. + expect(filter({ user: { id: 'u1' }, customId: thisCustomId })).toBe( + true, + ) + // Same user, a DIFFERENT (e.g. earlier, still-open) /vaga + // invocation's modal — must be rejected, not mixed in. + expect( + filter({ + user: { id: 'u1' }, + customId: 'vaga_descricao_some-other-interaction', + }), + ).toBe(false) + // Different user entirely — rejected. + expect(filter({ user: { id: 'u2' }, customId: thisCustomId })).toBe( + false, + ) + }) }) diff --git a/packages/bot/src/functions/general/commands/vaga.ts b/packages/bot/src/functions/general/commands/vaga.ts index 701f04f62..37d57cda3 100644 --- a/packages/bot/src/functions/general/commands/vaga.ts +++ b/packages/bot/src/functions/general/commands/vaga.ts @@ -6,6 +6,9 @@ import { ButtonStyle, ComponentType, ChannelType, + ModalBuilder, + TextInputBuilder, + TextInputStyle, type ChatInputCommandInteraction, type GuildBasedChannel, } from 'discord.js' @@ -46,6 +49,46 @@ function buildVagaMessage( return parts.join('\n\n') } +// Discord's plain string options are single-line inputs — pasting a bulleted +// requirements list into one collapses it onto one line (browsers flatten +//