refactor(notification): replace callback props with form actions in Dropdown and Bell
NotificationDropdown now wraps each row in a <form action="/aktivitaeten?/dismiss-notification"> and the mark-all control in <form action="/aktivitaeten?/mark-all-read">, wired via use:enhance for optimistic UI. Props renamed onMarkRead/onMarkAllRead → optimisticMarkRead/optimisticMarkAllRead to match the simplified store API. NotificationBell passes the store helpers directly; handleMarkRead is removed. Test mocks updated: $app/forms enhance mock fires SubmitFunction synchronously on form submit so callback assertions work without a real HTTP round-trip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,19 @@ import NotificationDropdown from './NotificationDropdown.svelte';
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
// Invoke the SubmitFunction synchronously on form submit so tests can assert
|
||||
// that optimisticMarkRead / optimisticMarkAllRead are called.
|
||||
vi.mock('$app/forms', () => ({
|
||||
enhance(node: HTMLFormElement, submit?: (opts: { formData: FormData }) => unknown) {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
submit?.({ formData: new FormData(node) } as never);
|
||||
};
|
||||
node.addEventListener('submit', handler);
|
||||
return { destroy: () => node.removeEventListener('submit', handler) };
|
||||
}
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
@@ -29,8 +42,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -42,8 +55,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -55,8 +68,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -70,8 +83,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification()],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -83,8 +96,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ type: 'REPLY', actorName: 'Bert' })],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -98,8 +111,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ type: 'MENTION', actorName: 'Clara' })],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -116,8 +129,8 @@ describe('NotificationDropdown', () => {
|
||||
makeNotification({ id: 'n1', read: false }),
|
||||
makeNotification({ id: 'n2', read: true })
|
||||
],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -126,37 +139,83 @@ describe('NotificationDropdown', () => {
|
||||
expect(unreadDots.length).toBe(1);
|
||||
});
|
||||
|
||||
it('calls onMarkRead with the notification when an item is clicked', async () => {
|
||||
const onMarkRead = vi.fn();
|
||||
it('each notification row is wrapped in a form posting to the dismiss action', async () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ id: 'n42' })],
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const form = document.querySelector('form[action="/aktivitaeten?/dismiss-notification"]');
|
||||
expect(form).not.toBeNull();
|
||||
expect(form?.getAttribute('method')).toBe('POST');
|
||||
});
|
||||
|
||||
it('the dismiss form has a hidden notificationId input with the notification id', async () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ id: 'n42' })],
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const input = document.querySelector<HTMLInputElement>(
|
||||
'form[action="/aktivitaeten?/dismiss-notification"] input[name="notificationId"]'
|
||||
);
|
||||
expect(input?.value).toBe('n42');
|
||||
});
|
||||
|
||||
it('calls optimisticMarkRead with the notification id when a row is submitted', async () => {
|
||||
const optimisticMarkRead = vi.fn();
|
||||
const n = makeNotification({ id: 'n42', actorName: 'Anna' });
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [n],
|
||||
onMarkRead,
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead,
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /Anna hat auf deinen/i }).click();
|
||||
|
||||
expect(onMarkRead).toHaveBeenCalledWith(n);
|
||||
expect(optimisticMarkRead).toHaveBeenCalledWith('n42');
|
||||
});
|
||||
|
||||
it('calls onMarkAllRead when the mark-all-read button is clicked', async () => {
|
||||
const onMarkAllRead = vi.fn();
|
||||
it('the mark-all-read control is a form posting to the mark-all-read action', async () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification()],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead,
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const form = document.querySelector('form[action="/aktivitaeten?/mark-all-read"]');
|
||||
expect(form).not.toBeNull();
|
||||
expect(form?.getAttribute('method')).toBe('POST');
|
||||
});
|
||||
|
||||
it('calls optimisticMarkAllRead when the mark-all-read button is submitted', async () => {
|
||||
const optimisticMarkAllRead = vi.fn();
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification()],
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead,
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /alle gelesen/i }).click();
|
||||
|
||||
expect(onMarkAllRead).toHaveBeenCalledOnce();
|
||||
expect(optimisticMarkAllRead).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls onClose when the view-all button is clicked', async () => {
|
||||
@@ -164,8 +223,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose
|
||||
}
|
||||
});
|
||||
@@ -179,8 +238,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -193,12 +252,15 @@ describe('NotificationDropdown', () => {
|
||||
it('calls onClose before navigating to /aktivitaeten', async () => {
|
||||
const callOrder: string[] = [];
|
||||
const onClose = vi.fn(() => callOrder.push('close'));
|
||||
vi.mocked(goto).mockImplementation(() => callOrder.push('goto'));
|
||||
vi.mocked(goto).mockImplementation(() => {
|
||||
callOrder.push('goto');
|
||||
return Promise.resolve();
|
||||
});
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose
|
||||
}
|
||||
});
|
||||
@@ -212,8 +274,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ id: 'm1', type: 'MENTION', actorName: 'Anna' })],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -225,8 +287,8 @@ describe('NotificationDropdown', () => {
|
||||
render(NotificationDropdown, {
|
||||
props: {
|
||||
notifications: [makeNotification({ id: 'r1', type: 'REPLY', actorName: 'Bert' })],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
@@ -242,14 +304,13 @@ describe('NotificationDropdown', () => {
|
||||
makeNotification({ id: 'n1', actorName: 'First' }),
|
||||
makeNotification({ id: 'n2', actorName: 'Second' })
|
||||
],
|
||||
onMarkRead: () => {},
|
||||
onMarkAllRead: () => {},
|
||||
optimisticMarkRead: () => {},
|
||||
optimisticMarkAllRead: () => {},
|
||||
onClose: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const items = document.querySelectorAll('button[type="button"]');
|
||||
// At least 2 items + mark-all button
|
||||
expect(items.length).toBeGreaterThanOrEqual(2);
|
||||
const forms = document.querySelectorAll('form[action="/aktivitaeten?/dismiss-notification"]');
|
||||
expect(forms.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user