AcadeDoc/apps/extension-clipper/tests/html-extractor.test.ts
Corentin 23a85267bf feat(acadenice): add sync blocks for cross-page content sharing — R4.2
Implements Notion-style sync blocks: a Tiptap node whose content is shared
across N pages. Editing via the Hocuspocus overlay propagates to all instances
via Yjs collab + SSE broadcast (EventEmitter2 bus).

Server: DB migration, NestJS module (CRUD + BFS cycle detection + broadcast),
Hocuspocus persistence extension extended for sync-block-* docs, 3 new RBAC
permissions (sync_blocks:create/edit/delete), seeded to Admin/Editor/Member.

Client: SyncBlockExtension (Tiptap node), SyncBlockNodeView (NodeView +
Mantine Modal overlay + SSE hook), /sync-block slash command, service client.

Tests: 32 server Jest + 18 client Vitest, all green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:40:12 +02:00

102 lines
2.8 KiB
TypeScript

/**
* Tests for html-extractor.ts helpers.
*
* DOMPurify is mocked to return input unchanged so we can test the surrounding
* logic without needing jsdom + DOMPurify integration.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock DOMPurify before importing the module under test.
vi.mock('dompurify', () => ({
default: {
sanitize: (html: string, _opts: unknown) => html,
},
}));
// Mock browser APIs used by html-extractor (getPageUrl uses querySelector).
const linkMock = { href: '' };
Object.defineProperty(global, 'document', {
value: {
title: 'Test Page Title',
querySelector: vi.fn().mockReturnValue(null),
createElement: (tag: string) => ({
innerHTML: '',
appendChild: function (node: any) {
this.innerHTML += node.innerHTML ?? '';
},
}),
},
writable: true,
});
Object.defineProperty(global, 'window', {
value: {
location: { href: 'https://example.com/path?q=1' },
getSelection: vi.fn(),
},
writable: true,
});
import {
sanitizeHtml,
getPageTitle,
getPageUrl,
extractSelection,
} from '../src/lib/html-extractor';
describe('sanitizeHtml', () => {
it('passes through safe HTML (mocked DOMPurify)', () => {
const safe = '<p>Hello <strong>world</strong></p>';
expect(sanitizeHtml(safe)).toBe(safe);
});
it('handles empty string', () => {
expect(sanitizeHtml('')).toBe('');
});
it('handles string with only whitespace', () => {
expect(sanitizeHtml(' ')).toBe(' ');
});
});
describe('getPageTitle', () => {
it('returns trimmed document.title', () => {
(global as any).document.title = ' My Article ';
expect(getPageTitle()).toBe('My Article');
});
it('returns empty string when document.title is blank', () => {
(global as any).document.title = ' ';
expect(getPageTitle()).toBe('');
});
});
describe('getPageUrl', () => {
it('returns location.href when no canonical link', () => {
(global as any).document.querySelector = vi.fn().mockReturnValue(null);
expect(getPageUrl()).toBe('https://example.com/path?q=1');
});
it('returns canonical href when present', () => {
(global as any).document.querySelector = vi.fn().mockReturnValue({
href: 'https://example.com/canonical',
});
expect(getPageUrl()).toBe('https://example.com/canonical');
});
});
describe('extractSelection', () => {
it('returns empty string when selection is collapsed', () => {
(global as any).window.getSelection = vi.fn().mockReturnValue({
isCollapsed: true,
rangeCount: 0,
});
expect(extractSelection()).toBe('');
});
it('returns empty string when getSelection returns null', () => {
(global as any).window.getSelection = vi.fn().mockReturnValue(null);
expect(extractSelection()).toBe('');
});
});