diff --git a/components/settings/__tests__/vacation-forward.test.tsx b/components/settings/__tests__/vacation-forward.test.tsx index 95ba8a002..bf2620166 100644 --- a/components/settings/__tests__/vacation-forward.test.tsx +++ b/components/settings/__tests__/vacation-forward.test.tsx @@ -112,8 +112,8 @@ describe('forwarding in the vacation card', () => { expect(screen.getByText('forward.title')).toBeInTheDocument(); fireEvent.click(screen.getByRole('switch', { name: 'status.label' })); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); await save(server); expect(saved()).toBe(true); @@ -136,8 +136,8 @@ describe('forwarding in the vacation card', () => { const server = stalwart([rule('a')]); await openCard(server); // The auto-reply stays off; only forwarding is switched on. - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); await save(server); expect(saved()).toBe(true); @@ -153,8 +153,8 @@ describe('forwarding in the vacation card', () => { const server = stalwart([]); await openCard(server); fireEvent.click(screen.getByRole('switch', { name: 'status.label' })); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); await save(server); expect(server.active()).toBe('filters'); @@ -170,27 +170,46 @@ describe('forwarding in the vacation card', () => { const server = stalwart([rule('a')]); await openCard(server); fireEvent.click(screen.getByRole('switch', { name: 'status.label' })); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); fireEvent.click(keepSwitch()); await save(server); expect(server.content('filters')).toContain(' redirect :copy "kollege@example.com";'); expect(server.content('filters')).not.toContain(' stop;'); }); - it('will not save forwarding without a usable address', async () => { + it('will not switch forwarding on without a usable address, and says to enter one', async () => { const server = stalwart([rule('a')]); await openCard(server); + // The address field is there before forwarding is on. + expect(addressInput().value).toBe(''); fireEvent.click(forwardSwitch()); + expect(forwardSwitch().getAttribute('aria-checked')).toBe('false'); + expect(screen.getByText('enter_address_first')).toBeInTheDocument(); fireEvent.change(addressInput(), { target: { value: 'kollege' } }); - expect(screen.getByText('warnings.forward_address')).toBeInTheDocument(); + fireEvent.click(forwardSwitch()); + expect(forwardSwitch().getAttribute('aria-checked')).toBe('false'); await settle(); expect(server.client.setVacationResponse).not.toHaveBeenCalled(); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); - expect(screen.queryByText('warnings.forward_address')).toBeNull(); + expect(screen.queryByText('enter_address_first')).toBeNull(); + fireEvent.click(forwardSwitch()); + expect(forwardSwitch().getAttribute('aria-checked')).toBe('true'); await save(server); }); + it('will not save forwarding that is on once its address is made unusable', async () => { + const server = stalwart([rule('a')]); + await openCard(server); + fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); + await save(server); + fireEvent.change(addressInput(), { target: { value: 'kollege' } }); + expect(screen.getByText('warnings.forward_address')).toBeInTheDocument(); + await settle(); + expect(server.client.setVacationResponse).toHaveBeenCalledTimes(1); + }); + it('does not count an address typed and switched off again as a change', async () => { const server = stalwart([rule('a')]); await openCard(server); @@ -205,8 +224,8 @@ describe('forwarding in the vacation card', () => { it('warns that a forwarding rule is skipped when a copy is kept and the server allows one redirect', async () => { const server = stalwart([rule('f', { actions: [{ type: 'forward', value: 'chef@example.com' }] })], { maxNumberRedirects: 1 }); await openCard(server); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); expect(screen.queryByText(/^forward_limit/)).toBeNull(); fireEvent.click(keepSwitch()); expect(screen.getByText('forward_limit 1')).toBeInTheDocument(); @@ -216,8 +235,8 @@ describe('forwarding in the vacation card', () => { const server = stalwart([rule('a')]); await openCard(server); fireEvent.click(screen.getByRole('switch', { name: 'status.label' })); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); + fireEvent.click(forwardSwitch()); await save(server); // Back as stored after the save, as the server has it now. @@ -256,9 +275,9 @@ describe('forwarding in the vacation card', () => { const server = stalwart([rule('a')]); await openCard(server); server.client.updateSieveScript.mockRejectedValueOnce(new Error('server said no')); + fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); fireEvent.click(screen.getByRole('switch', { name: 'status.label' })); fireEvent.click(forwardSwitch()); - fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); await save(server); expect(toast.error).toHaveBeenCalledWith('vacation_filters_save_failed'); expect(saved()).toBe(false); @@ -318,8 +337,8 @@ describe('forwarding in the vacation card', () => { it('starts over on another account, even where both store the same', async () => { const server = stalwart([rule('a')]); await openCard(server); - fireEvent.click(forwardSwitch()); fireEvent.change(addressInput(), { target: { value: 'chef@a.example' } }); + fireEvent.click(forwardSwitch()); // Neither account has forwarding stored, so no stored value changes. act(() => { useAuthStore.setState({ activeAccountId: 'other-login' }); }); @@ -406,7 +425,7 @@ describe('who gets the auto-reply, in the vacation card', () => { withIdentities(); await openCard(stalwart([rule('a')])); expect(audienceSelect().value).toBe('all'); - expect(screen.getByText('audience.description example.com, example.org')).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'audience_internal example.com, example.org' })).toBeInTheDocument(); }); it('is not offered without identities to tell the own domains', async () => { @@ -511,20 +530,25 @@ describe('saving the out-of-office card', () => { describe('layout of the out-of-office card', () => { const follows = (a: Node, b: Node) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0; - it('puts the period first, then the auto-reply and forwarding, each with its own switch and state', async () => { + it('puts the switch first, then the optional dates, the message and forwarding, each with its own switch and state', async () => { await openCard(stalwart([rule('a')])); - const [page, reply, forwarding] = ['title', 'auto_reply.title', 'forward.title'].map((key) => screen.getByText(key)); - const start = screen.getByText('date_range.start'); - expect(follows(page, start)).toBe(true); - expect(follows(start, reply)).toBe(true); - expect(follows(reply, forwarding)).toBe(true); - expect(screen.queryByText('date_range.title')).toBeNull(); + const [page, message, forwarding] = ['title', 'message_title', 'forward.title'].map((key) => screen.getByText(key)); + const replySwitch = screen.getByRole('switch', { name: 'status.label' }); + const [from, until] = ['from_label', 'until_label'].map((key) => screen.getByText(key)); + expect(follows(page, replySwitch)).toBe(true); + expect(follows(replySwitch, from)).toBe(true); + expect(follows(from, until)).toBe(true); + expect(follows(until, message)).toBe(true); + expect(follows(message, forwarding)).toBe(true); + // One message editor, no second "formatted message" box. + expect(screen.queryByText('message.html_label')).toBeNull(); // Two switches, each saying whether it is on, independent of the other. const replyRow = screen.getByRole('switch', { name: 'status.label' }).parentElement!; const forwardRow = forwardSwitch().parentElement!; expect(within(replyRow).getByText('status.inactive')).toBeInTheDocument(); expect(within(forwardRow).getByText('status.inactive')).toBeInTheDocument(); + fireEvent.change(addressInput(), { target: { value: 'kollege@example.com' } }); fireEvent.click(forwardSwitch()); expect(within(forwardRow).getByText('status.active')).toBeInTheDocument(); expect(within(replyRow).getByText('status.inactive')).toBeInTheDocument(); diff --git a/components/settings/__tests__/vacation-html-body.test.tsx b/components/settings/__tests__/vacation-html-body.test.tsx deleted file mode 100644 index 46035ede3..000000000 --- a/components/settings/__tests__/vacation-html-body.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import React from 'react'; -import { mockStalwartAccount } from '@/lib/filters/__tests__/sieve-mock'; -import { useAuthStore } from '@/stores/auth-store'; -import { useVacationStore } from '@/stores/vacation-store'; -import { VacationSettings } from '../vacation-settings'; - -// The HTML reply and its plain-text part in the real vacation card: a save -// derives the text from the HTML when the Body is left blank, and what the -// server then holds must not look like a change still waiting to be saved. - -const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn() })); -vi.mock('@/stores/toast-store', () => ({ toast })); -vi.mock('next-intl', () => { - const t = (key: string, values?: Record) => (values ? `${key} ${Object.values(values).join(' ')}` : key); - return { useTranslations: () => t, useLocale: () => 'en' }; -}); -// The editor hands the card its HTML as typed. -vi.mock('@/components/email/rich-text-editor', () => ({ - RichTextEditor: ({ content, onChange }: { content: string; onChange: (html: string) => void }) => ( -