Some checks failed
CI / Unit & Component Tests (push) Successful in 3m47s
CI / Backend Unit Tests (push) Successful in 2m41s
CI / E2E Tests (push) Failing after 2h25m30s
CI / Unit & Component Tests (pull_request) Successful in 2m48s
CI / Backend Unit Tests (pull_request) Successful in 2m29s
CI / E2E Tests (pull_request) Failing after 2h29m1s
- AnnotationSidePanel: cover visibility (null vs set annotationId), close button callback, and targetCommentId forwarding - layout.svelte.spec: mock $env/static/public to satisfy PUBLIC_NOTIFICATION_POLL_MS import from NotificationBell - mention.spec: update assertion to match span-based mention rendering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { detectMention, extractContent, renderBody } from './mention';
|
|
import type { MentionDTO } from '$lib/types';
|
|
|
|
// ─── detectMention ────────────────────────────────────────────────────────────
|
|
|
|
describe('detectMention', () => {
|
|
it('returns null when text has no @', () => {
|
|
expect(detectMention('hello world', 11)).toBeNull();
|
|
});
|
|
|
|
it('returns null when @ is not the most recent trigger word', () => {
|
|
// cursor is past a completed mention (next word started)
|
|
expect(detectMention('hello @Hans Müller more', 22)).toBeNull();
|
|
});
|
|
|
|
it('returns empty string immediately after @', () => {
|
|
expect(detectMention('hello @', 7)).toBe('');
|
|
});
|
|
|
|
it('returns query text after @', () => {
|
|
expect(detectMention('hello @Han', 10)).toBe('Han');
|
|
});
|
|
|
|
it('returns null when @ is preceded by a letter (email address pattern)', () => {
|
|
expect(detectMention('user@example', 12)).toBeNull();
|
|
});
|
|
|
|
it('returns query for @ at the very start of string', () => {
|
|
expect(detectMention('@Hans', 5)).toBe('Hans');
|
|
});
|
|
|
|
it('returns null when cursor is before the @', () => {
|
|
expect(detectMention('@Hans', 0)).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ─── extractContent ───────────────────────────────────────────────────────────
|
|
|
|
describe('extractContent', () => {
|
|
it('returns empty arrays for empty string', () => {
|
|
const result = extractContent('', []);
|
|
expect(result.content).toBe('');
|
|
expect(result.mentionedUserIds).toEqual([]);
|
|
});
|
|
|
|
it('returns plain content unchanged when no candidates', () => {
|
|
const result = extractContent('Hello world', []);
|
|
expect(result.content).toBe('Hello world');
|
|
expect(result.mentionedUserIds).toEqual([]);
|
|
});
|
|
|
|
it('extracts user id when @FirstName LastName is in content', () => {
|
|
const candidates: MentionDTO[] = [{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' }];
|
|
const result = extractContent('Hey @Hans Müller how are you?', candidates);
|
|
expect(result.mentionedUserIds).toContain('uuid-1');
|
|
});
|
|
|
|
it('deduplicates user ids when same user mentioned twice', () => {
|
|
const candidates: MentionDTO[] = [{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' }];
|
|
const result = extractContent('@Hans Müller and @Hans Müller again', candidates);
|
|
expect(result.mentionedUserIds).toHaveLength(1);
|
|
expect(result.mentionedUserIds).toContain('uuid-1');
|
|
});
|
|
|
|
it('collects multiple distinct users', () => {
|
|
const candidates: MentionDTO[] = [
|
|
{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' },
|
|
{ id: 'uuid-2', firstName: 'Anna', lastName: 'Schmidt' }
|
|
];
|
|
const result = extractContent('@Hans Müller and @Anna Schmidt', candidates);
|
|
expect(result.mentionedUserIds).toContain('uuid-1');
|
|
expect(result.mentionedUserIds).toContain('uuid-2');
|
|
});
|
|
});
|
|
|
|
// ─── renderBody ───────────────────────────────────────────────────────────────
|
|
|
|
describe('renderBody', () => {
|
|
it('returns escaped plain text when no mentions', () => {
|
|
expect(renderBody('Hello world', [])).toBe('Hello world');
|
|
});
|
|
|
|
it('escapes < and > in content', () => {
|
|
const result = renderBody('<script>alert(1)</script>', []);
|
|
expect(result).toContain('<script>');
|
|
expect(result).not.toContain('<script>');
|
|
});
|
|
|
|
it('escapes & in content', () => {
|
|
const result = renderBody('AT&T', []);
|
|
expect(result).toContain('AT&T');
|
|
});
|
|
|
|
it('wraps @mention in a mention span', () => {
|
|
const mentions: MentionDTO[] = [{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' }];
|
|
const result = renderBody('Hey @Hans Müller!', mentions);
|
|
expect(result).toContain('<span');
|
|
expect(result).toContain('class="mention"');
|
|
expect(result).toContain('Hans Müller');
|
|
});
|
|
|
|
it('does not double-encode already escaped text', () => {
|
|
const mentions: MentionDTO[] = [{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' }];
|
|
const result = renderBody('Check @Hans Müller', mentions);
|
|
expect(result).not.toContain('&');
|
|
});
|
|
|
|
it('replaces all occurrences of the same mention', () => {
|
|
const mentions: MentionDTO[] = [{ id: 'uuid-1', firstName: 'Hans', lastName: 'Müller' }];
|
|
const result = renderBody('@Hans Müller and @Hans Müller', mentions);
|
|
const spanCount = (result.match(/<span /g) ?? []).length;
|
|
expect(spanCount).toBe(2);
|
|
});
|
|
|
|
it('escapes HTML special chars in mention display names', () => {
|
|
const mentions: MentionDTO[] = [{ id: 'u1', firstName: '<script>', lastName: 'alert(1)' }];
|
|
const result = renderBody('@<script> alert(1)', mentions);
|
|
expect(result).not.toContain('<script>');
|
|
expect(result).toContain('<script>');
|
|
});
|
|
|
|
it('converts newlines to <br>', () => {
|
|
const result = renderBody('line1\nline2', []);
|
|
expect(result).toContain('<br>');
|
|
expect(result).not.toContain('\n');
|
|
});
|
|
});
|