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 });
});
});
});