feat: Start refactoring code base to be testable

Implement a spreadsheet service abstraction, GAS integration, and Jest testing setup.
This commit is contained in:
Ben Miller
2025-12-25 03:52:16 -07:00
parent 85cdfe1443
commit 3c6130778e
10 changed files with 5060 additions and 57 deletions

View File

@ -0,0 +1,113 @@
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
export class MockSpreadsheetService implements ISpreadsheetService {
// Store data as a map of sheetName -> array of rows (arrays of values)
// Row 0 is headers.
private sheets: Map<string, any[][]> = new Map();
constructor(initialData?: { [sheetName: string]: any[][] }) {
if (initialData) {
for (const [name, rows] of Object.entries(initialData)) {
this.sheets.set(name, JSON.parse(JSON.stringify(rows))); // Deep copy
}
}
}
setSheetData(sheetName: string, data: any[][]) {
this.sheets.set(sheetName, data);
}
private getSheet(sheetName: string): any[][] {
if (!this.sheets.has(sheetName)) {
// Create empty sheet with no headers if accessed but not defined?
// Or throw error to mimic GAS?
// Let's return empty array or throw.
throw new Error(`Sheet '${sheetName}' not found in mock`);
}
return this.sheets.get(sheetName)!;
}
getHeaders(sheetName: string): string[] {
const data = this.getSheet(sheetName);
if (data.length === 0) return [];
return data[0] as string[];
}
getRowData(sheetName: string, row: number): any[] {
const data = this.getSheet(sheetName);
// Row is 1-based index
if (row > data.length || row < 1) {
throw new Error(`Row ${row} out of bounds`);
}
return data[row - 1]; // Convert to 0-based
}
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
const data = this.getSheet(sheetName);
if (data.length < 2) return undefined; // Only headers or empty
const headers = data[0];
const colIndex = headers.indexOf(columnName);
if (colIndex === -1) return undefined;
for (let i = 1; i < data.length; i++) {
if (data[i][colIndex] === value) {
return i + 1; // Convert 0-based index to 1-based row number
}
}
return undefined;
}
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
const data = this.getSheet(sheetName);
const headers = data[0];
const colIndex = headers.indexOf(columnName);
if (colIndex === -1) {
throw new Error(`Column '${columnName}' not found`);
}
// Ensure row exists, extend if necessary (basic behavior)
while (data.length < row) {
data.push(new Array(headers.length).fill(""));
}
data[row - 1][colIndex] = value;
}
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
const data = this.getSheet(sheetName);
const headers = data[0];
const colIndex = headers.indexOf(columnName);
if (colIndex === -1) return null;
if (row > data.length) return null;
return data[row - 1][colIndex];
}
// Helper to store links: key = "sheetName:row:colIndex", value = url
private links: Map<string, string> = new Map();
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
const data = this.getSheet(sheetName);
const colIndex = data[0].indexOf(columnName);
if (colIndex === -1) return null;
const key = `${sheetName}:${row}:${colIndex}`;
return this.links.get(key) || null;
}
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
// Set text value
this.setCellValueByColumnName(sheetName, row, columnName, displayText);
// Set link
const data = this.getSheet(sheetName);
const colIndex = data[0].indexOf(columnName);
if (colIndex !== -1) {
const key = `${sheetName}:${row}:${colIndex}`;
this.links.set(key, url);
}
}
}