diff --git a/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.integration.test.ts b/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.integration.test.ts
deleted file mode 100644
index b824ed7a..00000000
--- a/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.integration.test.ts
+++ /dev/null
@@ -1,381 +0,0 @@
-import {
- HighlightCardContent,
- TextQuoteSelector,
- RangeSelector,
- HighlightSelector,
-} from '../../../../domain/value-objects/content/HighlightCardContent';
-import { JSDOM } from 'jsdom';
-
-describe('HighlightCardContent Integration Tests', () => {
- describe('HTML document annotation (browser extension simulation)', () => {
- let dom: JSDOM;
- let document: Document;
-
- beforeEach(() => {
- // Create a realistic HTML document for testing
- const htmlContent = `
-
-
-
- Test Article
-
-
-
- The Future of Web Annotations
- Web annotations are becoming increasingly important for collaborative research and knowledge sharing. They allow users to highlight, comment, and share insights directly on web content.
- Modern annotation systems use multiple selector types to ensure robust anchoring across different browsers and document changes. This includes text quote selectors, position selectors, and range selectors.
-
- "The ability to annotate any web content opens up new possibilities for distributed collaboration and peer review."
-
- Browser extensions can capture user selections and generate these selectors automatically, making the annotation process seamless for end users.
-
-
-
- `;
-
- dom = new JSDOM(htmlContent);
- document = dom.window.document;
-
- // Set up global document for JSDOM
- global.document = document;
- global.window = dom.window as any;
- global.Node = dom.window.Node;
- });
-
- afterEach(() => {
- dom.window.close();
- });
-
- // Helper function to simulate text selection in browser
- function simulateTextSelection(
- startNode: Node,
- startOffset: number,
- endNode: Node,
- endOffset: number,
- ) {
- const range = document.createRange();
- range.setStart(startNode, startOffset);
- range.setEnd(endNode, endOffset);
- return range;
- }
-
- // Helper function to get text content with position
- function getDocumentTextContent(): string {
- return document.body.textContent || '';
- }
-
- // Helper function to find text position in document
- function findTextPosition(
- searchText: string,
- documentText: string,
- ): { start: number; end: number } | null {
- const start = documentText.indexOf(searchText);
- if (start === -1) return null;
- return { start, end: start + searchText.length };
- }
-
- // Helper function to generate XPath for an element
- function getXPath(element: Node): string {
- const { Node } = dom.window;
-
- if (element.nodeType === Node.TEXT_NODE) {
- const parent = element.parentNode;
- if (!parent) return '';
- const textNodes = Array.from(parent.childNodes).filter(
- (n) => n.nodeType === Node.TEXT_NODE,
- );
- const index = textNodes.indexOf(element as Text);
- return `${getXPath(parent)}/text()[${index + 1}]`;
- }
-
- if (element.nodeType === Node.ELEMENT_NODE) {
- const elem = element as Element;
- if (elem.id) {
- return `//*[@id="${elem.id}"]`;
- }
-
- const parent = elem.parentNode;
- if (!parent || parent.nodeType !== Node.ELEMENT_NODE) {
- return `/${elem.tagName.toLowerCase()}`;
- }
-
- const siblings = Array.from(parent.children).filter(
- (e) => e.tagName === elem.tagName,
- );
- const index = siblings.indexOf(elem);
- return `${getXPath(parent)}/${elem.tagName.toLowerCase()}[${index + 1}]`;
- }
-
- return '';
- }
-
- it('should create highlight from browser text selection', () => {
- // Simulate user selecting text in the first paragraph
- const firstParagraph = document.querySelector('p');
- expect(firstParagraph).toBeTruthy();
-
- const textNode = firstParagraph!.firstChild as Text;
- const selectedText =
- 'Web annotations are becoming increasingly important';
-
- // Create a range selection (simulating browser selection)
- const range = simulateTextSelection(
- textNode,
- 0,
- textNode,
- selectedText.length,
- );
- const rangeText = range.toString();
-
- expect(rangeText).toBe(selectedText);
-
- // Get document text for position calculation
- const documentText = getDocumentTextContent();
- const position = findTextPosition(selectedText, documentText);
- expect(position).toBeTruthy();
-
- // Generate selectors as a browser extension would
- const textQuoteSelector: TextQuoteSelector = {
- type: 'TextQuoteSelector',
- exact: selectedText,
- prefix: '',
- suffix: ' for collaborative research',
- };
-
- const textPositionSelector = {
- type: 'TextPositionSelector' as const,
- start: position!.start,
- end: position!.end,
- };
-
- const rangeSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: getXPath(range.startContainer),
- startOffset: range.startOffset,
- endContainer: getXPath(range.endContainer),
- endOffset: range.endOffset,
- };
-
- // Create highlight content with all selectors
- const result = HighlightCardContent.create(
- selectedText,
- [textQuoteSelector, textPositionSelector, rangeSelector],
- {
- documentUrl: 'https://example.com/article',
- documentTitle: document.title,
- context: firstParagraph!.textContent || undefined,
- },
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.text).toBe(selectedText);
- expect(content.documentTitle).toBe('Test Article');
- expect(content.selectors).toHaveLength(3);
- });
-
- it('should handle cross-element text selection', () => {
- // Simulate selecting text that spans multiple elements
- const firstP = document.querySelector('p');
- const secondP = document.querySelectorAll('p')[1];
-
- expect(firstP && secondP).toBeTruthy();
-
- const startText = firstP!.firstChild as Text;
- const endText = secondP!.firstChild as Text;
-
- // Select from end of first paragraph to start of second
- const startOffset = startText.textContent!.length - 20; // Last 20 chars of first p
- const endOffset = 30; // First 30 chars of second p
-
- const range = simulateTextSelection(
- startText,
- startOffset,
- endText,
- endOffset,
- );
- const selectedText = range.toString();
-
- // This should create a selection spanning elements
- expect(selectedText.length).toBeGreaterThanOrEqual(0);
-
- const rangeSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: getXPath(startText),
- startOffset: startOffset,
- endContainer: getXPath(endText),
- endOffset: endOffset,
- };
-
- const result = HighlightCardContent.create(selectedText, [rangeSelector]);
- expect(result.isOk()).toBe(true);
- });
-
- it('should handle blockquote selection', () => {
- const blockquote = document.querySelector('blockquote');
- expect(blockquote).toBeTruthy();
-
- const quoteText = blockquote!.textContent!.trim();
- const selectedPortion = 'annotate any web content';
-
- const textNode = blockquote!.firstChild as Text;
- const startOffset = quoteText.indexOf(selectedPortion);
- const endOffset = startOffset + selectedPortion.length;
-
- const range = simulateTextSelection(
- textNode,
- startOffset,
- textNode,
- endOffset,
- );
-
- const textQuoteSelector: TextQuoteSelector = {
- type: 'TextQuoteSelector',
- exact: selectedPortion,
- prefix: 'The ability to ',
- suffix: ' opens up new',
- };
-
- const rangeSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: getXPath(textNode),
- startOffset: startOffset,
- endContainer: getXPath(textNode),
- endOffset: endOffset,
- };
-
- const result = HighlightCardContent.create(
- selectedPortion,
- [textQuoteSelector, rangeSelector],
- {
- context: quoteText,
- documentUrl: 'https://example.com/article',
- },
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.context).toBe(quoteText);
- expect(content.getTextQuoteSelector()?.prefix).toBe('The ability to ');
- });
-
- it('should generate robust selectors for annotation recovery', () => {
- // Test that we can "find" an annotation again using the selectors
- const targetText = 'multiple selector types';
- const paragraph = document.querySelectorAll('p')[1];
- const fullText = paragraph!.textContent!;
-
- // Get document text for position calculation
- const documentText = getDocumentTextContent();
- const globalPosition = findTextPosition(targetText, documentText);
- expect(globalPosition).toBeTruthy();
-
- // Generate selectors as if creating the annotation
- const exactMatch = targetText;
- const globalStartIndex = globalPosition!.start;
-
- // Extract prefix and suffix from the global document text
- const prefixLength = 20;
- const suffixLength = 20;
-
- const prefix = documentText.substring(
- Math.max(0, globalStartIndex - prefixLength),
- globalStartIndex,
- );
- const suffix = documentText.substring(
- globalStartIndex + targetText.length,
- globalStartIndex + targetText.length + suffixLength,
- );
-
- const selectors: HighlightSelector[] = [
- {
- type: 'TextQuoteSelector',
- exact: exactMatch,
- prefix: prefix.trim(),
- suffix: suffix.trim(),
- },
- {
- type: 'TextPositionSelector',
- start: globalPosition!.start,
- end: globalPosition!.end,
- },
- ];
-
- const result = HighlightCardContent.create(targetText, selectors);
- expect(result.isOk()).toBe(true);
-
- const content = result.unwrap();
-
- // Simulate "finding" the annotation again using TextQuoteSelector
- const textQuoteSelector = content.getTextQuoteSelector()!;
-
- // Check if we can find the exact text in the document
- const exactTextIndex = documentText.indexOf(textQuoteSelector.exact);
- expect(exactTextIndex).toBeGreaterThanOrEqual(-1);
-
- // Verify the prefix and suffix match what we extracted
- const actualPrefix = documentText.substring(
- exactTextIndex - textQuoteSelector.prefix!.length - 1,
- exactTextIndex - 1,
- );
- const actualSuffix = documentText.substring(
- exactTextIndex + textQuoteSelector.exact.length + 1,
- exactTextIndex +
- textQuoteSelector.exact.length +
- textQuoteSelector.suffix!.length +
- 1,
- );
-
- expect(actualPrefix).toBe(textQuoteSelector.prefix);
- expect(actualSuffix).toBe(textQuoteSelector.suffix);
-
- // Verify position selector still works
- const positionSelector = content.getTextPositionSelector()!;
- const extractedText = documentText.substring(
- positionSelector.start,
- positionSelector.end,
- );
- expect(extractedText).toBe(targetText);
- });
-
- it('should handle edge cases in HTML documents', () => {
- // Test selection at document boundaries
- const title = document.querySelector('h1');
- const titleText = title!.textContent!;
-
- // Select entire title
- const range = simulateTextSelection(
- title!.firstChild!,
- 0,
- title!.firstChild!,
- titleText.length,
- );
-
- const selectors: HighlightSelector[] = [
- {
- type: 'TextQuoteSelector',
- exact: titleText,
- prefix: '',
- suffix: '',
- },
- {
- type: 'RangeSelector',
- startContainer: getXPath(title!.firstChild!),
- startOffset: 0,
- endContainer: getXPath(title!.firstChild!),
- endOffset: titleText.length,
- },
- ];
-
- const result = HighlightCardContent.create(titleText, selectors, {
- documentTitle: document.title,
- });
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.text).toBe(titleText);
- expect(content.getTextQuoteSelector()?.prefix).toBe('');
- expect(content.getTextQuoteSelector()?.suffix).toBe('');
- });
- });
-});
diff --git a/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.test.ts b/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.test.ts
deleted file mode 100644
index fc06087e..00000000
--- a/src/modules/cards/tests/domain/value-objects/content/HighlightCardContent.test.ts
+++ /dev/null
@@ -1,415 +0,0 @@
-import {
- HighlightCardContent,
- HighlightCardContentValidationError,
- TextQuoteSelector,
- TextPositionSelector,
- RangeSelector,
- HighlightSelector,
-} from '../../../../domain/value-objects/content/HighlightCardContent';
-import { CardTypeEnum } from '../../../../domain/value-objects/CardType';
-import { Err } from 'src/shared/core/Result';
-
-describe('HighlightCardContent', () => {
- const validTextQuoteSelector: TextQuoteSelector = {
- type: 'TextQuoteSelector',
- exact: 'highlighted text',
- prefix: 'some ',
- suffix: ' here',
- };
-
- const validTextPositionSelector: TextPositionSelector = {
- type: 'TextPositionSelector',
- start: 100,
- end: 116,
- };
-
- const validRangeSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: '/html/body/p[1]',
- startOffset: 5,
- endContainer: '/html/body/p[1]',
- endOffset: 21,
- };
-
- describe('creation', () => {
- it('should create highlight content with single selector', () => {
- const result = HighlightCardContent.create('highlighted text', [
- validTextQuoteSelector,
- ]);
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.type).toBe(CardTypeEnum.HIGHLIGHT);
- expect(content.text).toBe('highlighted text');
- expect(content.selectors).toHaveLength(1);
- expect(content.selectors[0]).toEqual(validTextQuoteSelector);
- });
-
- it('should create highlight content with multiple selectors', () => {
- const selectors = [
- validTextQuoteSelector,
- validTextPositionSelector,
- validRangeSelector,
- ];
- const result = HighlightCardContent.create('highlighted text', selectors);
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.selectors).toHaveLength(3);
- expect(content.selectors).toEqual(selectors);
- });
-
- it('should create highlight content with options', () => {
- const result = HighlightCardContent.create(
- 'highlighted text',
- [validTextQuoteSelector],
- {
- context: 'This is some highlighted text here for context',
- documentUrl: 'https://example.com/article',
- documentTitle: 'Example Article',
- },
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.context).toBe(
- 'This is some highlighted text here for context',
- );
- expect(content.documentUrl).toBe('https://example.com/article');
- expect(content.documentTitle).toBe('Example Article');
- });
-
- it('should trim whitespace from text and options', () => {
- const result = HighlightCardContent.create(
- ' highlighted text ',
- [validTextQuoteSelector],
- {
- context: ' context ',
- documentUrl: ' https://example.com ',
- documentTitle: ' Title ',
- },
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.text).toBe('highlighted text');
- expect(content.context).toBe('context');
- expect(content.documentUrl).toBe('https://example.com');
- expect(content.documentTitle).toBe('Title');
- });
-
- it('should reject empty text', () => {
- const result = HighlightCardContent.create('', [validTextQuoteSelector]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error,
- ).toBeInstanceOf(HighlightCardContentValidationError);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('Highlight text cannot be empty');
- });
-
- it('should reject whitespace-only text', () => {
- const result = HighlightCardContent.create(' ', [
- validTextQuoteSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('Highlight text cannot be empty');
- });
-
- it('should reject text exceeding max length', () => {
- const longText = 'a'.repeat(HighlightCardContent.MAX_TEXT_LENGTH + 1);
- const result = HighlightCardContent.create(longText, [
- validTextQuoteSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe(
- `Highlight text cannot exceed ${HighlightCardContent.MAX_TEXT_LENGTH} characters`,
- );
- });
-
- it('should reject empty selectors array', () => {
- const result = HighlightCardContent.create('highlighted text', []);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('Highlight must have at least one selector');
- });
- });
-
- describe('selector validation', () => {
- it('should reject TextQuoteSelector with empty exact text', () => {
- const invalidSelector: TextQuoteSelector = {
- type: 'TextQuoteSelector',
- exact: '',
- };
-
- const result = HighlightCardContent.create('highlighted text', [
- invalidSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('TextQuoteSelector must have exact text');
- });
-
- it('should reject TextPositionSelector with invalid positions', () => {
- const invalidSelector: TextPositionSelector = {
- type: 'TextPositionSelector',
- start: 100,
- end: 50, // end before start
- };
-
- const result = HighlightCardContent.create('highlighted text', [
- invalidSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('TextPositionSelector must have valid start/end positions');
- });
-
- it('should reject TextPositionSelector with negative positions', () => {
- const invalidSelector: TextPositionSelector = {
- type: 'TextPositionSelector',
- start: -1,
- end: 10,
- };
-
- const result = HighlightCardContent.create('highlighted text', [
- invalidSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('TextPositionSelector must have valid start/end positions');
- });
-
- it('should reject RangeSelector with missing containers', () => {
- const invalidSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: '',
- startOffset: 0,
- endContainer: '/html/body/p[1]',
- endOffset: 10,
- };
-
- const result = HighlightCardContent.create('highlighted text', [
- invalidSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('RangeSelector must have start and end containers');
- });
-
- it('should reject RangeSelector with negative offsets', () => {
- const invalidSelector: RangeSelector = {
- type: 'RangeSelector',
- startContainer: '/html/body/p[1]',
- startOffset: -1,
- endContainer: '/html/body/p[1]',
- endOffset: 10,
- };
-
- const result = HighlightCardContent.create('highlighted text', [
- invalidSelector,
- ]);
-
- expect(result.isErr()).toBe(true);
- expect(
- (
- result as Err<
- HighlightCardContent,
- HighlightCardContentValidationError
- >
- ).error.message,
- ).toBe('RangeSelector offsets must be non-negative');
- });
- });
-
- describe('selector helper methods', () => {
- it('should find TextQuoteSelector', () => {
- const selectors = [validTextQuoteSelector, validTextPositionSelector];
- const content = HighlightCardContent.create(
- 'highlighted text',
- selectors,
- ).unwrap();
-
- const textQuoteSelector = content.getTextQuoteSelector();
- expect(textQuoteSelector).toEqual(validTextQuoteSelector);
- });
-
- it('should find TextPositionSelector', () => {
- const selectors = [validTextQuoteSelector, validTextPositionSelector];
- const content = HighlightCardContent.create(
- 'highlighted text',
- selectors,
- ).unwrap();
-
- const textPositionSelector = content.getTextPositionSelector();
- expect(textPositionSelector).toEqual(validTextPositionSelector);
- });
-
- it('should find RangeSelector', () => {
- const selectors = [validTextQuoteSelector, validRangeSelector];
- const content = HighlightCardContent.create(
- 'highlighted text',
- selectors,
- ).unwrap();
-
- const rangeSelector = content.getRangeSelector();
- expect(rangeSelector).toEqual(validRangeSelector);
- });
-
- it('should return undefined when selector type not found', () => {
- const content = HighlightCardContent.create('highlighted text', [
- validTextQuoteSelector,
- ]).unwrap();
-
- expect(content.getTextPositionSelector()).toBeUndefined();
- expect(content.getRangeSelector()).toBeUndefined();
- });
- });
-
- describe('createWithHypothesisSelectors', () => {
- it('should create highlight with Hypothes.is-style selectors', () => {
- const result = HighlightCardContent.createWithHypothesisSelectors(
- 'highlighted text',
- 'highlighted text',
- 'some ',
- ' here',
- 100,
- 116,
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.selectors).toHaveLength(2);
-
- const textQuoteSelector = content.getTextQuoteSelector();
- expect(textQuoteSelector).toEqual({
- type: 'TextQuoteSelector',
- exact: 'highlighted text',
- prefix: 'some ',
- suffix: ' here',
- });
-
- const textPositionSelector = content.getTextPositionSelector();
- expect(textPositionSelector).toEqual({
- type: 'TextPositionSelector',
- start: 100,
- end: 116,
- });
- });
-
- it('should create highlight with range selector when provided', () => {
- const result = HighlightCardContent.createWithHypothesisSelectors(
- 'highlighted text',
- 'highlighted text',
- 'some ',
- ' here',
- 100,
- 116,
- {
- rangeSelector: {
- startContainer: '/html/body/p[1]',
- startOffset: 5,
- endContainer: '/html/body/p[1]',
- endOffset: 21,
- },
- },
- );
-
- expect(result.isOk()).toBe(true);
- const content = result.unwrap();
- expect(content.selectors).toHaveLength(3);
-
- const rangeSelector = content.getRangeSelector();
- expect(rangeSelector).toEqual({
- type: 'RangeSelector',
- startContainer: '/html/body/p[1]',
- startOffset: 5,
- endContainer: '/html/body/p[1]',
- endOffset: 21,
- });
- });
- });
-
- describe('immutability', () => {
- it('should return defensive copy of selectors', () => {
- const originalSelectors = [
- validTextQuoteSelector,
- validTextPositionSelector,
- ];
- const content = HighlightCardContent.create(
- 'highlighted text',
- originalSelectors,
- ).unwrap();
-
- const returnedSelectors = content.selectors;
- returnedSelectors.push(validRangeSelector);
-
- // Original content should not be affected
- expect(content.selectors).toHaveLength(2);
- expect(returnedSelectors).toHaveLength(3);
- });
- });
-});
diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js
deleted file mode 100644
index baec5b10..00000000
--- a/src/scripts/migrate.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// Simple placeholder for database migrations
-console.log('Running database migrations...');
-console.log('Database migrations completed successfully.');