From cca91dd841d0c4ffd15670222a9758b496fc4013 Mon Sep 17 00:00:00 2001 From: "Adrien Minne (adrm)" Date: Tue, 5 Mar 2024 15:30:14 +0100 Subject: [PATCH] [IMP] tables: import table styles in xlsx This commit adds the conversion of table style from xlsx to o-spreadsheet. The xlsx pivots are now handled separately fro the tables, as they have a slightly different configuration. Task: 3789612 Part-of: odoo/o-spreadsheet#3799 --- src/types/xlsx.ts | 27 +++ src/xlsx/conversion/table_conversion.ts | 180 ++++-------------- src/xlsx/extraction/pivot_extractor.ts | 68 +++++-- src/xlsx/extraction/sheet_extractor.ts | 6 +- tests/__xlsx__/xlsx_demo_data.xlsx | Bin 91008 -> 91754 bytes tests/xlsx/xlsx_import.test.ts | 234 ++++++------------------ 6 files changed, 182 insertions(+), 333 deletions(-) diff --git a/src/types/xlsx.ts b/src/types/xlsx.ts index d4b38151c3..49c36bc544 100644 --- a/src/types/xlsx.ts +++ b/src/types/xlsx.ts @@ -35,6 +35,9 @@ import { ExcelFigureSize } from "./figure"; * - merge (string): §18.3.1.55 (mergeCell) * - number format (XLSXNumFormat) : §18.8.30 (numFmt) * - outline properties (XLSXOutlineProperties): §18.3.1.31 (outlinePr) + * - pivot table (XLSXPivotTable): §18.10.1.73 (pivotTableDefinition) + * - pivot table location (XLSXPivotTableLocation): §18.10.1.49 (location) + * - pivot table style info (XLSXPivotTableStyleInfo): §18.10.7.74 (pivotTableStyleInfo) * - rows (XLSXRow): §18.3.1.73 (row) * - sheet (XLSXWorksheet): §18.3.1.99 (worksheet) * - sheet format (XLSXSheetFormat): §18.3.1.81 (sheetFormatPr) @@ -230,6 +233,7 @@ export interface XLSXWorksheet { figures: XLSXFigure[]; hyperlinks: XLSXHyperLink[]; tables: XLSXTable[]; + pivotTables: XLSXPivotTable[]; } export interface XLSXSheetView { @@ -578,6 +582,29 @@ export interface XLSXTableStyleInfo { showColumnStripes?: boolean; } +export interface XLSXPivotTable { + name: string; + rowGrandTotals: boolean; + location: XLSXPivotTableLocation; + style?: XLSXPivotTableStyleInfo; +} + +export interface XLSXPivotTableLocation { + ref: string; + firstHeaderRow: number; + firstDataRow: number; + firstDataCol: number; +} + +export interface XLSXPivotTableStyleInfo { + name: string; + showRowHeaders: boolean; + showColHeaders: boolean; + showRowStripes: boolean; + showColStripes: boolean; + showLastColumn?: boolean; +} + export interface XLSXTableCol { name: string; id: string; diff --git a/src/xlsx/conversion/table_conversion.ts b/src/xlsx/conversion/table_conversion.ts index 73643bea55..5b355b1248 100644 --- a/src/xlsx/conversion/table_conversion.ts +++ b/src/xlsx/conversion/table_conversion.ts @@ -1,161 +1,61 @@ -import { deepEquals, positions, toCartesian, toXC, toZone, zoneToXc } from "../../helpers"; -import { BorderDescr, CellData, Style, WorkbookData, Zone } from "../../types"; +import { positions, toCartesian, toXC, toZone, zoneToXc } from "../../helpers"; +import { DEFAULT_TABLE_CONFIG, TABLE_PRESETS } from "../../helpers/table_presets"; +import { TableConfig, WorkbookData } from "../../types"; import { CellErrorType } from "../../types/errors"; import { SheetData } from "../../types/workbook_data"; -import { XLSXImportData, XLSXTable, XLSXWorksheet } from "../../types/xlsx"; -import { arrayToObject, objectToArray } from "../helpers/misc"; - -type CellMap = { [key: string]: CellData | undefined }; - -export const TABLE_HEADER_STYLE: Style = { - fillColor: "#000000", - textColor: "#ffffff", - bold: true, -}; - -export const TABLE_HIGHLIGHTED_CELL_STYLE: Style = { - bold: true, -}; - -export const TABLE_BORDER_STYLE: BorderDescr = { style: "thin", color: "#000000FF" }; +import { XLSXImportData, XLSXPivotTable, XLSXTable, XLSXWorksheet } from "../../types/xlsx"; /** - * Convert the imported XLSX tables. - * - * We will create a Table if the imported table have filters, then apply a style in all the cells of the table - * and convert the table-specific formula references into standard references. + * Convert the imported XLSX tables and pivots convert the table-specific formula references into standard references. * * Change the converted data in-place. */ export function convertTables(convertedData: WorkbookData, xlsxData: XLSXImportData) { for (const xlsxSheet of xlsxData.sheets) { + const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName); + if (!sheet) continue; + if (!sheet.tables) sheet.tables = []; + for (const table of xlsxSheet.tables) { - const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName); - if (!sheet || !table.autoFilter) continue; - if (!sheet.tables) sheet.tables = []; - sheet.tables.push({ range: table.ref }); + sheet.tables.push({ range: table.ref, config: convertTableConfig(table) }); + } + + for (const pivotTable of xlsxSheet.pivotTables) { + sheet.tables.push({ + range: pivotTable.location.ref, + config: convertPivotTableConfig(pivotTable), + }); } } - applyTableStyle(convertedData, xlsxData); convertTableFormulaReferences(convertedData.sheets, xlsxData.sheets); } -/** - * Apply a style to all the cells that are in a table, and add the created styles in the converted data. - * - * In XLSXs, the style of the cells of a table are not directly in the sheet, but rather deduced from the style of - * the table that is defined in the table's XML file. The style of the table is a string referencing a standard style - * defined in the OpenXML specifications. As there are 80+ different styles, we won't implement every one of them but - * we will just define a style that will be used for all the imported tables. - */ -function applyTableStyle(convertedData: WorkbookData, xlsxData: XLSXImportData) { - const styles = objectToArray(convertedData.styles); - const borders = objectToArray(convertedData.borders); - - for (let xlsxSheet of xlsxData.sheets) { - for (let table of xlsxSheet.tables) { - const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName); - if (!sheet) continue; - const tableZone = toZone(table.ref); - - // Table style - for (let i = 0; i < table.headerRowCount; i++) { - applyStyleToZone( - TABLE_HEADER_STYLE, - { ...tableZone, bottom: tableZone.top + i }, - sheet.cells, - styles - ); - } - for (let i = 0; i < table.totalsRowCount; i++) { - applyStyleToZone( - TABLE_HIGHLIGHTED_CELL_STYLE, - { ...tableZone, top: tableZone.bottom - i }, - sheet.cells, - styles - ); - } - if (table.style?.showFirstColumn) { - applyStyleToZone( - TABLE_HIGHLIGHTED_CELL_STYLE, - { ...tableZone, right: tableZone.left }, - sheet.cells, - styles - ); - } - if (table.style?.showLastColumn) { - applyStyleToZone( - TABLE_HIGHLIGHTED_CELL_STYLE, - { ...tableZone, left: tableZone.right }, - sheet.cells, - styles - ); - } - - // Table borders - // Borders at : table outline + col(/row) if showColumnStripes(/showRowStripes) + border above totalRow - for (let col = tableZone.left; col <= tableZone.right; col++) { - for (let row = tableZone.top; row <= tableZone.bottom; row++) { - const xc = toXC(col, row); - const cell = sheet.cells[xc]; - const border = { - left: - col === tableZone.left || table.style?.showColumnStripes - ? TABLE_BORDER_STYLE - : undefined, - right: col === tableZone.right ? TABLE_BORDER_STYLE : undefined, - top: - row === tableZone.top || - table.style?.showRowStripes || - row > tableZone.bottom - table.totalsRowCount - ? TABLE_BORDER_STYLE - : undefined, - bottom: row === tableZone.bottom ? TABLE_BORDER_STYLE : undefined, - }; - const newBorder = cell?.border ? { ...borders[cell.border], ...border } : border; - let borderIndex = borders.findIndex((border) => deepEquals(border, newBorder)); - if (borderIndex === -1) { - borderIndex = borders.length; - borders.push(newBorder); - } - if (cell) { - cell.border = borderIndex; - } else { - sheet.cells[xc] = { border: borderIndex }; - } - } - } - } - } - - convertedData.styles = arrayToObject(styles); - convertedData.borders = arrayToObject(borders); +function convertTableConfig(table: XLSXTable): TableConfig { + const styleId = table.style?.name || ""; + return { + hasFilters: table.autoFilter !== undefined, + numberOfHeaders: table.headerRowCount, + totalRow: table.totalsRowCount > 0, + firstColumn: table.style?.showFirstColumn || false, + lastColumn: table.style?.showLastColumn || false, + bandedRows: table.style?.showRowStripes || false, + bandedColumns: table.style?.showColumnStripes || false, + styleId: TABLE_PRESETS[styleId] ? styleId : DEFAULT_TABLE_CONFIG.styleId, + }; } -/** - * Apply a style to all the cells in the zone. The applied style WILL NOT overwrite values in existing style of the cell. - * - * If a style that was not in the styles array was applied, push it into the style array. - */ -function applyStyleToZone(appliedStyle: Style, zone: Zone, cells: CellMap, styles: Style[]) { - for (let col = zone.left; col <= zone.right; col++) { - for (let row = zone.top; row <= zone.bottom; row++) { - const xc = toXC(col, row); - const cell = cells[xc]; - const newStyle = cell?.style ? { ...styles[cell.style], ...appliedStyle } : appliedStyle; - let styleIndex = styles.findIndex((style) => deepEquals(style, newStyle)); - if (styleIndex === -1) { - styleIndex = styles.length; - styles.push(newStyle); - } - if (cell) { - cell.style = styleIndex; - } else { - cells[xc] = { style: styleIndex }; - } - } - } +function convertPivotTableConfig(pivotTable: XLSXPivotTable): TableConfig { + return { + hasFilters: false, + numberOfHeaders: pivotTable.location.firstDataRow, + totalRow: pivotTable.rowGrandTotals, + firstColumn: true, + lastColumn: pivotTable.style?.showLastColumn || false, + bandedRows: pivotTable.style?.showRowStripes || false, + bandedColumns: pivotTable.style?.showColStripes || false, + styleId: DEFAULT_TABLE_CONFIG.styleId, + }; } /** diff --git a/src/xlsx/extraction/pivot_extractor.ts b/src/xlsx/extraction/pivot_extractor.ts index 48edf00210..832a747875 100644 --- a/src/xlsx/extraction/pivot_extractor.ts +++ b/src/xlsx/extraction/pivot_extractor.ts @@ -1,31 +1,67 @@ -import { XLSXTable } from "../../types/xlsx"; +import { XLSXPivotTable, XLSXPivotTableLocation, XLSXPivotTableStyleInfo } from "../../types/xlsx"; import { XlsxBaseExtractor } from "./base_extractor"; /** * We don't really support pivot tables, we'll just extract them as Tables. */ export class XlsxPivotExtractor extends XlsxBaseExtractor { - getPivotTable(): XLSXTable { + getPivotTable(): XLSXPivotTable { return this.mapOnElements( // Use :root instead of "pivotTableDefinition" because others pivotTableDefinition elements are present inside the root // pivotTableDefinition elements. { query: ":root", parent: this.rootFile.file.xml }, - (pivotElement): XLSXTable => { + (pivotElement): XLSXPivotTable => { return { - displayName: this.extractAttr(pivotElement, "name", { required: true }).asString(), - id: this.extractAttr(pivotElement, "name", { required: true }).asString(), - ref: this.extractChildAttr(pivotElement, "location", "ref", { + name: this.extractAttr(pivotElement, "name", { required: true }).asString(), + rowGrandTotals: this.extractAttr(pivotElement, "rowGrandTotals", { + default: true, + }).asBool(), + location: this.extractPivotLocation(pivotElement), + style: this.extractPivotStyleInfo(pivotElement), + }; + } + )[0]; + } + + private extractPivotLocation(pivotElement: Element): XLSXPivotTableLocation { + return this.mapOnElements( + { query: "location", parent: pivotElement }, + (pivotStyleElement): XLSXPivotTableLocation => { + return { + ref: this.extractAttr(pivotStyleElement, "ref", { required: true }).asString(), + firstHeaderRow: this.extractAttr(pivotStyleElement, "firstHeaderRow", { + required: true, + }).asNum(), + firstDataRow: this.extractAttr(pivotStyleElement, "firstDataRow", { + required: true, + }).asNum(), + firstDataCol: this.extractAttr(pivotStyleElement, "firstDataCol", { + required: true, + }).asNum(), + }; + } + )[0]; + } + + private extractPivotStyleInfo(pivotElement: Element): XLSXPivotTableStyleInfo | undefined { + return this.mapOnElements( + { query: "pivotTableStyleInfo", parent: pivotElement }, + (pivotStyleElement): XLSXPivotTableStyleInfo => { + return { + name: this.extractAttr(pivotStyleElement, "name", { required: true }).asString(), + showRowHeaders: this.extractAttr(pivotStyleElement, "showRowHeaders", { + required: true, + }).asBool(), + showColHeaders: this.extractAttr(pivotStyleElement, "showColHeaders", { + required: true, + }).asBool(), + showRowStripes: this.extractAttr(pivotStyleElement, "showRowStripes", { + required: true, + }).asBool(), + showColStripes: this.extractAttr(pivotStyleElement, "showColStripes", { required: true, - }).asString()!, - headerRowCount: this.extractChildAttr(pivotElement, "location", "firstDataRow", { - default: 0, - }).asNum()!, - totalsRowCount: 1, - cols: [], - style: { - showFirstColumn: true, - showRowStripes: true, - }, + }).asBool(), + showLastColumn: this.extractAttr(pivotStyleElement, "showLastColumn")?.asBool(), }; } )[0]; diff --git a/src/xlsx/extraction/sheet_extractor.ts b/src/xlsx/extraction/sheet_extractor.ts index 279c5ab04c..bca7e2771b 100644 --- a/src/xlsx/extraction/sheet_extractor.ts +++ b/src/xlsx/extraction/sheet_extractor.ts @@ -8,6 +8,7 @@ import { XLSXHyperLink, XLSXImportFile, XLSXOutlineProperties, + XLSXPivotTable, XLSXRow, XLSXSheetFormat, XLSXSheetProperties, @@ -58,7 +59,8 @@ export class XlsxSheetExtractor extends XlsxBaseExtractor { cfs: this.extractConditionalFormats(), figures: this.extractFigures(sheetElement), hyperlinks: this.extractHyperLinks(sheetElement), - tables: [...this.extractTables(sheetElement), ...this.extractPivotTables()], + tables: this.extractTables(sheetElement), + pivotTables: this.extractPivotTables(), isVisible: sheetWorkbookInfo.state === "visible" ? true : false, }; } @@ -191,7 +193,7 @@ export class XlsxSheetExtractor extends XlsxBaseExtractor { ); } - private extractPivotTables(): XLSXTable[] { + private extractPivotTables(): XLSXPivotTable[] { try { return Object.values(this.relationships) .filter((relationship) => relationship.type.endsWith("pivotTable")) diff --git a/tests/__xlsx__/xlsx_demo_data.xlsx b/tests/__xlsx__/xlsx_demo_data.xlsx index 30f6f2af15b1476553407898c40e0a800a1d778a..ea43cac5ecaba8d8b3044fbf37b3a2c9cb71599b 100644 GIT binary patch delta 20587 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 delta 19824 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 { } }); - test("tables with headers are imported as tables", () => { + test("tables are imported", () => { const sheet = getWorkbookSheet("jestTable", convertedData)!; - expect(sheet.tables).toHaveLength(3); - expect(sheet.tables[0]).toEqual({ range: "C3:J6" }); - expect(sheet.tables[1]).toEqual({ range: "C11:D12" }); - expect(sheet.tables[2]).toEqual({ range: "C30:D32" }); + expect(sheet.tables).toHaveLength(10); }); test("rows filtered by a table filter are hidden", () => { const sheet = getWorkbookSheet("jestTable", convertedData)!; - expect(sheet.tables[2]).toEqual({ range: "C30:D32" }); expect(sheet.cells["C31"]?.content).toEqual("Hidden"); expect(sheet.rows[30].isHidden).toBeTruthy(); }); @@ -526,139 +511,63 @@ describe("Import xlsx data", () => { tableTestSheet = getWorkbookSheet("jestTable", convertedData)!; }); - test("Can display basic table style (borders on table outline)", () => { - const tableZone = toZone("C8:D9"); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.left, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ - top: TABLE_BORDER_STYLE, - bottom: undefined, - left: TABLE_BORDER_STYLE, - right: undefined, - }); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.right, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ - top: TABLE_BORDER_STYLE, - bottom: undefined, - right: TABLE_BORDER_STYLE, - left: undefined, - }); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.left, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ - bottom: TABLE_BORDER_STYLE, - top: undefined, - left: TABLE_BORDER_STYLE, - right: undefined, - }); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.right, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ - bottom: TABLE_BORDER_STYLE, - top: undefined, - right: TABLE_BORDER_STYLE, - left: undefined, + test("Can import basic table style", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C8:D9")!; + expect(table?.config).toMatchObject({ + numberOfHeaders: 0, + totalRow: false, + bandedRows: false, + bandedColumns: false, + firstColumn: false, + lastColumn: false, + hasFilters: false, + styleId: "TableStyleLight8", }); }); - test("Can display header style", () => { - const tableZone = toZone("C11:D12"); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.left, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HEADER_STYLE); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.right, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HEADER_STYLE); + test("Can import table style id", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C3:J6"); + expect(table?.config).toMatchObject({ styleId: "TableStyleLight10" }); }); - test("Can highlight first table column", () => { - const tableZone = toZone("C14:D15"); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.left, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HIGHLIGHTED_CELL_STYLE); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.left, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HIGHLIGHTED_CELL_STYLE); + test("Can import table with headers", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C11:D12"); + expect(table?.config).toMatchObject({ numberOfHeaders: 1 }); + }); + + test("Can import table with first column style", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C14:D15"); + expect(table?.config).toMatchObject({ firstColumn: true }); }); test("Can highlight last table column", () => { - const tableZone = toZone("C17:D18"); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.right, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HIGHLIGHTED_CELL_STYLE); - expect( - getWorkbookCellStyle( - getWorkbookCell(tableZone.right, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject(TABLE_HIGHLIGHTED_CELL_STYLE); + const table = tableTestSheet.tables.find((table) => table.range === "C17:D18"); + expect(table?.config).toMatchObject({ lastColumn: true }); + }); + + test("Can import table with banded rows", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C20:D21"); + expect(table?.config).toMatchObject({ bandedRows: true }); + }); + + test("Can import table with banded columns", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C23:D24"); + expect(table?.config).toMatchObject({ bandedColumns: true }); }); - test("Can display banded rows (borders between rows)", () => { - const tableZone = toZone("C20:D21"); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.left, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ top: TABLE_BORDER_STYLE }); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.right, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ top: TABLE_BORDER_STYLE }); + test("Can import table with total rows", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C26:D28"); + expect(table?.config).toMatchObject({ totalRow: true }); }); - test("Can display banded columns (borders between columns)", () => { - const tableZone = toZone("C23:D24"); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.right, tableZone.top, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ left: TABLE_BORDER_STYLE }); - expect( - getWorkbookCellBorder( - getWorkbookCell(tableZone.right, tableZone.bottom, tableTestSheet)!, - convertedData - ) - ).toMatchObject({ left: TABLE_BORDER_STYLE }); + test("Can import table with filters", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C30:D32"); + expect(table?.config).toMatchObject({ hasFilters: true }); }); - test("Can display total row", () => { - const tableZone = toZone("C26:D28"); - expect(getWorkbookCell(tableZone.left, tableZone.bottom, tableTestSheet)!.content).toEqual( - "Total" - ); + test("Table with custom style will be converted to default table style", () => { + const table = tableTestSheet.tables.find((table) => table.range === "C34:D35"); + expect(table?.config).toMatchObject({ styleId: DEFAULT_TABLE_CONFIG.styleId }); }); }); @@ -687,45 +596,20 @@ describe("Import xlsx data", () => { expect(testSheet.cells["J5"]?.content).toEqual("=E3"); }); - // We just import pivots as a Table (cells with some styling/borders). + // We just import pivots as a Table test("can import pivots", () => { - // Test pivot coordinates are in A1 const testSheet = getWorkbookSheet("jestPivot", convertedData)!; - const pivotZone = toZone("C3:L21"); - - for (let col = pivotZone.left; col <= pivotZone.right; col++) { - for (let row = pivotZone.top; row <= pivotZone.bottom; row++) { - // Special style for headers and first column - let expectedStyle: Style | undefined = undefined; - if (row === pivotZone.top || row === pivotZone.top + 1) { - expectedStyle = TABLE_HEADER_STYLE; - } else if (col === pivotZone.left) { - expectedStyle = TABLE_HIGHLIGHTED_CELL_STYLE; - } - - // Borders = outline of the table + top border between each row - const expectedBorder: Border = {}; - if (col === pivotZone.right) { - expectedBorder.right = TABLE_BORDER_STYLE; - } - if (col === pivotZone.left) { - expectedBorder.left = TABLE_BORDER_STYLE; - } - if (row === pivotZone.bottom) { - expectedBorder.bottom = TABLE_BORDER_STYLE; - } - expectedBorder.top = TABLE_BORDER_STYLE; - - if (expectedStyle) { - expect( - getWorkbookCellStyle(getWorkbookCell(col, row, testSheet)!, convertedData) - ).toMatchObject(expectedStyle); - } - expect(getWorkbookCellBorder(getWorkbookCell(col, row, testSheet)!, convertedData)).toEqual( - expectedBorder - ); - } - } + const table = testSheet.tables[0]; + expect(table.range).toEqual("C3:L21"); + expect(table.config).toMatchObject({ + numberOfHeaders: 2, + totalRow: true, + firstColumn: true, + lastColumn: true, + bandedRows: false, + bandedColumns: false, + hasFilters: false, + }); }); test.each([