test(chronik): extract applyClientFilter helper with full test coverage
Addresses review concern: the fuer-dich predicate (youMentioned || youParticipated) had zero test coverage after feedFilters.test.ts was deleted. The new clientFilter module is a pure function that is directly testable, and the test explicitly documents why MENTION_CREATED items without the youMentioned flag are now excluded (they would have shown mentions directed at OTHER users under the old feedFilters.ts logic). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import ChronikEmptyState from '$lib/components/chronik/ChronikEmptyState.svelte'
|
|||||||
import ChronikErrorCard from '$lib/components/chronik/ChronikErrorCard.svelte';
|
import ChronikErrorCard from '$lib/components/chronik/ChronikErrorCard.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
import type { FilterValue } from './+page.server';
|
import type { FilterValue } from './+page.server';
|
||||||
|
import { applyClientFilter } from './clientFilter';
|
||||||
|
|
||||||
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
||||||
|
|
||||||
@@ -83,13 +84,7 @@ async function onMarkAllRead() {
|
|||||||
await notificationStore.markAllRead();
|
await notificationStore.markAllRead();
|
||||||
}
|
}
|
||||||
|
|
||||||
// fuer-dich cannot be expressed as a server-side kinds filter — youMentioned/youParticipated
|
const displayFeed = $derived(applyClientFilter(data.activityFeed, data.filter));
|
||||||
// are user-scoped flags that require client-side narrowing
|
|
||||||
const displayFeed = $derived(
|
|
||||||
data.filter === 'fuer-dich'
|
|
||||||
? data.activityFeed.filter((item) => item.youMentioned || item.youParticipated)
|
|
||||||
: data.activityFeed
|
|
||||||
);
|
|
||||||
|
|
||||||
const isEmpty = $derived(displayFeed.length === 0);
|
const isEmpty = $derived(displayFeed.length === 0);
|
||||||
const emptyVariant = $derived<'first-run' | 'filter-empty' | 'inbox-zero'>(
|
const emptyVariant = $derived<'first-run' | 'filter-empty' | 'inbox-zero'>(
|
||||||
|
|||||||
94
frontend/src/routes/chronik/clientFilter.test.ts
Normal file
94
frontend/src/routes/chronik/clientFilter.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { applyClientFilter } from './clientFilter';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type Item = components['schemas']['ActivityFeedItemDTO'];
|
||||||
|
|
||||||
|
function makeItem(overrides: Partial<Item> = {}): Item {
|
||||||
|
return {
|
||||||
|
kind: 'FILE_UPLOADED',
|
||||||
|
documentId: 'd1',
|
||||||
|
documentTitle: 'Brief A',
|
||||||
|
happenedAt: '2026-04-20T10:00:00Z',
|
||||||
|
youMentioned: false,
|
||||||
|
youParticipated: false,
|
||||||
|
count: 1,
|
||||||
|
actor: null,
|
||||||
|
happenedAtUntil: null,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('applyClientFilter', () => {
|
||||||
|
describe('non-fuer-dich filters pass through unchanged', () => {
|
||||||
|
it('alle returns all items', () => {
|
||||||
|
const items = [makeItem(), makeItem({ kind: 'COMMENT_ADDED' })];
|
||||||
|
expect(applyClientFilter(items, 'alle')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hochgeladen passes through (server already filtered by kinds)', () => {
|
||||||
|
const items = [makeItem({ kind: 'FILE_UPLOADED' }), makeItem({ kind: 'COMMENT_ADDED' })];
|
||||||
|
expect(applyClientFilter(items, 'hochgeladen')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transkription passes through (server already filtered by kinds)', () => {
|
||||||
|
const items = [makeItem({ kind: 'TEXT_SAVED' })];
|
||||||
|
expect(applyClientFilter(items, 'transkription')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('kommentare passes through (server already filtered by kinds)', () => {
|
||||||
|
const items = [makeItem({ kind: 'COMMENT_ADDED' })];
|
||||||
|
expect(applyClientFilter(items, 'kommentare')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fuer-dich applies youMentioned || youParticipated predicate', () => {
|
||||||
|
it('includes items where youMentioned is true', () => {
|
||||||
|
const items = [makeItem({ youMentioned: true })];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes items where youParticipated is true', () => {
|
||||||
|
const items = [makeItem({ youParticipated: true })];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes items where both flags are true', () => {
|
||||||
|
const items = [makeItem({ youMentioned: true, youParticipated: true })];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes items where neither flag is set', () => {
|
||||||
|
const items = [makeItem({ kind: 'COMMENT_ADDED' })];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MENTION_CREATED without youMentioned flag is excluded', () => {
|
||||||
|
// youMentioned is set by the backend only when this event is directed at the current user.
|
||||||
|
// Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally —
|
||||||
|
// that incorrectly showed mentions directed at OTHER users.
|
||||||
|
const items = [
|
||||||
|
makeItem({ kind: 'MENTION_CREATED', youMentioned: false, youParticipated: false })
|
||||||
|
];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MENTION_CREATED with youMentioned true is included', () => {
|
||||||
|
const items = [makeItem({ kind: 'MENTION_CREATED', youMentioned: true })];
|
||||||
|
expect(applyClientFilter(items, 'fuer-dich')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters mixed items to only personally relevant ones', () => {
|
||||||
|
const items = [
|
||||||
|
makeItem({ kind: 'FILE_UPLOADED' }),
|
||||||
|
makeItem({ kind: 'COMMENT_ADDED', youParticipated: true }),
|
||||||
|
makeItem({ kind: 'MENTION_CREATED', youMentioned: true }),
|
||||||
|
makeItem({ kind: 'TEXT_SAVED' })
|
||||||
|
];
|
||||||
|
const result = applyClientFilter(items, 'fuer-dich');
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].kind).toBe('COMMENT_ADDED');
|
||||||
|
expect(result[1].kind).toBe('MENTION_CREATED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
18
frontend/src/routes/chronik/clientFilter.ts
Normal file
18
frontend/src/routes/chronik/clientFilter.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
import type { FilterValue } from './+page.server';
|
||||||
|
|
||||||
|
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
||||||
|
|
||||||
|
// All server-side filters (hochgeladen, transkription, kommentare) are already applied via
|
||||||
|
// kinds param — the server returns only matching items, so client-side is a no-op.
|
||||||
|
// fuer-dich is the only filter that requires client-side narrowing: youMentioned and
|
||||||
|
// youParticipated are user-scoped flags that cannot be expressed as a kinds filter.
|
||||||
|
export function applyClientFilter(
|
||||||
|
items: ActivityFeedItemDTO[],
|
||||||
|
filter: FilterValue
|
||||||
|
): ActivityFeedItemDTO[] {
|
||||||
|
if (filter === 'fuer-dich') {
|
||||||
|
return items.filter((item) => item.youMentioned || item.youParticipated);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user