From e7098110a5bb6a52f8c7a77d02eedc609a3a6519 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:16:39 +0100 Subject: [PATCH] test: discard drafts without a question and once Undo is over, and take onSend's second argument, since the bin and Undo send changed --- .../composer-draft-attachments.test.tsx | 19 +++++++++------ .../__tests__/composer-draft-login.test.tsx | 24 +++++++++++-------- ...composer-save-pending-attachments.test.tsx | 3 ++- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/components/email/__tests__/composer-draft-attachments.test.tsx b/components/email/__tests__/composer-draft-attachments.test.tsx index b3667e961..d4e3be641 100644 --- a/components/email/__tests__/composer-draft-attachments.test.tsx +++ b/components/email/__tests__/composer-draft-attachments.test.tsx @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { EmailComposer } from '../email-composer'; import { useAuthStore } from '@/stores/auth-store'; +import { UNDO_TOAST_DURATION } from '@/lib/email-action-toast'; // ─── Heavy component mocks (mirrors composer-close-guard.test.tsx) ──────────── @@ -129,6 +130,7 @@ vi.mock('@/stores/template-store', () => { vi.mock('@/stores/toast-store', () => ({ toast: { info: () => {}, error: () => {}, success: () => {} }, + useToastStore: { getState: () => ({ toasts: [] }) }, })); vi.mock('@/lib/plugin-hooks', () => ({ @@ -352,7 +354,7 @@ describe('a re-opened draft that lives in a group account', () => { fireEvent.click(screen.getAllByTestId('composer-send')[0]); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); - expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'eaaaaab', draftAccountId: 'c' })); + expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'eaaaaab', draftAccountId: 'c' }), expect.anything()); }); it('sends a version saved on the way without the group account', async () => { @@ -366,7 +368,7 @@ describe('a re-opened draft that lives in a group account', () => { await act(async () => { await vi.advanceTimersByTimeAsync(0); }); expect(createDraft.mock.calls[0][13]?.previousDraftAccountId).toBe('c'); - expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'draft-v2', draftAccountId: undefined })); + expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'draft-v2', draftAccountId: undefined }), expect.anything()); }); it('tells the host where the draft lives when it is discarded', async () => { @@ -376,9 +378,11 @@ describe('a re-opened draft that lives in a group account', () => { render(); fireEvent.change(screen.getByDisplayValue('Quarterly numbers'), { target: { value: 'Quarterly numbers v2' } }); - // Closing keeps the draft now; Discard is the way to delete it. + // Closing keeps the draft now; Discard is the way to delete it, once its Undo is over. fireEvent.click(screen.getAllByTestId('composer-discard')[0]); - fireEvent.click(within(screen.getByRole('alertdialog')).getByText('discard')); + expect(screen.queryByRole('alertdialog')).toBeNull(); + expect(onDiscardDraft).not.toHaveBeenCalled(); + await act(async () => { await vi.advanceTimersByTimeAsync(UNDO_TOAST_DURATION); }); expect(onDiscardDraft).toHaveBeenCalledWith('eaaaaab', { accountId: 'c', localAccountId: undefined }); }); @@ -394,9 +398,10 @@ describe('a re-opened draft that lives in a group account', () => { expect(createDraft.mock.calls[0][13]?.previousDraftAccountId).toBeUndefined(); fireEvent.change(screen.getByDisplayValue('Quarterly numbers v2'), { target: { value: 'Quarterly numbers v3' } }); - // Closing keeps the draft now; Discard is the way to delete it. + // Closing keeps the draft now; Discard is the way to delete it, once its Undo is over. fireEvent.click(screen.getAllByTestId('composer-discard')[0]); - fireEvent.click(within(screen.getByRole('alertdialog')).getByText('discard')); + expect(onDiscardDraft).not.toHaveBeenCalled(); + await act(async () => { await vi.advanceTimersByTimeAsync(UNDO_TOAST_DURATION); }); expect(onDiscardDraft).toHaveBeenCalledWith('draft-v2', { accountId: undefined, localAccountId: undefined }); }); }); @@ -450,6 +455,6 @@ describe('send while a draft save is in flight', () => { expect(createDraft).toHaveBeenCalledTimes(2); expect(createDraft.mock.calls[1][7]).toBe('draft-v2'); expect(onSend).toHaveBeenCalledTimes(1); - expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'draft-v3', subject: 'Quarterly numbers v3' })); + expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ draftId: 'draft-v3', subject: 'Quarterly numbers v3' }), expect.anything()); }); }); diff --git a/components/email/__tests__/composer-draft-login.test.tsx b/components/email/__tests__/composer-draft-login.test.tsx index 7f6e1a4b3..6d2695dc3 100644 --- a/components/email/__tests__/composer-draft-login.test.tsx +++ b/components/email/__tests__/composer-draft-login.test.tsx @@ -1,8 +1,9 @@ -import { render, screen, fireEvent, act, within } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { EmailComposer, type ComposerDraftData } from '../email-composer'; import { useAuthStore } from '@/stores/auth-store'; +import { UNDO_TOAST_DURATION } from '@/lib/email-action-toast'; const offlineQueue = vi.hoisted(() => ({ noteDraftReplacedElsewhere: vi.fn(async () => {}) })); vi.mock('@/lib/offline/queue', async (importOriginal) => ({ @@ -143,6 +144,7 @@ vi.mock('@/stores/template-store', () => { vi.mock('@/stores/toast-store', () => ({ toast: { info: () => {}, error: () => {}, success: () => {} }, + useToastStore: { getState: () => ({ toasts: [] }) }, })); vi.mock('@/lib/plugin-hooks', () => ({ @@ -254,10 +256,12 @@ describe('a draft and the login it was saved with', () => { await act(async () => { await vi.advanceTimersByTimeAsync(2500); }); }, switchFrom: (identityId: string) => fireEvent.change(screen.getByTestId('composer-from'), { target: { value: identityId } }), - discard: () => { - // The Discard button asks even when the draft is saved. + discard: async (onDiscardDraft: ReturnType) => { + // The bin asks nothing; the draft goes once its Undo is over. fireEvent.click(screen.getByText('discard')); - fireEvent.click(within(screen.getByRole('alertdialog')).getByText('discard')); + expect(screen.queryByRole('alertdialog')).toBeNull(); + expect(onDiscardDraft).not.toHaveBeenCalled(); + await act(async () => { await vi.advanceTimersByTimeAsync(UNDO_TOAST_DURATION); }); }, send: async () => { fireEvent.click(screen.getAllByTestId('composer-send')[0]); @@ -274,7 +278,7 @@ describe('a draft and the login it was saved with', () => { expect(clientA.createDraft).toHaveBeenCalledTimes(1); composer.switchFrom('login-b::id-b'); await act(async () => { await vi.advanceTimersByTimeAsync(2500); }); - composer.discard(); + await composer.discard(onDiscardDraft); expect(onDiscardDraft).toHaveBeenCalledWith('draft-a2', { accountId: undefined, localAccountId: 'login-a' }); }); @@ -295,7 +299,7 @@ describe('a draft and the login it was saved with', () => { expect(clientA.deleteEmail).toHaveBeenCalledWith('draft-a2', undefined); expect(clientB.deleteEmail).not.toHaveBeenCalled(); - composer.discard(); + await composer.discard(onDiscardDraft); expect(onDiscardDraft).toHaveBeenCalledWith('draft-b2', { accountId: undefined, localAccountId: 'login-b' }); }); @@ -330,7 +334,7 @@ describe('a draft and the login it was saved with', () => { composer.switchFrom('login-b::id-b'); await composer.send(); - expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ localAccountId: 'login-b', draftId: undefined, draftAccountId: undefined })); + expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ localAccountId: 'login-b', draftId: undefined, draftAccountId: undefined }), expect.anything()); expect(clientA.deleteEmail).toHaveBeenCalledWith('draft-a2', undefined); expect(clientB.deleteEmail).not.toHaveBeenCalled(); }); @@ -373,16 +377,16 @@ describe('a draft and the login it was saved with', () => { expect(clientB.deleteEmail).toHaveBeenCalledWith('draft-b1', 'c'); expect(clientA.deleteEmail).not.toHaveBeenCalled(); - composer.discard(); + await composer.discard(onDiscardDraft); expect(onDiscardDraft).toHaveBeenCalledWith('draft-a2', { accountId: undefined, localAccountId: 'login-a' }); }); - it('discards an unsaved draft opened from another login through that login and account', () => { + it('discards an unsaved draft opened from another login through that login and account', async () => { const onDiscardDraft = vi.fn(); const composer = open({ ...DRAFT, draftId: 'draft-b1', draftAccountId: 'c', draftLocalAccountId: 'login-b' }, { onDiscardDraft }); fireEvent.change(screen.getByDisplayValue('Hello'), { target: { value: 'Hello 2' } }); - composer.discard(); + await composer.discard(onDiscardDraft); expect(onDiscardDraft).toHaveBeenCalledWith('draft-b1', { accountId: 'c', localAccountId: 'login-b' }); }); diff --git a/components/email/__tests__/composer-save-pending-attachments.test.tsx b/components/email/__tests__/composer-save-pending-attachments.test.tsx index 4bb4a0d5b..3fef2ec9a 100644 --- a/components/email/__tests__/composer-save-pending-attachments.test.tsx +++ b/components/email/__tests__/composer-save-pending-attachments.test.tsx @@ -230,8 +230,9 @@ describe('Save and close with attachment uploads', () => { }); it('can discard during the wait without a later save or close follow-up', async () => { const c = await openComposer(); await c.add(file()); await c.chooseSave(); + // The bin asks nothing: the composer goes at once, with Undo on a toast. fireEvent.click(screen.getAllByTestId('composer-discard')[0]); - fireEvent.click(within(await screen.findByRole('alertdialog')).getByRole('button', { name: /^Discard$/ })); + expect(screen.queryByRole('alertdialog')).toBeNull(); await complete(0); await new Promise(resolve => setTimeout(resolve, 200)); expect(creations).toEqual([]); expect(c.closed).toHaveBeenCalledOnce(); });