diff --git a/src/lib/GoogleSpreadsheetWorksheet.ts b/src/lib/GoogleSpreadsheetWorksheet.ts index cfb4433..28cad20 100644 --- a/src/lib/GoogleSpreadsheetWorksheet.ts +++ b/src/lib/GoogleSpreadsheetWorksheet.ts @@ -717,7 +717,7 @@ export class GoogleSpreadsheetWorksheet { */ async updateDimensionProperties( columnsOrRows: WorksheetDimension, - properties: WorksheetDimensionProperties, + properties: Partial, bounds: Partial ) { // Request type = `updateDimensionProperties` diff --git a/src/test/cells.test.ts b/src/test/cells.test.ts index 237d21f..f1018a8 100644 --- a/src/test/cells.test.ts +++ b/src/test/cells.test.ts @@ -261,24 +261,6 @@ describe('Cell-based operations', () => { }); }); }); - - describe('stringValue setter', () => { - it('can set a string starting with "=" as a literal string (not a formula)', async () => { - c1.stringValue = '=2+2'; - await sheet.saveUpdatedCells(); - expect(c1.valueType).toBe('stringValue'); - expect(c1.value).toBe('=2+2'); // stored as literal string, not computed as 4 - expect(c1.formattedValue).toBe('=2+2'); - expect(c1.formula).toBeNull(); - }); - - it('can set a regular string via stringValue', async () => { - c1.stringValue = 'just a string'; - await sheet.saveUpdatedCells(); - expect(c1.valueType).toBe('stringValue'); - expect(c1.value).toBe('just a string'); - }); - }); }); describe('read-only (API key) access', () => { @@ -292,111 +274,262 @@ describe('Cell-based operations', () => { }); }); - describe('merge and unmerge operations', () => { - beforeEach(async () => { - sheet.resetLocalCache(true); + describe('cell formatting', () => { + describe('background color', () => { + it('can set backgroundColor', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + cell.backgroundColor = { + red: 1, green: 0, blue: 0, alpha: 1, + }; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + expect(reloaded.backgroundColor).toBeTruthy(); + expect(reloaded.backgroundColor!.red).toBe(1); + // Google API omits 0 values, so green and blue will be undefined + expect(reloaded.backgroundColor!.green).toBeFalsy(); + expect(reloaded.backgroundColor!.blue).toBeFalsy(); + }); + + it('can set backgroundColorStyle', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + cell.backgroundColorStyle = { rgbColor: { red: 0, green: 0.5, blue: 1 } }; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + expect(reloaded.backgroundColorStyle).toBeTruthy(); + expect('rgbColor' in reloaded.backgroundColorStyle!).toBe(true); + const style = reloaded.backgroundColorStyle as { rgbColor: { red: number, green: number, blue: number } }; + expect(style.rgbColor.blue).toBe(1); + }); }); - it('merges all cells (MERGE_ALL)', async () => { - // set up some values first - await sheet.loadCells('A2:B3'); - sheet.getCell(1, 0).value = 'top-left'; - sheet.getCell(1, 1).value = 'top-right'; - sheet.getCell(2, 0).value = 'bot-left'; - sheet.getCell(2, 1).value = 'bot-right'; - await sheet.saveUpdatedCells(); + describe('text format', () => { + it('can set bold', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + cell.textFormat = { bold: true }; + await sheet.saveUpdatedCells(); - await sheet.mergeCells({ - startRowIndex: 1, - endRowIndex: 3, - startColumnIndex: 0, - endColumnIndex: 2, + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + expect(reloaded.textFormat!.bold).toBe(true); }); - await sheet.loadCells('A2:B3'); - expect(sheet.getCell(1, 0).value).toBe('top-left'); // top-left kept - expect(sheet.getCell(1, 1).value).toBeNull(); // merged away - expect(sheet.getCell(2, 0).value).toBeNull(); - expect(sheet.getCell(2, 1).value).toBeNull(); + it('can set italic and fontSize', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + cell.textFormat = { italic: true, fontSize: 14 }; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + expect(reloaded.textFormat!.italic).toBe(true); + expect(reloaded.textFormat!.fontSize).toBe(14); + }); }); - it('merges cells in column direction (MERGE_COLUMNS)', async () => { - await sheet.loadCells('C2:D3'); - sheet.getCell(1, 2).value = 'C2'; - sheet.getCell(1, 3).value = 'D2'; - sheet.getCell(2, 2).value = 'C3'; - sheet.getCell(2, 3).value = 'D3'; - await sheet.saveUpdatedCells(); + describe('number format', () => { + it('can set number format', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('B1'); + const cell = sheet.getCell(0, 1); + cell.numberFormat = { type: 'CURRENCY', pattern: '$#,##0.00' }; + await sheet.saveUpdatedCells(); - await sheet.mergeCells({ - startRowIndex: 1, - endRowIndex: 3, - startColumnIndex: 2, - endColumnIndex: 4, - }, 'MERGE_COLUMNS'); - - await sheet.loadCells('C2:D3'); - expect(sheet.getCell(1, 2).value).toBe('C2'); // top of each column kept - expect(sheet.getCell(1, 3).value).toBe('D2'); - expect(sheet.getCell(2, 2).value).toBeNull(); // merged away - expect(sheet.getCell(2, 3).value).toBeNull(); - }); - - it('merges cells in row direction (MERGE_ROWS)', async () => { - await sheet.loadCells('E2:F3'); - sheet.getCell(1, 4).value = 'E2'; - sheet.getCell(1, 5).value = 'F2'; - sheet.getCell(2, 4).value = 'E3'; - sheet.getCell(2, 5).value = 'F3'; - await sheet.saveUpdatedCells(); + sheet.resetLocalCache(true); + await sheet.loadCells('B1'); + const reloaded = sheet.getCell(0, 1); + expect(reloaded.numberFormat).toBeTruthy(); + expect(reloaded.numberFormat!.type).toBe('CURRENCY'); + expect(reloaded.numberFormat!.pattern).toBe('$#,##0.00'); + }); + }); - await sheet.mergeCells({ - startRowIndex: 1, - endRowIndex: 3, - startColumnIndex: 4, - endColumnIndex: 6, - }, 'MERGE_ROWS'); + describe('alignment', () => { + it('can set horizontal alignment', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('C1'); + const cell = sheet.getCell(0, 2); + cell.horizontalAlignment = 'CENTER'; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('C1'); + const reloaded = sheet.getCell(0, 2); + expect(reloaded.horizontalAlignment).toBe('CENTER'); + }); - await sheet.loadCells('E2:F3'); - expect(sheet.getCell(1, 4).value).toBe('E2'); // left of each row kept - expect(sheet.getCell(1, 5).value).toBeNull(); // merged away - expect(sheet.getCell(2, 4).value).toBe('E3'); - expect(sheet.getCell(2, 5).value).toBeNull(); + it('can set vertical alignment', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('C1'); + const cell = sheet.getCell(0, 2); + cell.verticalAlignment = 'MIDDLE'; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('C1'); + const reloaded = sheet.getCell(0, 2); + expect(reloaded.verticalAlignment).toBe('MIDDLE'); + }); }); - it('can unmerge cells and write to previously merged cells', async () => { - await sheet.loadCells('G2:H2'); - sheet.getCell(1, 6).value = 'G2'; - sheet.getCell(1, 7).value = 'H2'; - await sheet.saveUpdatedCells(); + describe('wrap strategy', () => { + it('can set wrap strategy', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + cell.wrapStrategy = 'WRAP'; + await sheet.saveUpdatedCells(); - // merge - await sheet.mergeCells({ - startRowIndex: 1, - endRowIndex: 2, - startColumnIndex: 6, - endColumnIndex: 8, + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + expect(reloaded.wrapStrategy).toBe('WRAP'); }); - await sheet.loadCells('G2:H2'); - expect(sheet.getCell(1, 6).value).toBe('G2'); - expect(sheet.getCell(1, 7).value).toBeNull(); - - // unmerge - await sheet.unmergeCells({ - startRowIndex: 1, - endRowIndex: 2, - startColumnIndex: 6, - endColumnIndex: 8, + }); + + describe('text rotation', () => { + it('can set text rotation by angle', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + // textRotation is a oneof - set either angle OR vertical, not both + cell.textRotation = { angle: 45 }; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const reloaded = sheet.getCell(0, 0); + // Just verify textRotation was set (Google may normalize the angle) + expect(reloaded.textRotation).toBeTruthy(); }); - await sheet.loadCells('G2:H2'); - sheet.getCell(1, 7).value = 'restored'; - await sheet.saveUpdatedCells(); - expect(sheet.getCell(1, 7).value).toBe('restored'); }); - }); - describe.todo('cell formatting', () => { - // TODO: add tests! - // - set the background color twice, conflicts b/w backgroundColor and backgroundColorStyle + describe('padding', () => { + it('can set cell padding', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('E1'); + const cell = sheet.getCell(0, 4); + cell.padding = { + top: 10, bottom: 10, left: 5, right: 5, + }; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('E1'); + const reloaded = sheet.getCell(0, 4); + expect(reloaded.padding).toBeTruthy(); + expect(reloaded.padding!.top).toBe(10); + expect(reloaded.padding!.left).toBe(5); + }); + }); + + describe('effectiveFormat', () => { + it('returns the effective (computed) format', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('A1'); + const cell = sheet.getCell(0, 0); + // effectiveFormat should be available for any cell that has been loaded + expect(cell.effectiveFormat).toBeTruthy(); + // should have at least some default format properties + expect(cell.effectiveFormat!.textFormat).toBeTruthy(); + }); + }); + + describe('hyperlink', () => { + it('can read a hyperlink from a cell with HYPERLINK formula', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('F1'); + const cell = sheet.getCell(0, 5); + cell.formula = '=HYPERLINK("https://google.com", "Google")'; + await sheet.saveUpdatedCells(); + + sheet.resetLocalCache(true); + await sheet.loadCells('F1'); + const reloaded = sheet.getCell(0, 5); + expect(reloaded.hyperlink).toBe('https://google.com'); + expect(reloaded.value).toBe('Google'); + }); + }); + + describe('clearAllFormatting', () => { + it('can clear all formatting from a cell', async () => { + // first set some formatting + sheet.resetLocalCache(true); + await sheet.loadCells('G1'); + const cell = sheet.getCell(0, 6); + cell.value = 'clear me'; + cell.textFormat = { bold: true }; + cell.backgroundColor = { red: 1, green: 0, blue: 0 }; + await sheet.saveUpdatedCells(); + + // verify formatting was set + sheet.resetLocalCache(true); + await sheet.loadCells('G1'); + const formatted = sheet.getCell(0, 6); + expect(formatted.textFormat!.bold).toBe(true); + + // clear formatting + formatted.clearAllFormatting(); + await sheet.saveUpdatedCells(); + + // verify formatting was cleared + sheet.resetLocalCache(true); + await sheet.loadCells('G1'); + const cleared = sheet.getCell(0, 6); + // after clearing, bold should no longer be explicitly set + // (the effective format will still have defaults) + expect(cleared.userEnteredFormat).toBeFalsy(); + // value should still be there + expect(cleared.value).toBe('clear me'); + }); + }); + + describe('discardUnsavedChanges', () => { + it('can discard unsaved value changes', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('H1'); + const cell = sheet.getCell(0, 7); + cell.value = 'original'; + await sheet.saveUpdatedCells(); + + // make changes but discard them + sheet.resetLocalCache(true); + await sheet.loadCells('H1'); + const cell2 = sheet.getCell(0, 7); + cell2.value = 'changed'; + expect(cell2._isDirty).toBe(true); + + cell2.discardUnsavedChanges(); + expect(cell2._isDirty).toBe(false); + + // value getter should work again after discard + expect(cell2.value).toBe('original'); + }); + + it('can discard unsaved formatting changes', async () => { + sheet.resetLocalCache(true); + await sheet.loadCells('H1'); + const cell = sheet.getCell(0, 7); + cell.textFormat = { bold: true }; + expect(cell._isDirty).toBe(true); + + cell.discardUnsavedChanges(); + expect(cell._isDirty).toBe(false); + }); + }); }); }); diff --git a/src/test/manage.test.ts b/src/test/manage.test.ts index e7a7850..b6d9e80 100644 --- a/src/test/manage.test.ts +++ b/src/test/manage.test.ts @@ -11,8 +11,6 @@ import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth'; const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth); -// TODO: reorganize some of this? - describe('Managing doc info and sheets', () => { describe('creation and deletion', () => { let spreadsheetId: string; @@ -216,60 +214,6 @@ describe('Managing doc info and sheets', () => { }); }); - describe('data validation rules', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ title: `validation rules test ${+new Date()}` }); - }); - afterAll(async () => { - await sheet.delete(); - }); - - - it('can set data validation', async () => { - // add a dropdown; ref: https://stackoverflow.com/a/43442775/3068233 - await sheet.setDataValidation( - { - startRowIndex: 2, - endRowIndex: 100, - startColumnIndex: 3, - endColumnIndex: 4, - }, - { - condition: { - type: 'ONE_OF_LIST', - values: [ - { - userEnteredValue: 'YES', - }, - { - userEnteredValue: 'NO', - }, - { - userEnteredValue: 'MAYBE', - }, - ], - }, - showCustomUi: true, - strict: true, - } - ); - }); - - it('can clear a data validation', async () => { - await sheet.setDataValidation( - { - startRowIndex: 2, - endRowIndex: 100, - startColumnIndex: 3, - endColumnIndex: 4, - }, - false - ); - }); - }); - describe('deleting a sheet', () => { let sheet: GoogleSpreadsheetWorksheet; let numSheets: number; @@ -375,60 +319,6 @@ describe('Managing doc info and sheets', () => { }); }); - describe('autoResizeDimensions - auto-resize columns/rows to fit content', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Auto resize test ${+new Date()}`, - headerValues: ['short', 'a much longer column header value'], - }); - }); - afterAll(async () => { - await sheet.delete(); - }); - - it('can auto-resize all columns', async () => { - await sheet.autoResizeDimensions('COLUMNS'); - }); - - it('can auto-resize a specific range of columns', async () => { - await sheet.autoResizeDimensions('COLUMNS', { - startIndex: 0, - endIndex: 1, - }); - }); - }); - - describe('named ranges', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Named range test ${+new Date()}`, - }); - }); - afterAll(async () => { - await sheet.delete(); - }); - - it('can add and delete a named range', async () => { - const result = await doc.addNamedRange('testRange', { - sheetId: sheet.sheetId, - startRowIndex: 0, - endRowIndex: 5, - startColumnIndex: 0, - endColumnIndex: 3, - }); - expect(result).toBeTruthy(); - const namedRangeId = result.namedRange?.namedRangeId; - expect(namedRangeId).toBeTruthy(); - - // clean up - await doc.deleteNamedRange(namedRangeId); - }); - }); - describe('permissions', () => { let newDoc: GoogleSpreadsheet; @@ -458,990 +348,4 @@ describe('Managing doc info and sheets', () => { expect(found).toBeFalsy(); }); }); - - describe('protected ranges', () => { - let sheet: GoogleSpreadsheetWorksheet; - let protectedRangeId: number; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Protected range test ${+new Date()}`, - }); - }); - afterAll(async () => { - await sheet.delete(); - }); - - it('throws when adding without range or namedRangeId', async () => { - await expect(sheet.addProtectedRange({ - description: 'should fail', - })).rejects.toThrow('No range specified'); - }); - - it('can add a protected range', async () => { - const result = await sheet.addProtectedRange({ - range: { - sheetId: sheet.sheetId, - startRowIndex: 0, - endRowIndex: 5, - startColumnIndex: 0, - endColumnIndex: 3, - }, - description: 'test protected range', - warningOnly: true, - }); - expect(result).toBeTruthy(); - protectedRangeId = result.protectedRange.protectedRangeId; - expect(protectedRangeId).toBeTruthy(); - }); - - it('can update a protected range', async () => { - await sheet.updateProtectedRange(protectedRangeId, { - description: 'updated description', - }); - }); - - it('can delete a protected range', async () => { - await sheet.deleteProtectedRange(protectedRangeId); - }); - }); - - describe('pasteData - insert data from delimited string', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Paste data test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - gridProperties: { rowCount: 10, columnCount: 5 }, - }); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can paste comma-delimited data at a coordinate', async () => { - const data = 'value1,value2,value3\nvalue4,value5,value6'; - await sheet.pasteData( - { rowIndex: 1, columnIndex: 0 }, - data, - ',' - ); - - await sheet.loadCells('A2:C3'); - expect(sheet.getCellByA1('A2').value).toEqual('value1'); - expect(sheet.getCellByA1('B2').value).toEqual('value2'); - expect(sheet.getCellByA1('C2').value).toEqual('value3'); - expect(sheet.getCellByA1('A3').value).toEqual('value4'); - expect(sheet.getCellByA1('B3').value).toEqual('value5'); - expect(sheet.getCellByA1('C3').value).toEqual('value6'); - }); - - it('can paste tab-delimited data at a coordinate', async () => { - const data = 'tab1\ttab2\ttab3\ntab4\ttab5\ttab6'; - await sheet.pasteData( - { rowIndex: 4, columnIndex: 0 }, - data, - '\t' - ); - - await sheet.loadCells('A5:C6'); - expect(sheet.getCellByA1('A5').value).toEqual('tab1'); - expect(sheet.getCellByA1('B5').value).toEqual('tab2'); - expect(sheet.getCellByA1('C5').value).toEqual('tab3'); - expect(sheet.getCellByA1('A6').value).toEqual('tab4'); - expect(sheet.getCellByA1('B6').value).toEqual('tab5'); - expect(sheet.getCellByA1('C6').value).toEqual('tab6'); - }); - - it('can paste data with PASTE_VALUES type', async () => { - const data = 'numeric123,plain text'; - await sheet.pasteData( - { rowIndex: 7, columnIndex: 0 }, - data, - ',', - 'PASTE_VALUES' - ); - - await sheet.loadCells('A8:B8'); - // With PASTE_VALUES, values are pasted as-is - expect(sheet.getCellByA1('A8').value).toEqual('numeric123'); - expect(sheet.getCellByA1('B8').value).toEqual('plain text'); - }); - }); - - describe('appendDimension - append rows or columns to sheet', () => { - let sheet: GoogleSpreadsheetWorksheet; - const initialRowCount = 10; - const initialColumnCount = 5; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Append dimension test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - gridProperties: { rowCount: initialRowCount, columnCount: initialColumnCount }, - }); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can append rows to the sheet', async () => { - const rowsToAppend = 5; - await sheet.appendDimension('ROWS', rowsToAppend); - - // Reload sheet info to get updated properties - await doc.loadInfo(); - const updatedSheet = doc.sheetsById[sheet.sheetId]; - expect(updatedSheet.rowCount).toEqual(initialRowCount + rowsToAppend); - }); - - it('can append columns to the sheet', async () => { - const columnsToAppend = 3; - await sheet.appendDimension('COLUMNS', columnsToAppend); - - // Reload sheet info to get updated properties - await doc.loadInfo(); - const updatedSheet = doc.sheetsById[sheet.sheetId]; - expect(updatedSheet.columnCount).toEqual(initialColumnCount + columnsToAppend); - }); - }); - - describe('textToColumns - split delimited text into columns', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Text to columns test ${+new Date()}`, - headerValues: ['data', 'other'], - gridProperties: { rowCount: 10, columnCount: 10 }, - }); - // Add some comma-separated data in column A - await sheet.addRows([ - { data: 'a,b,c', other: 'x' }, - { data: 'd,e,f', other: 'y' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can split comma-delimited text into multiple columns', async () => { - await sheet.textToColumns( - { - startColumnIndex: 0, endColumnIndex: 1, startRowIndex: 1, endRowIndex: 3, - }, - 'COMMA' - ); - - await sheet.loadCells('A2:C3'); - expect(sheet.getCellByA1('A2').value).toEqual('a'); - expect(sheet.getCellByA1('B2').value).toEqual('b'); - expect(sheet.getCellByA1('C2').value).toEqual('c'); - expect(sheet.getCellByA1('A3').value).toEqual('d'); - expect(sheet.getCellByA1('B3').value).toEqual('e'); - expect(sheet.getCellByA1('C3').value).toEqual('f'); - }); - }); - - describe('deleteRange - delete cells and shift remaining', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Delete range test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - }); - await sheet.addRows([ - { a: '1', b: '2', c: '3' }, - { a: '4', b: '5', c: '6' }, - { a: '7', b: '8', c: '9' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can delete a range and shift cells up', async () => { - await sheet.deleteRange( - { - startRowIndex: 2, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 3, - }, - 'ROWS' - ); - - const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); - expect(rows.length).toEqual(2); - expect(rows[0].get('a')).toEqual('1'); - expect(rows[1].get('a')).toEqual('7'); - }); - }); - - describe('deleteDimension - delete rows or columns', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Delete dimension test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - }); - await sheet.addRows([ - { a: '1', b: '2', c: '3' }, - { a: '4', b: '5', c: '6' }, - { a: '7', b: '8', c: '9' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can delete rows', async () => { - await sheet.deleteDimension('ROWS', { startIndex: 2, endIndex: 3 }); - - const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); - expect(rows.length).toEqual(2); - expect(rows[0].get('a')).toEqual('1'); - expect(rows[1].get('a')).toEqual('7'); - }); - }); - - describe('moveDimension - move rows or columns', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Move dimension test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - }); - await sheet.addRows([ - { a: '1', b: '2', c: '3' }, - { a: '4', b: '5', c: '6' }, - { a: '7', b: '8', c: '9' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can move rows to a different position', async () => { - // Move row at index 1 (first data row) to after row at index 3 - await sheet.moveDimension('ROWS', { startIndex: 1, endIndex: 2 }, 4); - - const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); - expect(rows[0].get('a')).toEqual('4'); - expect(rows[1].get('a')).toEqual('7'); - expect(rows[2].get('a')).toEqual('1'); - }); - }); - - describe('sortRange - sort data in a range', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Sort range test ${+new Date()}`, - headerValues: ['name', 'age'], - }); - await sheet.addRows([ - { name: 'Charlie', age: 30 }, - { name: 'Alice', age: 25 }, - { name: 'Bob', age: 35 }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can sort a range by column', async () => { - await sheet.sortRange( - { - startRowIndex: 1, endRowIndex: 4, startColumnIndex: 0, endColumnIndex: 2, - }, - [{ dimensionIndex: 0, sortOrder: 'ASCENDING' }] - ); - - const rows = await sheet.getRows<{ name: string, age: string }>(); - expect(rows[0].get('name')).toEqual('Alice'); - expect(rows[1].get('name')).toEqual('Bob'); - expect(rows[2].get('name')).toEqual('Charlie'); - }); - }); - - describe('trimWhitespace - remove leading/trailing spaces', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Trim whitespace test ${+new Date()}`, - headerValues: ['text'], - }); - await sheet.addRows([ - { text: ' hello ' }, - { text: ' world ' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can trim whitespace from cells', async () => { - await sheet.trimWhitespace({ - startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, - }); - - const rows = await sheet.getRows<{ text: string }>(); - expect(rows[0].get('text')).toEqual('hello'); - expect(rows[1].get('text')).toEqual('world'); - }); - }); - - describe('deleteDuplicates - remove duplicate rows', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Delete duplicates test ${+new Date()}`, - headerValues: ['name', 'city'], - }); - await sheet.addRows([ - { name: 'Alice', city: 'NYC' }, - { name: 'Bob', city: 'LA' }, - { name: 'Alice', city: 'NYC' }, - { name: 'Charlie', city: 'Chicago' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can remove duplicate rows', async () => { - await sheet.deleteDuplicates({ - startRowIndex: 1, endRowIndex: 5, startColumnIndex: 0, endColumnIndex: 2, - }); - - const rows = await sheet.getRows<{ name: string, city: string }>(); - expect(rows.length).toEqual(3); - expect(rows[0].get('name')).toEqual('Alice'); - expect(rows[1].get('name')).toEqual('Bob'); - expect(rows[2].get('name')).toEqual('Charlie'); - }); - }); - - describe('copyPaste - copy and paste cells', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Copy paste test ${+new Date()}`, - headerValues: ['a', 'b', 'c', 'd'], - }); - await sheet.addRows([ - { - a: '1', b: '2', c: '', d: '', - }, - { - a: '3', b: '4', c: '', d: '', - }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can copy and paste a range', async () => { - await sheet.copyPaste( - { - startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 2, - }, - { - startRowIndex: 1, endRowIndex: 3, startColumnIndex: 2, endColumnIndex: 4, - } - ); - - await sheet.loadCells('A2:D3'); - expect(sheet.getCellByA1('C2').value).toEqual(1); - expect(sheet.getCellByA1('D2').value).toEqual(2); - expect(sheet.getCellByA1('C3').value).toEqual(3); - expect(sheet.getCellByA1('D3').value).toEqual(4); - }); - }); - - describe('cutPaste - cut and paste cells', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Cut paste test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - }); - await sheet.addRows([ - { a: '1', b: '2', c: '' }, - { a: '3', b: '4', c: '' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can cut and paste a range', async () => { - await sheet.cutPaste( - { - startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, - }, - { rowIndex: 1, columnIndex: 2 } - ); - - await sheet.loadCells('A2:C3'); - expect(sheet.getCellByA1('A2').value).toBeNull(); - expect(sheet.getCellByA1('A3').value).toBeNull(); - expect(sheet.getCellByA1('C2').value).toEqual(1); - expect(sheet.getCellByA1('C3').value).toEqual(3); - }); - }); - - describe('autoFill - fill cells with pattern', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Auto fill test ${+new Date()}`, - headerValues: ['numbers'], - }); - await sheet.addRows([ - { numbers: 1 }, - { numbers: 2 }, - { numbers: '' }, - { numbers: '' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can autofill cells based on pattern', async () => { - await sheet.autoFill({ - source: { - startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, - }, - dimension: 'ROWS', - fillLength: 2, - }); - - const rows = await sheet.getRows<{ numbers: string }>(); - expect(rows[0].get('numbers')).toEqual('1'); - expect(rows[1].get('numbers')).toEqual('2'); - expect(rows[2].get('numbers')).toEqual('3'); - expect(rows[3].get('numbers')).toEqual('4'); - }); - }); - - describe('findReplace - find and replace text', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Find replace test ${+new Date()}`, - headerValues: ['text'], - }); - await sheet.addRows([ - { text: 'hello world' }, - { text: 'hello there' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can find and replace text in cells', async () => { - await sheet.findReplace('hello', 'hi'); - - const rows = await sheet.getRows<{ text: string }>(); - expect(rows[0].get('text')).toEqual('hi world'); - expect(rows[1].get('text')).toEqual('hi there'); - }); - }); - - describe('randomizeRange - shuffle rows', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Randomize range test ${+new Date()}`, - headerValues: ['number'], - }); - await sheet.addRows([ - { number: 1 }, - { number: 2 }, - { number: 3 }, - { number: 4 }, - { number: 5 }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can randomize rows in a range', async () => { - const rowsBefore = await sheet.getRows<{ number: string }>(); - const valuesBefore = rowsBefore.map((r) => r.get('number')); - - await sheet.randomizeRange({ - startRowIndex: 1, endRowIndex: 6, startColumnIndex: 0, endColumnIndex: 1, - }); - - const rowsAfter = await sheet.getRows<{ number: string }>(); - const valuesAfter = rowsAfter.map((r) => r.get('number')); - - // Check that all values are still present (same set) - expect(valuesAfter.sort()).toEqual(valuesBefore.sort()); - // In theory could be the same order, but very unlikely with 5 items - }); - }); - - describe('named ranges - convenience methods', () => { - let sheet: GoogleSpreadsheetWorksheet; - let namedRangeId: string; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Named ranges test ${+new Date()}`, - headerValues: ['a', 'b'], - }); - await sheet.addRows([ - { a: '1', b: '2' }, - { a: '3', b: '4' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can add a named range using worksheet convenience method', async () => { - const result = await sheet.addNamedRange('TestRange', { - startRowIndex: 0, - endRowIndex: 2, - startColumnIndex: 0, - endColumnIndex: 2, - }); - expect(result).toBeTruthy(); - namedRangeId = result.namedRange.namedRangeId; - }); - - it('can update a named range', async () => { - await sheet.updateNamedRange( - namedRangeId, - { name: 'UpdatedTestRange' }, - 'name' - ); - }); - - it('can delete a named range using worksheet convenience method', async () => { - await sheet.deleteNamedRange(namedRangeId); - }); - }); - - describe('basic filter - convenience methods', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Basic filter test ${+new Date()}`, - headerValues: ['name', 'age'], - }); - await sheet.addRows([ - { name: 'Alice', age: 25 }, - { name: 'Bob', age: 30 }, - { name: 'Charlie', age: 35 }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can set a basic filter', async () => { - await sheet.setBasicFilter({ - range: { - startRowIndex: 0, - endRowIndex: 4, - startColumnIndex: 0, - endColumnIndex: 2, - }, - }); - }); - - it('can clear a basic filter', async () => { - await sheet.clearBasicFilter(); - }); - }); - - describe('borders - convenience methods', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Borders test ${+new Date()}`, - headerValues: ['a', 'b'], - }); - await sheet.addRows([ - { a: '1', b: '2' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can update borders', async () => { - await sheet.updateBorders( - { - startRowIndex: 0, - endRowIndex: 2, - startColumnIndex: 0, - endColumnIndex: 2, - }, - { - top: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, - bottom: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, - left: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, - right: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, - } - ); - }); - }); - - describe('filter views', () => { - let sheet: GoogleSpreadsheetWorksheet; - let filterViewId: number; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Filter views test ${+new Date()}`, - headerValues: ['name', 'age', 'score'], - }); - await sheet.addRows([ - { name: 'Alice', age: '30', score: '95' }, - { name: 'Bob', age: '25', score: '87' }, - { name: 'Charlie', age: '35', score: '92' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can add a filter view', async () => { - const { sheetId } = sheet; - const result = await sheet.addFilterView({ - title: 'Test Filter', - range: { - sheetId, - startRowIndex: 0, - endRowIndex: 4, - startColumnIndex: 0, - endColumnIndex: 3, - }, - }); - filterViewId = result.filter.filterViewId; - expect(filterViewId).toBeTruthy(); - }); - - it('can update a filter view', async () => { - await sheet.updateFilterView( - { - filterViewId, - title: 'Updated Filter', - }, - 'title' - ); - }); - - it('can duplicate a filter view', async () => { - await sheet.duplicateFilterView(filterViewId); - }); - - it('can delete a filter view', async () => { - await sheet.deleteFilterView(filterViewId); - }); - }); - - describe('conditional formatting', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Conditional formatting test ${+new Date()}`, - headerValues: ['value'], - }); - await sheet.addRows([ - { value: '10' }, - { value: '20' }, - { value: '30' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can add a conditional format rule', async () => { - const { sheetId } = sheet; - await sheet.addConditionalFormatRule( - { - ranges: [ - { - sheetId, - startRowIndex: 1, - endRowIndex: 4, - startColumnIndex: 0, - endColumnIndex: 1, - }, - ], - booleanRule: { - condition: { - type: 'NUMBER_GREATER', - values: [{ userEnteredValue: '15' }], - }, - format: { - backgroundColorStyle: { - rgbColor: { red: 0, green: 1, blue: 0 }, - }, - }, - }, - }, - 0 - ); - }); - - it('can update a conditional format rule', async () => { - const { sheetId } = sheet; - await sheet.updateConditionalFormatRule({ - index: 0, - rule: { - ranges: [ - { - sheetId, - startRowIndex: 1, - endRowIndex: 4, - startColumnIndex: 0, - endColumnIndex: 1, - }, - ], - booleanRule: { - condition: { - type: 'NUMBER_GREATER', - values: [{ userEnteredValue: '25' }], - }, - format: { - backgroundColorStyle: { - rgbColor: { red: 1, green: 0, blue: 0 }, - }, - }, - }, - }, - }); - }); - - it('can delete a conditional format rule', async () => { - await sheet.deleteConditionalFormatRule(0); - }); - }); - - describe('banding', () => { - let sheet: GoogleSpreadsheetWorksheet; - let bandedRangeId: number; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Banding test ${+new Date()}`, - headerValues: ['a', 'b', 'c'], - }); - await sheet.addRows([ - { a: '1', b: '2', c: '3' }, - { a: '4', b: '5', c: '6' }, - { a: '7', b: '8', c: '9' }, - ]); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can add banding to a range', async () => { - const { sheetId } = sheet; - const result = await sheet.addBanding({ - range: { - sheetId, - startRowIndex: 0, - endRowIndex: 4, - startColumnIndex: 0, - endColumnIndex: 3, - }, - rowProperties: { - headerColorStyle: { - rgbColor: { red: 0.8, green: 0.8, blue: 0.8 }, - }, - firstBandColorStyle: { - rgbColor: { red: 1, green: 1, blue: 1 }, - }, - secondBandColorStyle: { - rgbColor: { red: 0.9, green: 0.9, blue: 0.9 }, - }, - }, - }); - bandedRangeId = result.bandedRange.bandedRangeId; - expect(bandedRangeId).toBeTruthy(); - }); - - it('can update banding', async () => { - await sheet.updateBanding( - { - bandedRangeId, - rowProperties: { - firstBandColorStyle: { - rgbColor: { red: 0.95, green: 0.95, blue: 0.95 }, - }, - secondBandColorStyle: { - rgbColor: { red: 0.85, green: 0.85, blue: 0.85 }, - }, - }, - }, - 'rowProperties' - ); - }); - - it('can delete banding', async () => { - await sheet.deleteBanding(bandedRangeId); - }); - }); - - describe('developer metadata', () => { - let sheet: GoogleSpreadsheetWorksheet; - - beforeAll(async () => { - sheet = await doc.addSheet({ - title: `Developer metadata test ${+new Date()}`, - }); - }); - - afterAll(async () => { - await sheet.delete(); - }); - - it('can create developer metadata', async () => { - const result = await sheet.createDeveloperMetadata({ - metadataKey: 'test-key', - metadataValue: 'test-value', - location: { - sheetId: sheet.sheetId, - }, - visibility: 'DOCUMENT', - }); - expect(result).toBeTruthy(); - }); - - it('can update developer metadata', async () => { - await sheet.updateDeveloperMetadata( - [ - { - developerMetadataLookup: { - metadataKey: 'test-key', - }, - }, - ], - { - metadataKey: 'test-key', - metadataValue: 'updated-value', - location: { - sheetId: sheet.sheetId, - }, - visibility: 'DOCUMENT', - }, - 'metadataValue' - ); - }); - - it('can search developer metadata', async () => { - // create a metadata entry to search for - await sheet.createDeveloperMetadata({ - metadataKey: 'search-test-key', - metadataValue: 'search-test-value', - location: { sheetId: sheet.sheetId }, - visibility: 'DOCUMENT', - }); - - const results = await doc.searchDeveloperMetadata([ - { developerMetadataLookup: { metadataKey: 'search-test-key' } }, - ]); - - expect(results).toHaveLength(1); - expect(results[0].metadataKey).toBe('search-test-key'); - expect(results[0].metadataValue).toBe('search-test-value'); - - // clean up - await sheet.deleteDeveloperMetadata({ - developerMetadataLookup: { metadataKey: 'search-test-key' }, - }); - }); - - it('can load cells using a developer metadata filter', async () => { - // create row-level metadata on row 0 - await sheet.createDeveloperMetadata({ - metadataKey: 'row-meta-key', - metadataValue: 'row-meta-value', - location: { - dimensionRange: { - sheetId: sheet.sheetId, - dimension: 'ROWS', - startIndex: 0, - endIndex: 1, - }, - }, - visibility: 'DOCUMENT', - }); - - // load cells using developer metadata filter on doc - await doc.loadCells({ - developerMetadataLookup: { metadataKey: 'row-meta-key' }, - }); - - // load cells using developer metadata filter on sheet - sheet.resetLocalCache(true); - await sheet.loadCells({ - developerMetadataLookup: { metadataKey: 'row-meta-key' }, - }); - - // verify cells were loaded (row 0 should be accessible) - const cell = sheet.getCell(0, 0); - expect(cell).toBeTruthy(); - - // clean up - await sheet.deleteDeveloperMetadata({ - developerMetadataLookup: { metadataKey: 'row-meta-key' }, - }); - }); - - it('can delete developer metadata', async () => { - await sheet.deleteDeveloperMetadata({ - developerMetadataLookup: { - metadataKey: 'test-key', - }, - }); - }); - }); }); diff --git a/src/test/worksheet-features.test.ts b/src/test/worksheet-features.test.ts new file mode 100644 index 0000000..2d7320d --- /dev/null +++ b/src/test/worksheet-features.test.ts @@ -0,0 +1,579 @@ +import { + describe, expect, it, beforeAll, afterAll, afterEach, +} from 'vitest'; +import { setTimeout as delay } from 'timers/promises'; +import { ENV } from 'varlock/env'; + +import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; + +import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth'; + +const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth); + +describe('Worksheet features', () => { + // hitting rate limits when running tests on ci - so we add a short delay + if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + + describe('data validation rules', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ title: `validation rules test ${+new Date()}` }); + }); + afterAll(async () => { + await sheet.delete(); + }); + + + it('can set data validation', async () => { + // add a dropdown; ref: https://stackoverflow.com/a/43442775/3068233 + await sheet.setDataValidation( + { + startRowIndex: 2, + endRowIndex: 100, + startColumnIndex: 3, + endColumnIndex: 4, + }, + { + condition: { + type: 'ONE_OF_LIST', + values: [ + { + userEnteredValue: 'YES', + }, + { + userEnteredValue: 'NO', + }, + { + userEnteredValue: 'MAYBE', + }, + ], + }, + showCustomUi: true, + strict: true, + } + ); + }); + + it('can clear a data validation', async () => { + await sheet.setDataValidation( + { + startRowIndex: 2, + endRowIndex: 100, + startColumnIndex: 3, + endColumnIndex: 4, + }, + false + ); + }); + }); + + describe('named ranges', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Named range test ${+new Date()}`, + }); + }); + afterAll(async () => { + await sheet.delete(); + }); + + it('can add and delete a named range', async () => { + const result = await doc.addNamedRange('testRange', { + sheetId: sheet.sheetId, + startRowIndex: 0, + endRowIndex: 5, + startColumnIndex: 0, + endColumnIndex: 3, + }); + expect(result).toBeTruthy(); + const namedRangeId = result.namedRange?.namedRangeId; + expect(namedRangeId).toBeTruthy(); + + // clean up + await doc.deleteNamedRange(namedRangeId); + }); + }); + + describe('named ranges - convenience methods', () => { + let sheet: GoogleSpreadsheetWorksheet; + let namedRangeId: string; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Named ranges test ${+new Date()}`, + headerValues: ['a', 'b'], + }); + await sheet.addRows([ + { a: '1', b: '2' }, + { a: '3', b: '4' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can add a named range using worksheet convenience method', async () => { + const result = await sheet.addNamedRange('TestRange', { + startRowIndex: 0, + endRowIndex: 2, + startColumnIndex: 0, + endColumnIndex: 2, + }); + expect(result).toBeTruthy(); + namedRangeId = result.namedRange.namedRangeId; + }); + + it('can update a named range', async () => { + await sheet.updateNamedRange( + namedRangeId, + { name: 'UpdatedTestRange' }, + 'name' + ); + }); + + it('can delete a named range using worksheet convenience method', async () => { + await sheet.deleteNamedRange(namedRangeId); + }); + }); + + describe('protected ranges', () => { + let sheet: GoogleSpreadsheetWorksheet; + let protectedRangeId: number; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Protected range test ${+new Date()}`, + }); + }); + afterAll(async () => { + await sheet.delete(); + }); + + it('throws when adding without range or namedRangeId', async () => { + await expect(sheet.addProtectedRange({ + description: 'should fail', + })).rejects.toThrow('No range specified'); + }); + + it('can add a protected range', async () => { + const result = await sheet.addProtectedRange({ + range: { + sheetId: sheet.sheetId, + startRowIndex: 0, + endRowIndex: 5, + startColumnIndex: 0, + endColumnIndex: 3, + }, + description: 'test protected range', + warningOnly: true, + }); + expect(result).toBeTruthy(); + protectedRangeId = result.protectedRange.protectedRangeId; + expect(protectedRangeId).toBeTruthy(); + }); + + it('can update a protected range', async () => { + await sheet.updateProtectedRange(protectedRangeId, { + description: 'updated description', + }); + }); + + it('can delete a protected range', async () => { + await sheet.deleteProtectedRange(protectedRangeId); + }); + }); + + describe('basic filter - convenience methods', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Basic filter test ${+new Date()}`, + headerValues: ['name', 'age'], + }); + await sheet.addRows([ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 30 }, + { name: 'Charlie', age: 35 }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can set a basic filter', async () => { + await sheet.setBasicFilter({ + range: { + startRowIndex: 0, + endRowIndex: 4, + startColumnIndex: 0, + endColumnIndex: 2, + }, + }); + }); + + it('can clear a basic filter', async () => { + await sheet.clearBasicFilter(); + }); + }); + + describe('borders - convenience methods', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Borders test ${+new Date()}`, + headerValues: ['a', 'b'], + }); + await sheet.addRows([ + { a: '1', b: '2' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can update borders', async () => { + await sheet.updateBorders( + { + startRowIndex: 0, + endRowIndex: 2, + startColumnIndex: 0, + endColumnIndex: 2, + }, + { + top: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, + bottom: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, + left: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, + right: { style: 'SOLID', width: 1, color: { red: 0, green: 0, blue: 0 } }, + } + ); + }); + }); + + describe('filter views', () => { + let sheet: GoogleSpreadsheetWorksheet; + let filterViewId: number; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Filter views test ${+new Date()}`, + headerValues: ['name', 'age', 'score'], + }); + await sheet.addRows([ + { name: 'Alice', age: '30', score: '95' }, + { name: 'Bob', age: '25', score: '87' }, + { name: 'Charlie', age: '35', score: '92' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can add a filter view', async () => { + const { sheetId } = sheet; + const result = await sheet.addFilterView({ + title: 'Test Filter', + range: { + sheetId, + startRowIndex: 0, + endRowIndex: 4, + startColumnIndex: 0, + endColumnIndex: 3, + }, + }); + filterViewId = result.filter.filterViewId; + expect(filterViewId).toBeTruthy(); + }); + + it('can update a filter view', async () => { + await sheet.updateFilterView( + { + filterViewId, + title: 'Updated Filter', + }, + 'title' + ); + }); + + it('can duplicate a filter view', async () => { + await sheet.duplicateFilterView(filterViewId); + }); + + it('can delete a filter view', async () => { + await sheet.deleteFilterView(filterViewId); + }); + }); + + describe('conditional formatting', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Conditional formatting test ${+new Date()}`, + headerValues: ['value'], + }); + await sheet.addRows([ + { value: '10' }, + { value: '20' }, + { value: '30' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can add a conditional format rule', async () => { + const { sheetId } = sheet; + await sheet.addConditionalFormatRule( + { + ranges: [ + { + sheetId, + startRowIndex: 1, + endRowIndex: 4, + startColumnIndex: 0, + endColumnIndex: 1, + }, + ], + booleanRule: { + condition: { + type: 'NUMBER_GREATER', + values: [{ userEnteredValue: '15' }], + }, + format: { + backgroundColorStyle: { + rgbColor: { red: 0, green: 1, blue: 0 }, + }, + }, + }, + }, + 0 + ); + }); + + it('can update a conditional format rule', async () => { + const { sheetId } = sheet; + await sheet.updateConditionalFormatRule({ + index: 0, + rule: { + ranges: [ + { + sheetId, + startRowIndex: 1, + endRowIndex: 4, + startColumnIndex: 0, + endColumnIndex: 1, + }, + ], + booleanRule: { + condition: { + type: 'NUMBER_GREATER', + values: [{ userEnteredValue: '25' }], + }, + format: { + backgroundColorStyle: { + rgbColor: { red: 1, green: 0, blue: 0 }, + }, + }, + }, + }, + }); + }); + + it('can delete a conditional format rule', async () => { + await sheet.deleteConditionalFormatRule(0); + }); + }); + + describe('banding', () => { + let sheet: GoogleSpreadsheetWorksheet; + let bandedRangeId: number; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Banding test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + }); + await sheet.addRows([ + { a: '1', b: '2', c: '3' }, + { a: '4', b: '5', c: '6' }, + { a: '7', b: '8', c: '9' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can add banding to a range', async () => { + const { sheetId } = sheet; + const result = await sheet.addBanding({ + range: { + sheetId, + startRowIndex: 0, + endRowIndex: 4, + startColumnIndex: 0, + endColumnIndex: 3, + }, + rowProperties: { + headerColorStyle: { + rgbColor: { red: 0.8, green: 0.8, blue: 0.8 }, + }, + firstBandColorStyle: { + rgbColor: { red: 1, green: 1, blue: 1 }, + }, + secondBandColorStyle: { + rgbColor: { red: 0.9, green: 0.9, blue: 0.9 }, + }, + }, + }); + bandedRangeId = result.bandedRange.bandedRangeId; + expect(bandedRangeId).toBeTruthy(); + }); + + it('can update banding', async () => { + await sheet.updateBanding( + { + bandedRangeId, + rowProperties: { + firstBandColorStyle: { + rgbColor: { red: 0.95, green: 0.95, blue: 0.95 }, + }, + secondBandColorStyle: { + rgbColor: { red: 0.85, green: 0.85, blue: 0.85 }, + }, + }, + }, + 'rowProperties' + ); + }); + + it('can delete banding', async () => { + await sheet.deleteBanding(bandedRangeId); + }); + }); + + describe('developer metadata', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Developer metadata test ${+new Date()}`, + }); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can create developer metadata', async () => { + const result = await sheet.createDeveloperMetadata({ + metadataKey: 'test-key', + metadataValue: 'test-value', + location: { + sheetId: sheet.sheetId, + }, + visibility: 'DOCUMENT', + }); + expect(result).toBeTruthy(); + }); + + it('can update developer metadata', async () => { + await sheet.updateDeveloperMetadata( + [ + { + developerMetadataLookup: { + metadataKey: 'test-key', + }, + }, + ], + { + metadataKey: 'test-key', + metadataValue: 'updated-value', + location: { + sheetId: sheet.sheetId, + }, + visibility: 'DOCUMENT', + }, + 'metadataValue' + ); + }); + + it('can search developer metadata', async () => { + // create a metadata entry to search for + await sheet.createDeveloperMetadata({ + metadataKey: 'search-test-key', + metadataValue: 'search-test-value', + location: { sheetId: sheet.sheetId }, + visibility: 'DOCUMENT', + }); + + const results = await doc.searchDeveloperMetadata([ + { developerMetadataLookup: { metadataKey: 'search-test-key' } }, + ]); + + expect(results).toHaveLength(1); + expect(results[0].metadataKey).toBe('search-test-key'); + expect(results[0].metadataValue).toBe('search-test-value'); + + // clean up + await sheet.deleteDeveloperMetadata({ + developerMetadataLookup: { metadataKey: 'search-test-key' }, + }); + }); + + it('can load cells using a developer metadata filter', async () => { + // create row-level metadata on row 0 + await sheet.createDeveloperMetadata({ + metadataKey: 'row-meta-key', + metadataValue: 'row-meta-value', + location: { + dimensionRange: { + sheetId: sheet.sheetId, + dimension: 'ROWS', + startIndex: 0, + endIndex: 1, + }, + }, + visibility: 'DOCUMENT', + }); + + // load cells using developer metadata filter on doc + await doc.loadCells({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + + // load cells using developer metadata filter on sheet + sheet.resetLocalCache(true); + await sheet.loadCells({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + + // verify cells were loaded (row 0 should be accessible) + const cell = sheet.getCell(0, 0); + expect(cell).toBeTruthy(); + + // clean up + await sheet.deleteDeveloperMetadata({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + }); + + it('can delete developer metadata', async () => { + await sheet.deleteDeveloperMetadata({ + developerMetadataLookup: { + metadataKey: 'test-key', + }, + }); + }); + }); +}); diff --git a/src/test/worksheet-operations.test.ts b/src/test/worksheet-operations.test.ts new file mode 100644 index 0000000..2125be3 --- /dev/null +++ b/src/test/worksheet-operations.test.ts @@ -0,0 +1,747 @@ +import { + describe, expect, it, beforeAll, afterAll, afterEach, +} from 'vitest'; +import { setTimeout as delay } from 'timers/promises'; +import { ENV } from 'varlock/env'; + +import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; + +import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth'; + +const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth); + +describe('Worksheet data operations', () => { + // hitting rate limits when running tests on ci - so we add a short delay + if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + + describe('repeatCell - fill range with same cell data', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Repeat cell test ${+new Date()}`, + gridProperties: { rowCount: 5, columnCount: 5 }, + }); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can fill a range with the same value', async () => { + await sheet.repeatCell( + { + startRowIndex: 0, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 2, + }, + { + userEnteredValue: { stringValue: 'filled' }, + }, + 'userEnteredValue' + ); + + await sheet.loadCells('A1:B3'); + expect(sheet.getCellByA1('A1').value).toBe('filled'); + expect(sheet.getCellByA1('B1').value).toBe('filled'); + expect(sheet.getCellByA1('A2').value).toBe('filled'); + expect(sheet.getCellByA1('B2').value).toBe('filled'); + expect(sheet.getCellByA1('A3').value).toBe('filled'); + expect(sheet.getCellByA1('B3').value).toBe('filled'); + }); + + it('can fill a range with formatting', async () => { + await sheet.repeatCell( + { + startRowIndex: 0, endRowIndex: 2, startColumnIndex: 0, endColumnIndex: 2, + }, + { + userEnteredFormat: { + backgroundColor: { red: 1, green: 0, blue: 0 }, + }, + }, + 'userEnteredFormat.backgroundColor' + ); + + await sheet.loadCells('A1:B2'); + const cell = sheet.getCellByA1('A1'); + expect(cell.backgroundColor).toBeTruthy(); + expect(cell.backgroundColor!.red).toBe(1); + }); + }); + + describe('appendCells - append rows after last data', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Append cells test ${+new Date()}`, + headerValues: ['a', 'b'], + }); + await sheet.addRows([ + { a: 'existing1', b: 'existing2' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can append cells after existing data', async () => { + await sheet.appendCells( + [ + { values: [{ userEnteredValue: { stringValue: 'appended1' } }, { userEnteredValue: { stringValue: 'appended2' } }] }, + { values: [{ userEnteredValue: { stringValue: 'appended3' } }, { userEnteredValue: { stringValue: 'appended4' } }] }, + ], + 'userEnteredValue' + ); + + await sheet.loadCells('A3:B4'); + expect(sheet.getCellByA1('A3').value).toBe('appended1'); + expect(sheet.getCellByA1('B3').value).toBe('appended2'); + expect(sheet.getCellByA1('A4').value).toBe('appended3'); + expect(sheet.getCellByA1('B4').value).toBe('appended4'); + }); + }); + + describe('updateDimensionProperties - set row/column size and visibility', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Dimension props test ${+new Date()}`, + gridProperties: { rowCount: 10, columnCount: 5 }, + }); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can set column pixel size', async () => { + await sheet.updateDimensionProperties( + 'COLUMNS', + { pixelSize: 200 }, + { startIndex: 0, endIndex: 2 } + ); + // if it doesn't throw, the API accepted it + }); + + it('can set row pixel size', async () => { + await sheet.updateDimensionProperties( + 'ROWS', + { pixelSize: 50 }, + { startIndex: 0, endIndex: 3 } + ); + }); + + it('can hide rows', async () => { + await sheet.updateDimensionProperties( + 'ROWS', + { hiddenByUser: true }, + { startIndex: 5, endIndex: 7 } + ); + }); + }); + + describe('getCellsInRange - read cell values by A1 range', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Get cells in range test ${+new Date()}`, + headerValues: ['col1', 'col2', 'col3'], + }); + await sheet.addRows([ + { col1: 'a', col2: 'b', col3: 'c' }, + { col1: 'd', col2: 'e', col3: 'f' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can get cells in a range', async () => { + const values = await sheet.getCellsInRange('A1:C3'); + expect(values).toBeTruthy(); + expect(values.length).toBe(3); // 3 rows (header + 2 data) + expect(values[0]).toEqual(['col1', 'col2', 'col3']); + expect(values[1]).toEqual(['a', 'b', 'c']); + expect(values[2]).toEqual(['d', 'e', 'f']); + }); + + it('can get a single row', async () => { + const values = await sheet.getCellsInRange('A2:C2'); + expect(values).toBeTruthy(); + expect(values.length).toBe(1); + expect(values[0]).toEqual(['a', 'b', 'c']); + }); + }); + + describe('batchGetCellsInRange - read multiple ranges at once', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Batch get cells test ${+new Date()}`, + headerValues: ['x', 'y', 'z'], + }); + await sheet.addRows([ + { x: '1', y: '2', z: '3' }, + { x: '4', y: '5', z: '6' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can get multiple ranges at once', async () => { + const results = await sheet.batchGetCellsInRange(['A1:A3', 'C1:C3']); + expect(results).toBeTruthy(); + expect(results.length).toBe(2); + + // first range: column A + expect(results[0].length).toBe(3); + expect(results[0][0]).toEqual(['x']); + expect(results[0][1]).toEqual(['1']); + expect(results[0][2]).toEqual(['4']); + + // second range: column C + expect(results[1].length).toBe(3); + expect(results[1][0]).toEqual(['z']); + expect(results[1][1]).toEqual(['3']); + expect(results[1][2]).toEqual(['6']); + }); + }); + + describe('autoResizeDimensions - auto-resize columns/rows to fit content', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Auto resize test ${+new Date()}`, + headerValues: ['short', 'a much longer column header value'], + }); + }); + afterAll(async () => { + await sheet.delete(); + }); + + it('can auto-resize all columns', async () => { + await sheet.autoResizeDimensions('COLUMNS'); + }); + + it('can auto-resize a specific range of columns', async () => { + await sheet.autoResizeDimensions('COLUMNS', { + startIndex: 0, + endIndex: 1, + }); + }); + }); + + describe('pasteData - insert data from delimited string', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Paste data test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + gridProperties: { rowCount: 10, columnCount: 5 }, + }); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can paste comma-delimited data at a coordinate', async () => { + const data = 'value1,value2,value3\nvalue4,value5,value6'; + await sheet.pasteData( + { rowIndex: 1, columnIndex: 0 }, + data, + ',' + ); + + await sheet.loadCells('A2:C3'); + expect(sheet.getCellByA1('A2').value).toEqual('value1'); + expect(sheet.getCellByA1('B2').value).toEqual('value2'); + expect(sheet.getCellByA1('C2').value).toEqual('value3'); + expect(sheet.getCellByA1('A3').value).toEqual('value4'); + expect(sheet.getCellByA1('B3').value).toEqual('value5'); + expect(sheet.getCellByA1('C3').value).toEqual('value6'); + }); + + it('can paste tab-delimited data at a coordinate', async () => { + const data = 'tab1\ttab2\ttab3\ntab4\ttab5\ttab6'; + await sheet.pasteData( + { rowIndex: 4, columnIndex: 0 }, + data, + '\t' + ); + + await sheet.loadCells('A5:C6'); + expect(sheet.getCellByA1('A5').value).toEqual('tab1'); + expect(sheet.getCellByA1('B5').value).toEqual('tab2'); + expect(sheet.getCellByA1('C5').value).toEqual('tab3'); + expect(sheet.getCellByA1('A6').value).toEqual('tab4'); + expect(sheet.getCellByA1('B6').value).toEqual('tab5'); + expect(sheet.getCellByA1('C6').value).toEqual('tab6'); + }); + + it('can paste data with PASTE_VALUES type', async () => { + const data = 'numeric123,plain text'; + await sheet.pasteData( + { rowIndex: 7, columnIndex: 0 }, + data, + ',', + 'PASTE_VALUES' + ); + + await sheet.loadCells('A8:B8'); + // With PASTE_VALUES, values are pasted as-is + expect(sheet.getCellByA1('A8').value).toEqual('numeric123'); + expect(sheet.getCellByA1('B8').value).toEqual('plain text'); + }); + }); + + describe('appendDimension - append rows or columns to sheet', () => { + let sheet: GoogleSpreadsheetWorksheet; + const initialRowCount = 10; + const initialColumnCount = 5; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Append dimension test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + gridProperties: { rowCount: initialRowCount, columnCount: initialColumnCount }, + }); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can append rows to the sheet', async () => { + const rowsToAppend = 5; + await sheet.appendDimension('ROWS', rowsToAppend); + + // Reload sheet info to get updated properties + await doc.loadInfo(); + const updatedSheet = doc.sheetsById[sheet.sheetId]; + expect(updatedSheet.rowCount).toEqual(initialRowCount + rowsToAppend); + }); + + it('can append columns to the sheet', async () => { + const columnsToAppend = 3; + await sheet.appendDimension('COLUMNS', columnsToAppend); + + // Reload sheet info to get updated properties + await doc.loadInfo(); + const updatedSheet = doc.sheetsById[sheet.sheetId]; + expect(updatedSheet.columnCount).toEqual(initialColumnCount + columnsToAppend); + }); + }); + + describe('textToColumns - split delimited text into columns', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Text to columns test ${+new Date()}`, + headerValues: ['data', 'other'], + gridProperties: { rowCount: 10, columnCount: 10 }, + }); + // Add some comma-separated data in column A + await sheet.addRows([ + { data: 'a,b,c', other: 'x' }, + { data: 'd,e,f', other: 'y' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can split comma-delimited text into multiple columns', async () => { + await sheet.textToColumns( + { + startColumnIndex: 0, endColumnIndex: 1, startRowIndex: 1, endRowIndex: 3, + }, + 'COMMA' + ); + + await sheet.loadCells('A2:C3'); + expect(sheet.getCellByA1('A2').value).toEqual('a'); + expect(sheet.getCellByA1('B2').value).toEqual('b'); + expect(sheet.getCellByA1('C2').value).toEqual('c'); + expect(sheet.getCellByA1('A3').value).toEqual('d'); + expect(sheet.getCellByA1('B3').value).toEqual('e'); + expect(sheet.getCellByA1('C3').value).toEqual('f'); + }); + }); + + describe('deleteRange - delete cells and shift remaining', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Delete range test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + }); + await sheet.addRows([ + { a: '1', b: '2', c: '3' }, + { a: '4', b: '5', c: '6' }, + { a: '7', b: '8', c: '9' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can delete a range and shift cells up', async () => { + await sheet.deleteRange( + { + startRowIndex: 2, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 3, + }, + 'ROWS' + ); + + const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); + expect(rows.length).toEqual(2); + expect(rows[0].get('a')).toEqual('1'); + expect(rows[1].get('a')).toEqual('7'); + }); + }); + + describe('deleteDimension - delete rows or columns', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Delete dimension test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + }); + await sheet.addRows([ + { a: '1', b: '2', c: '3' }, + { a: '4', b: '5', c: '6' }, + { a: '7', b: '8', c: '9' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can delete rows', async () => { + await sheet.deleteDimension('ROWS', { startIndex: 2, endIndex: 3 }); + + const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); + expect(rows.length).toEqual(2); + expect(rows[0].get('a')).toEqual('1'); + expect(rows[1].get('a')).toEqual('7'); + }); + }); + + describe('moveDimension - move rows or columns', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Move dimension test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + }); + await sheet.addRows([ + { a: '1', b: '2', c: '3' }, + { a: '4', b: '5', c: '6' }, + { a: '7', b: '8', c: '9' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can move rows to a different position', async () => { + // Move row at index 1 (first data row) to after row at index 3 + await sheet.moveDimension('ROWS', { startIndex: 1, endIndex: 2 }, 4); + + const rows = await sheet.getRows<{ a: string, b: string, c: string }>(); + expect(rows[0].get('a')).toEqual('4'); + expect(rows[1].get('a')).toEqual('7'); + expect(rows[2].get('a')).toEqual('1'); + }); + }); + + describe('sortRange - sort data in a range', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Sort range test ${+new Date()}`, + headerValues: ['name', 'age'], + }); + await sheet.addRows([ + { name: 'Charlie', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 35 }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can sort a range by column', async () => { + await sheet.sortRange( + { + startRowIndex: 1, endRowIndex: 4, startColumnIndex: 0, endColumnIndex: 2, + }, + [{ dimensionIndex: 0, sortOrder: 'ASCENDING' }] + ); + + const rows = await sheet.getRows<{ name: string, age: string }>(); + expect(rows[0].get('name')).toEqual('Alice'); + expect(rows[1].get('name')).toEqual('Bob'); + expect(rows[2].get('name')).toEqual('Charlie'); + }); + }); + + describe('trimWhitespace - remove leading/trailing spaces', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Trim whitespace test ${+new Date()}`, + headerValues: ['text'], + }); + await sheet.addRows([ + { text: ' hello ' }, + { text: ' world ' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can trim whitespace from cells', async () => { + await sheet.trimWhitespace({ + startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, + }); + + const rows = await sheet.getRows<{ text: string }>(); + expect(rows[0].get('text')).toEqual('hello'); + expect(rows[1].get('text')).toEqual('world'); + }); + }); + + describe('deleteDuplicates - remove duplicate rows', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Delete duplicates test ${+new Date()}`, + headerValues: ['name', 'city'], + }); + await sheet.addRows([ + { name: 'Alice', city: 'NYC' }, + { name: 'Bob', city: 'LA' }, + { name: 'Alice', city: 'NYC' }, + { name: 'Charlie', city: 'Chicago' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can remove duplicate rows', async () => { + await sheet.deleteDuplicates({ + startRowIndex: 1, endRowIndex: 5, startColumnIndex: 0, endColumnIndex: 2, + }); + + const rows = await sheet.getRows<{ name: string, city: string }>(); + expect(rows.length).toEqual(3); + expect(rows[0].get('name')).toEqual('Alice'); + expect(rows[1].get('name')).toEqual('Bob'); + expect(rows[2].get('name')).toEqual('Charlie'); + }); + }); + + describe('copyPaste - copy and paste cells', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Copy paste test ${+new Date()}`, + headerValues: ['a', 'b', 'c', 'd'], + }); + await sheet.addRows([ + { + a: '1', b: '2', c: '', d: '', + }, + { + a: '3', b: '4', c: '', d: '', + }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can copy and paste a range', async () => { + await sheet.copyPaste( + { + startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 2, + }, + { + startRowIndex: 1, endRowIndex: 3, startColumnIndex: 2, endColumnIndex: 4, + } + ); + + await sheet.loadCells('A2:D3'); + expect(sheet.getCellByA1('C2').value).toEqual(1); + expect(sheet.getCellByA1('D2').value).toEqual(2); + expect(sheet.getCellByA1('C3').value).toEqual(3); + expect(sheet.getCellByA1('D3').value).toEqual(4); + }); + }); + + describe('cutPaste - cut and paste cells', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Cut paste test ${+new Date()}`, + headerValues: ['a', 'b', 'c'], + }); + await sheet.addRows([ + { a: '1', b: '2', c: '' }, + { a: '3', b: '4', c: '' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can cut and paste a range', async () => { + await sheet.cutPaste( + { + startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, + }, + { rowIndex: 1, columnIndex: 2 } + ); + + await sheet.loadCells('A2:C3'); + expect(sheet.getCellByA1('A2').value).toBeNull(); + expect(sheet.getCellByA1('A3').value).toBeNull(); + expect(sheet.getCellByA1('C2').value).toEqual(1); + expect(sheet.getCellByA1('C3').value).toEqual(3); + }); + }); + + describe('autoFill - fill cells with pattern', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Auto fill test ${+new Date()}`, + headerValues: ['numbers'], + }); + await sheet.addRows([ + { numbers: 1 }, + { numbers: 2 }, + { numbers: '' }, + { numbers: '' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can autofill cells based on pattern', async () => { + await sheet.autoFill({ + source: { + startRowIndex: 1, endRowIndex: 3, startColumnIndex: 0, endColumnIndex: 1, + }, + dimension: 'ROWS', + fillLength: 2, + }); + + const rows = await sheet.getRows<{ numbers: string }>(); + expect(rows[0].get('numbers')).toEqual('1'); + expect(rows[1].get('numbers')).toEqual('2'); + expect(rows[2].get('numbers')).toEqual('3'); + expect(rows[3].get('numbers')).toEqual('4'); + }); + }); + + describe('findReplace - find and replace text', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Find replace test ${+new Date()}`, + headerValues: ['text'], + }); + await sheet.addRows([ + { text: 'hello world' }, + { text: 'hello there' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can find and replace text in cells', async () => { + await sheet.findReplace('hello', 'hi'); + + const rows = await sheet.getRows<{ text: string }>(); + expect(rows[0].get('text')).toEqual('hi world'); + expect(rows[1].get('text')).toEqual('hi there'); + }); + }); + + describe('randomizeRange - shuffle rows', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ + title: `Randomize range test ${+new Date()}`, + headerValues: ['number'], + }); + await sheet.addRows([ + { number: 1 }, + { number: 2 }, + { number: 3 }, + { number: 4 }, + { number: 5 }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + it('can randomize rows in a range', async () => { + const rowsBefore = await sheet.getRows<{ number: string }>(); + const valuesBefore = rowsBefore.map((r) => r.get('number')); + + await sheet.randomizeRange({ + startRowIndex: 1, endRowIndex: 6, startColumnIndex: 0, endColumnIndex: 1, + }); + + const rowsAfter = await sheet.getRows<{ number: string }>(); + const valuesAfter = rowsAfter.map((r) => r.get('number')); + + // Check that all values are still present (same set) + expect(valuesAfter.sort()).toEqual(valuesBefore.sort()); + // In theory could be the same order, but very unlikely with 5 items + }); + }); +});