import { describe, it, expect } from 'vitest';
import { buildInsertTableWithDataRequests } from './insertTableWithData.js';
describe('buildInsertTableWithDataRequests', () => {
describe('table structure', () => {
it('should create an insertTable request with correct dimensions', () => {
const requests = buildInsertTableWithDataRequests(
[
['A', 'B'],
['C', 'D'],
],
1,
false
);
const tableReq = requests.find((r) => r.insertTable);
expect(tableReq).toBeDefined();
expect(tableReq!.insertTable!.rows).toBe(2);
expect(tableReq!.insertTable!.columns).toBe(2);
expect(tableReq!.insertTable!.location!.index).toBe(1);
});
it('should create a single-cell table', () => {
const requests = buildInsertTableWithDataRequests([['Only']], 1, false);
const tableReq = requests.find((r) => r.insertTable);
expect(tableReq!.insertTable!.rows).toBe(1);
expect(tableReq!.insertTable!.columns).toBe(1);
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(1);
expect(insertTexts[0].insertText!.text).toBe('Only');
});
it('should pad ragged rows to the widest row', () => {
const requests = buildInsertTableWithDataRequests([['A', 'B', 'C'], ['D']], 1, false);
const tableReq = requests.find((r) => r.insertTable);
expect(tableReq!.insertTable!.rows).toBe(2);
expect(tableReq!.insertTable!.columns).toBe(3);
// Only non-empty cells get insertText: A, B, C, D
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(4);
});
});
describe('cell index math', () => {
// For a 2x2 table at T=1, numCols=2:
// baseCellIndex(1, r, c, 2) = 1 + 4 + r * (1 + 4) + 2*c = 5 + 5r + 2c
// cell(0,0) base = 5, cell(0,1) base = 7
// cell(1,0) base = 10, cell(1,1) base = 12
it('should insert text at correct indices for a 2x2 table', () => {
const requests = buildInsertTableWithDataRequests(
[
['A', 'B'],
['C', 'D'],
],
1,
false
);
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(4);
// cell(0,0): base=5, cumulative=0 → index=5
expect(insertTexts[0].insertText!.text).toBe('A');
expect(insertTexts[0].insertText!.location!.index).toBe(5);
// cell(0,1): base=7, cumulative=1 (len of "A") → index=8
expect(insertTexts[1].insertText!.text).toBe('B');
expect(insertTexts[1].insertText!.location!.index).toBe(8);
// cell(1,0): base=10, cumulative=2 (len of "A"+"B") → index=12
expect(insertTexts[2].insertText!.text).toBe('C');
expect(insertTexts[2].insertText!.location!.index).toBe(12);
// cell(1,1): base=12, cumulative=3 (len of "A"+"B"+"C") → index=15
expect(insertTexts[3].insertText!.text).toBe('D');
expect(insertTexts[3].insertText!.location!.index).toBe(15);
});
it('should handle multi-character cell text correctly', () => {
const requests = buildInsertTableWithDataRequests(
[
['Hello', 'World'],
['Foo', 'Bar'],
],
1,
false
);
const insertTexts = requests.filter((r) => r.insertText);
// cell(0,0): base=5, cumulative=0 → 5
expect(insertTexts[0].insertText!.location!.index).toBe(5);
// cell(0,1): base=7, cumulative=5 ("Hello") → 12
expect(insertTexts[1].insertText!.location!.index).toBe(12);
// cell(1,0): base=10, cumulative=10 ("Hello"+"World") → 20
expect(insertTexts[2].insertText!.location!.index).toBe(20);
// cell(1,1): base=12, cumulative=13 ("Hello"+"World"+"Foo") → 25
expect(insertTexts[3].insertText!.location!.index).toBe(25);
});
it('should work with a different starting index', () => {
const requests = buildInsertTableWithDataRequests([['X']], 50, false);
const tableReq = requests.find((r) => r.insertTable);
expect(tableReq!.insertTable!.location!.index).toBe(50);
const insertTexts = requests.filter((r) => r.insertText);
// cell(0,0): 50 + 4 + 0 + 0 = 54
expect(insertTexts[0].insertText!.location!.index).toBe(54);
});
it('should handle a 3x3 table correctly', () => {
const requests = buildInsertTableWithDataRequests(
[
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i'],
],
1,
false
);
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(9);
// For 3x3 at T=1: baseCellIndex = 1 + 4 + r*(1+6) + 2c = 5 + 7r + 2c
// row 0: bases = 5, 7, 9
// row 1: bases = 12, 14, 16
// row 2: bases = 19, 21, 23
// cell(0,0): base=5, cum=0 → 5
expect(insertTexts[0].insertText!.location!.index).toBe(5);
// cell(0,1): base=7, cum=1 → 8
expect(insertTexts[1].insertText!.location!.index).toBe(8);
// cell(0,2): base=9, cum=2 → 11
expect(insertTexts[2].insertText!.location!.index).toBe(11);
// cell(1,0): base=12, cum=3 → 15
expect(insertTexts[3].insertText!.location!.index).toBe(15);
// cell(1,1): base=14, cum=4 → 18
expect(insertTexts[4].insertText!.location!.index).toBe(18);
// cell(1,2): base=16, cum=5 → 21
expect(insertTexts[5].insertText!.location!.index).toBe(21);
// cell(2,0): base=19, cum=6 → 25
expect(insertTexts[6].insertText!.location!.index).toBe(25);
// cell(2,1): base=21, cum=7 → 28
expect(insertTexts[7].insertText!.location!.index).toBe(28);
// cell(2,2): base=23, cum=8 → 31
expect(insertTexts[8].insertText!.location!.index).toBe(31);
});
});
describe('empty cells', () => {
it('should skip insertText for empty string cells', () => {
const requests = buildInsertTableWithDataRequests(
[
['A', ''],
['', 'D'],
],
1,
false
);
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(2);
expect(insertTexts[0].insertText!.text).toBe('A');
expect(insertTexts[1].insertText!.text).toBe('D');
});
it('should compute correct indices when cells are skipped', () => {
// 2x2 at T=1, but cell(0,1) is empty
const requests = buildInsertTableWithDataRequests(
[
['A', ''],
['C', 'D'],
],
1,
false
);
const insertTexts = requests.filter((r) => r.insertText);
expect(insertTexts).toHaveLength(3);
// cell(0,0): base=5, cum=0 → 5
expect(insertTexts[0].insertText!.location!.index).toBe(5);
// cell(0,1) skipped
// cell(1,0): base=10, cum=1 ("A") → 11
expect(insertTexts[1].insertText!.location!.index).toBe(11);
// cell(1,1): base=12, cum=2 ("A"+"C") → 14
expect(insertTexts[2].insertText!.location!.index).toBe(14);
});
});
describe('header row bolding', () => {
it('should add updateTextStyle requests for header cells when hasHeaderRow=true', () => {
const requests = buildInsertTableWithDataRequests(
[
['Name', 'Age'],
['Alice', '30'],
],
1,
true
);
const styleReqs = requests.filter((r) => r.updateTextStyle);
expect(styleReqs).toHaveLength(2);
// "Name" bold: range starts at 5 (base for cell(0,0), cum=0), ends at 5+4=9
expect(styleReqs[0].updateTextStyle!.textStyle!.bold).toBe(true);
expect(styleReqs[0].updateTextStyle!.range!.startIndex).toBe(5);
expect(styleReqs[0].updateTextStyle!.range!.endIndex).toBe(9);
// "Age" bold: base for cell(0,1)=7, cum=4 ("Name") → 11, ends at 11+3=14
expect(styleReqs[1].updateTextStyle!.textStyle!.bold).toBe(true);
expect(styleReqs[1].updateTextStyle!.range!.startIndex).toBe(11);
expect(styleReqs[1].updateTextStyle!.range!.endIndex).toBe(14);
});
it('should not add style requests when hasHeaderRow=false', () => {
const requests = buildInsertTableWithDataRequests(
[
['Name', 'Age'],
['Alice', '30'],
],
1,
false
);
const styleReqs = requests.filter((r) => r.updateTextStyle);
expect(styleReqs).toHaveLength(0);
});
it('should not bold empty header cells', () => {
const requests = buildInsertTableWithDataRequests(
[
['Name', ''],
['Alice', '30'],
],
1,
true
);
const styleReqs = requests.filter((r) => r.updateTextStyle);
expect(styleReqs).toHaveLength(1);
expect(styleReqs[0].updateTextStyle!.range!.startIndex).toBe(5);
});
it('should not bold non-header rows', () => {
const requests = buildInsertTableWithDataRequests(
[
['H1', 'H2'],
['A', 'B'],
['C', 'D'],
],
1,
true
);
const styleReqs = requests.filter((r) => r.updateTextStyle);
// Only 2 bold requests (for H1 and H2), not for A, B, C, D
expect(styleReqs).toHaveLength(2);
});
});
describe('tabId propagation', () => {
it('should include tabId in all request locations when provided', () => {
const tabId = 'tab123';
const requests = buildInsertTableWithDataRequests(
[
['A', 'B'],
['C', 'D'],
],
1,
true,
tabId
);
const tableReq = requests.find((r) => r.insertTable);
expect((tableReq!.insertTable!.location as any).tabId).toBe(tabId);
const insertTexts = requests.filter((r) => r.insertText);
for (const req of insertTexts) {
expect((req.insertText!.location as any).tabId).toBe(tabId);
}
const styleReqs = requests.filter((r) => r.updateTextStyle);
for (const req of styleReqs) {
expect(req.updateTextStyle!.range!.tabId).toBe(tabId);
}
});
it('should not include tabId when not provided', () => {
const requests = buildInsertTableWithDataRequests([['A']], 1, false);
const tableReq = requests.find((r) => r.insertTable);
expect((tableReq!.insertTable!.location as any).tabId).toBeUndefined();
const insertTexts = requests.filter((r) => r.insertText);
expect((insertTexts[0].insertText!.location as any).tabId).toBeUndefined();
});
});
describe('error handling', () => {
it('should throw on empty data array', () => {
expect(() => buildInsertTableWithDataRequests([], 1, false)).toThrow();
});
it('should throw on data with only empty rows', () => {
expect(() => buildInsertTableWithDataRequests([[]], 1, false)).toThrow();
});
});
});