Spaces:
Sleeping
Sleeping
| import { describe, it, expect, vi } from 'vitest'; | |
| import { findTextRange, getTableCellRange, getParagraphRange } from './googleDocsApiHelpers.js'; | |
| describe('Text Range Finding', () => { | |
| describe('findTextRange', () => { | |
| it('should find text within a single text run correctly', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| textRun: { | |
| content: 'This is a test sentence.', | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| const result = await findTextRange(mockDocs as any, 'doc123', 'test', 1); | |
| expect(result).toEqual({ startIndex: 11, endIndex: 15 }); | |
| expect(mockDocs.documents.get).toHaveBeenCalledOnce(); | |
| expect(mockDocs.documents.get).toHaveBeenCalledWith({ | |
| documentId: 'doc123', | |
| fields: | |
| 'body(content(paragraph(elements(startIndex,endIndex,textRun(content))),table,sectionBreak,tableOfContents,startIndex,endIndex))', | |
| }); | |
| }); | |
| it('should find the nth instance of text correctly', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 41, | |
| textRun: { | |
| content: 'Test test test. This is a test sentence.', | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| const result = await findTextRange(mockDocs as any, 'doc123', 'test', 3); | |
| expect(result).toEqual({ startIndex: 27, endIndex: 31 }); | |
| }); | |
| it('should return null if text is not found', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| textRun: { | |
| content: 'This is a sample sentence.', | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| const result = await findTextRange(mockDocs as any, 'doc123', 'test', 1); | |
| expect(result).toBeNull(); | |
| }); | |
| it('should find text in a specific tab when tabId is provided', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| tabs: [ | |
| { | |
| tabProperties: { tabId: 'tab1' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 20, | |
| textRun: { content: 'Tab 1 content here' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| { | |
| tabProperties: { tabId: 'tab2' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| textRun: { content: 'Meeting Notes are here.' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| ], | |
| }, | |
| })), | |
| }, | |
| }; | |
| // Search in tab2 where "Meeting Notes" exists | |
| const result = await findTextRange(mockDocs as any, 'doc123', 'Meeting Notes', 1, 'tab2'); | |
| expect(result).toEqual({ startIndex: 1, endIndex: 14 }); | |
| // Verify includeTabsContent was used | |
| expect(mockDocs.documents.get).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| documentId: 'doc123', | |
| includeTabsContent: true, | |
| }) | |
| ); | |
| }); | |
| it('should throw UserError when tabId is not found', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| tabs: [ | |
| { | |
| tabProperties: { tabId: 'tab1' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 10, | |
| textRun: { content: 'Some text' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| ], | |
| }, | |
| })), | |
| }, | |
| }; | |
| await expect( | |
| findTextRange(mockDocs as any, 'doc123', 'test', 1, 'nonexistent') | |
| ).rejects.toThrow('Tab with ID "nonexistent" not found in document.'); | |
| }); | |
| it('should not use includeTabsContent when no tabId is provided', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| textRun: { content: 'This is a test sentence.' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| await findTextRange(mockDocs as any, 'doc123', 'test', 1); | |
| // Should NOT include includeTabsContent | |
| expect(mockDocs.documents.get).toHaveBeenCalledWith( | |
| expect.not.objectContaining({ | |
| includeTabsContent: true, | |
| }) | |
| ); | |
| }); | |
| it('should handle text spanning multiple text runs', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 6, | |
| textRun: { content: 'This ' }, | |
| }, | |
| { | |
| startIndex: 6, | |
| endIndex: 11, | |
| textRun: { content: 'is a ' }, | |
| }, | |
| { | |
| startIndex: 11, | |
| endIndex: 20, | |
| textRun: { content: 'test case' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| const result = await findTextRange(mockDocs as any, 'doc123', 'a test', 1); | |
| expect(result).toEqual({ startIndex: 9, endIndex: 15 }); | |
| }); | |
| }); | |
| }); | |
| describe('Paragraph Range Finding', () => { | |
| describe('getParagraphRange', () => { | |
| it('should find paragraph containing index in a specific tab', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| tabs: [ | |
| { | |
| tabProperties: { tabId: 'tab1' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| startIndex: 0, | |
| endIndex: 1, | |
| sectionBreak: {}, | |
| }, | |
| { | |
| startIndex: 1, | |
| endIndex: 20, | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 20, | |
| textRun: { content: 'First paragraph.\n' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| { | |
| tabProperties: { tabId: 'tab2' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| startIndex: 0, | |
| endIndex: 1, | |
| sectionBreak: {}, | |
| }, | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 25, | |
| textRun: { content: 'Tab 2 first paragraph.\n' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| { | |
| startIndex: 25, | |
| endIndex: 50, | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 25, | |
| endIndex: 50, | |
| textRun: { content: 'Tab 2 second paragraph.\n' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| ], | |
| }, | |
| })), | |
| }, | |
| }; | |
| // Search for paragraph containing index 30 in tab2 | |
| const result = await getParagraphRange(mockDocs as any, 'doc123', 30, 'tab2'); | |
| expect(result).toEqual({ startIndex: 25, endIndex: 50 }); | |
| // Verify includeTabsContent was used | |
| expect(mockDocs.documents.get).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| documentId: 'doc123', | |
| includeTabsContent: true, | |
| }) | |
| ); | |
| }); | |
| it('should throw UserError when tabId is not found', async () => { | |
| const mockDocs = { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| tabs: [ | |
| { | |
| tabProperties: { tabId: 'tab1' }, | |
| documentTab: { | |
| body: { | |
| content: [ | |
| { | |
| startIndex: 1, | |
| endIndex: 10, | |
| paragraph: {}, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| ], | |
| }, | |
| })), | |
| }, | |
| }; | |
| await expect(getParagraphRange(mockDocs as any, 'doc123', 5, 'nonexistent')).rejects.toThrow( | |
| 'Tab with ID "nonexistent" not found in document.' | |
| ); | |
| }); | |
| }); | |
| }); | |
| describe('Table Cell Range Finding', () => { | |
| // Helper to build a mock document with a table | |
| function buildMockDocsWithTable(tableStartIndex: number, tableRows: any[][]) { | |
| return { | |
| documents: { | |
| get: vi.fn(async () => ({ | |
| data: { | |
| body: { | |
| content: [ | |
| { | |
| startIndex: 0, | |
| endIndex: tableStartIndex, | |
| paragraph: { | |
| elements: [ | |
| { | |
| startIndex: 0, | |
| endIndex: tableStartIndex, | |
| textRun: { content: 'Before table\n' }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| { | |
| startIndex: tableStartIndex, | |
| endIndex: 200, | |
| table: { | |
| rows: tableRows.length, | |
| columns: tableRows[0]?.length ?? 0, | |
| tableRows: tableRows.map((row) => ({ | |
| tableCells: row.map((cell) => ({ | |
| content: cell.content, | |
| })), | |
| })), | |
| }, | |
| }, | |
| ], | |
| }, | |
| }, | |
| })), | |
| }, | |
| }; | |
| } | |
| describe('getTableCellRange', () => { | |
| it('should return correct range for a cell with text content', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [ | |
| { | |
| content: [ | |
| { | |
| startIndex: 16, | |
| endIndex: 26, | |
| paragraph: { | |
| elements: [{ startIndex: 16, endIndex: 26, textRun: { content: 'Cell A1\n' } }], | |
| }, | |
| }, | |
| ], | |
| }, | |
| { | |
| content: [ | |
| { | |
| startIndex: 28, | |
| endIndex: 38, | |
| paragraph: { | |
| elements: [{ startIndex: 28, endIndex: 38, textRun: { content: 'Cell B1\n' } }], | |
| }, | |
| }, | |
| ], | |
| }, | |
| ], | |
| ]); | |
| const result = await getTableCellRange(mockDocs as any, 'doc123', 14, 0, 0); | |
| // endIndex should exclude the trailing \n (26 - 1 = 25) | |
| expect(result).toEqual({ startIndex: 16, endIndex: 25 }); | |
| }); | |
| it('should return correct range for second column', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [ | |
| { | |
| content: [ | |
| { | |
| startIndex: 16, | |
| endIndex: 26, | |
| paragraph: { | |
| elements: [{ startIndex: 16, endIndex: 26, textRun: { content: 'Cell A1\n' } }], | |
| }, | |
| }, | |
| ], | |
| }, | |
| { | |
| content: [ | |
| { | |
| startIndex: 28, | |
| endIndex: 38, | |
| paragraph: { | |
| elements: [{ startIndex: 28, endIndex: 38, textRun: { content: 'Cell B1\n' } }], | |
| }, | |
| }, | |
| ], | |
| }, | |
| ], | |
| ]); | |
| const result = await getTableCellRange(mockDocs as any, 'doc123', 14, 0, 1); | |
| expect(result).toEqual({ startIndex: 28, endIndex: 37 }); | |
| }); | |
| it('should throw UserError if table not found at given startIndex', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [{ content: [{ startIndex: 16, endIndex: 20, paragraph: { elements: [] } }] }], | |
| ]); | |
| await expect(getTableCellRange(mockDocs as any, 'doc123', 999, 0, 0)).rejects.toThrow( | |
| 'No table found at startIndex 999' | |
| ); | |
| }); | |
| it('should throw UserError if row index is out of range', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [{ content: [{ startIndex: 16, endIndex: 20, paragraph: { elements: [] } }] }], | |
| ]); | |
| await expect(getTableCellRange(mockDocs as any, 'doc123', 14, 5, 0)).rejects.toThrow( | |
| 'Row index 5 is out of range' | |
| ); | |
| }); | |
| it('should throw UserError if column index is out of range', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [{ content: [{ startIndex: 16, endIndex: 20, paragraph: { elements: [] } }] }], | |
| ]); | |
| await expect(getTableCellRange(mockDocs as any, 'doc123', 14, 0, 5)).rejects.toThrow( | |
| 'Column index 5 is out of range' | |
| ); | |
| }); | |
| it('should handle cell with multiple paragraphs', async () => { | |
| const mockDocs = buildMockDocsWithTable(14, [ | |
| [ | |
| { | |
| content: [ | |
| { | |
| startIndex: 16, | |
| endIndex: 26, | |
| paragraph: { | |
| elements: [{ startIndex: 16, endIndex: 26, textRun: { content: 'Line one\n' } }], | |
| }, | |
| }, | |
| { | |
| startIndex: 26, | |
| endIndex: 36, | |
| paragraph: { | |
| elements: [{ startIndex: 26, endIndex: 36, textRun: { content: 'Line two\n' } }], | |
| }, | |
| }, | |
| ], | |
| }, | |
| ], | |
| ]); | |
| const result = await getTableCellRange(mockDocs as any, 'doc123', 14, 0, 0); | |
| // Should span from first paragraph start to last paragraph end - 1 | |
| expect(result).toEqual({ startIndex: 16, endIndex: 35 }); | |
| }); | |
| }); | |
| }); | |