diff --git a/bigframes/display/table_widget.css b/bigframes/display/table_widget.css index 34134b043d..b02caa004e 100644 --- a/bigframes/display/table_widget.css +++ b/bigframes/display/table_widget.css @@ -14,24 +14,83 @@ * limitations under the License. */ -.bigframes-widget { +/* Increase specificity to override framework styles without !important */ +.bigframes-widget.bigframes-widget { + /* Default Light Mode Variables */ + --bf-bg: white; + --bf-border-color: #ccc; + --bf-error-bg: #fbe; + --bf-error-border: red; + --bf-error-fg: black; + --bf-fg: black; + --bf-header-bg: #f5f5f5; + --bf-null-fg: gray; + --bf-row-even-bg: #f5f5f5; + --bf-row-odd-bg: white; + + background-color: var(--bf-bg); + box-sizing: border-box; + color: var(--bf-fg); display: flex; flex-direction: column; + font-family: + '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', sans-serif; + margin: 0; + padding: 0; +} + +.bigframes-widget * { + box-sizing: border-box; +} + +/* Dark Mode Overrides: + * 1. @media (prefers-color-scheme: dark) - System-wide dark mode + * 2. .bigframes-dark-mode - Explicit class for VSCode theme detection + * 3. html[theme="dark"], body[data-theme="dark"] - Colab/Pantheon manual override + */ +@media (prefers-color-scheme: dark) { + .bigframes-widget.bigframes-widget { + --bf-bg: var(--vscode-editor-background, #202124); + --bf-border-color: #444; + --bf-error-bg: #511; + --bf-error-border: #f88; + --bf-error-fg: #fcc; + --bf-fg: white; + --bf-header-bg: var(--vscode-editor-background, black); + --bf-null-fg: #aaa; + --bf-row-even-bg: #202124; + --bf-row-odd-bg: #383838; + } +} + +.bigframes-widget.bigframes-dark-mode.bigframes-dark-mode, +html[theme='dark'] .bigframes-widget.bigframes-widget, +body[data-theme='dark'] .bigframes-widget.bigframes-widget { + --bf-bg: var(--vscode-editor-background, #202124); + --bf-border-color: #444; + --bf-error-bg: #511; + --bf-error-border: #f88; + --bf-error-fg: #fcc; + --bf-fg: white; + --bf-header-bg: var(--vscode-editor-background, black); + --bf-null-fg: #aaa; + --bf-row-even-bg: #202124; + --bf-row-odd-bg: #383838; } .bigframes-widget .table-container { + background-color: var(--bf-bg); + margin: 0; max-height: 620px; overflow: auto; + padding: 0; } .bigframes-widget .footer { align-items: center; - /* TODO(b/460861328): We will support dark mode in a media selector once we - * determine how to override the background colors as well. */ - color: black; + background-color: var(--bf-bg); + color: var(--bf-fg); display: flex; - font-family: - "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", sans-serif; font-size: 0.8rem; justify-content: space-between; padding: 8px; @@ -70,16 +129,28 @@ margin-right: 8px; } -.bigframes-widget table { +.bigframes-widget table.bigframes-widget-table, +.bigframes-widget table.dataframe { + background-color: var(--bf-bg); + border: 1px solid var(--bf-border-color); border-collapse: collapse; - /* TODO(b/460861328): We will support dark mode in a media selector once we - * determine how to override the background colors as well. */ - color: black; + border-spacing: 0; + box-shadow: none; + color: var(--bf-fg); + margin: 0; + outline: none; text-align: left; + width: auto; /* Fix stretching */ +} + +.bigframes-widget tr { + border: none; } .bigframes-widget th { - background-color: var(--colab-primary-surface-color, var(--jp-layout-color0)); + background-color: var(--bf-header-bg); + border: 1px solid var(--bf-border-color); + color: var(--bf-fg); padding: 0; position: sticky; text-align: left; @@ -87,6 +158,22 @@ z-index: 1; } +.bigframes-widget td { + border: 1px solid var(--bf-border-color); + color: var(--bf-fg); + padding: 0.5em; +} + +.bigframes-widget table tbody tr:nth-child(odd), +.bigframes-widget table tbody tr:nth-child(odd) td { + background-color: var(--bf-row-odd-bg); +} + +.bigframes-widget table tbody tr:nth-child(even), +.bigframes-widget table tbody tr:nth-child(even) td { + background-color: var(--bf-row-even-bg); +} + .bigframes-widget .bf-header-content { box-sizing: border-box; height: 100%; @@ -106,8 +193,13 @@ } .bigframes-widget button { + background-color: transparent; + border: 1px solid currentColor; + border-radius: 4px; + color: inherit; cursor: pointer; display: inline-block; + padding: 2px 8px; text-align: center; text-decoration: none; user-select: none; @@ -120,11 +212,10 @@ } .bigframes-widget .bigframes-error-message { - background-color: #fbe; - border: 1px solid red; + background-color: var(--bf-error-bg); + border: 1px solid var(--bf-error-border); border-radius: 4px; - font-family: - "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", sans-serif; + color: var(--bf-error-fg); font-size: 14px; margin-bottom: 8px; padding: 8px; @@ -139,14 +230,9 @@ } .bigframes-widget .null-value { - color: gray; -} - -.bigframes-widget td { - padding: 0.5em; + color: var(--bf-null-fg); } -.bigframes-widget tr:hover td, -.bigframes-widget td.row-hover { - background-color: var(--colab-hover-surface-color, var(--jp-layout-color2)); +.bigframes-widget .debug-info { + border-top: 1px solid var(--bf-border-color); } diff --git a/bigframes/display/table_widget.js b/bigframes/display/table_widget.js index 3944f48da7..40a027a8bc 100644 --- a/bigframes/display/table_widget.js +++ b/bigframes/display/table_widget.js @@ -15,19 +15,19 @@ */ const ModelProperty = { - ERROR_MESSAGE: "error_message", - ORDERABLE_COLUMNS: "orderable_columns", - PAGE: "page", - PAGE_SIZE: "page_size", - ROW_COUNT: "row_count", - SORT_CONTEXT: "sort_context", - TABLE_HTML: "table_html", + ERROR_MESSAGE: 'error_message', + ORDERABLE_COLUMNS: 'orderable_columns', + PAGE: 'page', + PAGE_SIZE: 'page_size', + ROW_COUNT: 'row_count', + SORT_CONTEXT: 'sort_context', + TABLE_HTML: 'table_html', }; const Event = { - CHANGE: "change", - CHANGE_TABLE_HTML: "change:table_html", - CLICK: "click", + CHANGE: 'change', + CHANGE_TABLE_HTML: 'change:table_html', + CLICK: 'click', }; /** @@ -35,297 +35,253 @@ const Event = { * @param {{ model: any, el: !HTMLElement }} props - The widget properties. */ function render({ model, el }) { - // Main container with a unique class for CSS scoping - el.classList.add("bigframes-widget"); - - // Add error message container at the top - const errorContainer = document.createElement("div"); - errorContainer.classList.add("error-message"); - - const tableContainer = document.createElement("div"); - tableContainer.classList.add("table-container"); - const footer = document.createElement("footer"); - footer.classList.add("footer"); - - // Pagination controls - const paginationContainer = document.createElement("div"); - paginationContainer.classList.add("pagination"); - const prevPage = document.createElement("button"); - const pageIndicator = document.createElement("span"); - pageIndicator.classList.add("page-indicator"); - const nextPage = document.createElement("button"); - const rowCountLabel = document.createElement("span"); - rowCountLabel.classList.add("row-count"); - - // Page size controls - const pageSizeContainer = document.createElement("div"); - pageSizeContainer.classList.add("page-size"); - const pageSizeLabel = document.createElement("label"); - const pageSizeInput = document.createElement("select"); - - prevPage.textContent = "<"; - nextPage.textContent = ">"; - pageSizeLabel.textContent = "Page size:"; - - // Page size options - const pageSizes = [10, 25, 50, 100]; - for (const size of pageSizes) { - const option = document.createElement("option"); - option.value = size; - option.textContent = size; - if (size === model.get(ModelProperty.PAGE_SIZE)) { - option.selected = true; - } - pageSizeInput.appendChild(option); - } - - /** Updates the footer states and page label based on the model. */ - function updateButtonStates() { - const currentPage = model.get(ModelProperty.PAGE); - const pageSize = model.get(ModelProperty.PAGE_SIZE); - const rowCount = model.get(ModelProperty.ROW_COUNT); - - if (rowCount === null) { - // Unknown total rows - rowCountLabel.textContent = "Total rows unknown"; - pageIndicator.textContent = `Page ${( - currentPage + 1 - ).toLocaleString()} of many`; - prevPage.disabled = currentPage === 0; - nextPage.disabled = false; // Allow navigation until we hit the end - } else if (rowCount === 0) { - // Empty dataset - rowCountLabel.textContent = "0 total rows"; - pageIndicator.textContent = "Page 1 of 1"; - prevPage.disabled = true; - nextPage.disabled = true; - } else { - // Known total rows - const totalPages = Math.ceil(rowCount / pageSize); - rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`; - pageIndicator.textContent = `Page ${( - currentPage + 1 - ).toLocaleString()} of ${totalPages.toLocaleString()}`; - prevPage.disabled = currentPage === 0; - nextPage.disabled = currentPage >= totalPages - 1; - } - pageSizeInput.value = pageSize; - } - - /** - * Handles page navigation. - * @param {number} direction - The direction to navigate (-1 for previous, 1 for next). - */ - function handlePageChange(direction) { - const currentPage = model.get(ModelProperty.PAGE); - model.set(ModelProperty.PAGE, currentPage + direction); - model.save_changes(); - } - - /** - * Handles page size changes. - * @param {number} newSize - The new page size. - */ - function handlePageSizeChange(newSize) { - model.set(ModelProperty.PAGE_SIZE, newSize); - model.set(ModelProperty.PAGE, 0); // Reset to first page - model.save_changes(); - } - - /** Updates the HTML in the table container and refreshes button states. */ - function handleTableHTMLChange() { - // Note: Using innerHTML is safe here because the content is generated - // by a trusted backend (DataFrame.to_html). - tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); - - // Get sortable columns from backend - const sortableColumns = model.get(ModelProperty.ORDERABLE_COLUMNS); - const currentSortContext = model.get(ModelProperty.SORT_CONTEXT) || []; - - const getSortIndex = (colName) => - currentSortContext.findIndex((item) => item.column === colName); - - // Add click handlers to column headers for sorting - const headers = tableContainer.querySelectorAll("th"); - headers.forEach((header) => { - const headerDiv = header.querySelector("div"); - const columnName = headerDiv.textContent.trim(); - - // Only add sorting UI for sortable columns - if (columnName && sortableColumns.includes(columnName)) { - header.style.cursor = "pointer"; - - // Create a span for the indicator - const indicatorSpan = document.createElement("span"); - indicatorSpan.classList.add("sort-indicator"); - indicatorSpan.style.paddingLeft = "5px"; - - // Determine sort indicator and initial visibility - let indicator = "●"; // Default: unsorted (dot) - const sortIndex = getSortIndex(columnName); - - if (sortIndex !== -1) { - const isAscending = currentSortContext[sortIndex].ascending; - indicator = isAscending ? "▲" : "▼"; - indicatorSpan.style.visibility = "visible"; // Sorted arrows always visible - } else { - indicatorSpan.style.visibility = "hidden"; // Unsorted dot hidden by default - } - indicatorSpan.textContent = indicator; - - // Add indicator to the header, replacing the old one if it exists - const existingIndicator = headerDiv.querySelector(".sort-indicator"); - if (existingIndicator) { - headerDiv.removeChild(existingIndicator); - } - headerDiv.appendChild(indicatorSpan); - - // Add hover effects for unsorted columns only - header.addEventListener("mouseover", () => { - if (getSortIndex(columnName) === -1) { - indicatorSpan.style.visibility = "visible"; - } - }); - header.addEventListener("mouseout", () => { - if (getSortIndex(columnName) === -1) { - indicatorSpan.style.visibility = "hidden"; - } - }); - - // Add click handler for three-state toggle - header.addEventListener(Event.CLICK, (event) => { - const sortIndex = getSortIndex(columnName); - let newContext = [...currentSortContext]; - - if (event.shiftKey) { - if (sortIndex !== -1) { - // Already sorted. Toggle or Remove. - if (newContext[sortIndex].ascending) { - // Asc -> Desc - // Clone object to avoid mutation issues - newContext[sortIndex] = { - ...newContext[sortIndex], - ascending: false, - }; - } else { - // Desc -> Remove - newContext.splice(sortIndex, 1); - } - } else { - // Not sorted -> Append Asc - newContext.push({ column: columnName, ascending: true }); - } - } else { - // No shift key. Single column mode. - if (sortIndex !== -1 && newContext.length === 1) { - // Already only this column. Toggle or Remove. - if (newContext[sortIndex].ascending) { - newContext[sortIndex] = { - ...newContext[sortIndex], - ascending: false, - }; - } else { - newContext = []; - } - } else { - // Start fresh with this column - newContext = [{ column: columnName, ascending: true }]; - } - } - - model.set(ModelProperty.SORT_CONTEXT, newContext); - model.save_changes(); - }); - } - }); - - const table = tableContainer.querySelector("table"); - if (table) { - const tableBody = table.querySelector("tbody"); - - /** - * Handles row hover events. - * @param {!Event} event - The mouse event. - * @param {boolean} isHovering - True to add hover class, false to remove. - */ - function handleRowHover(event, isHovering) { - const cell = event.target.closest("td"); - if (cell) { - const row = cell.closest("tr"); - const origRowId = row.dataset.origRow; - if (origRowId) { - const allCellsInGroup = tableBody.querySelectorAll( - `tr[data-orig-row="${origRowId}"] td`, - ); - allCellsInGroup.forEach((c) => { - c.classList.toggle("row-hover", isHovering); - }); - } - } - } - - if (tableBody) { - tableBody.addEventListener("mouseover", (event) => - handleRowHover(event, true), - ); - tableBody.addEventListener("mouseout", (event) => - handleRowHover(event, false), - ); - } - } - - updateButtonStates(); - } - - // Add error message handler - function handleErrorMessageChange() { - const errorMsg = model.get(ModelProperty.ERROR_MESSAGE); - if (errorMsg) { - errorContainer.textContent = errorMsg; - errorContainer.style.display = "block"; - } else { - errorContainer.style.display = "none"; - } - } - - // Add event listeners - prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1)); - nextPage.addEventListener(Event.CLICK, () => handlePageChange(1)); - pageSizeInput.addEventListener(Event.CHANGE, (e) => { - const newSize = Number(e.target.value); - if (newSize) { - handlePageSizeChange(newSize); - } - }); - model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange); - model.on(`change:${ModelProperty.ROW_COUNT}`, updateButtonStates); - model.on(`change:${ModelProperty.ERROR_MESSAGE}`, handleErrorMessageChange); - model.on(`change:_initial_load_complete`, (val) => { - if (val) { - updateButtonStates(); - } - }); - model.on(`change:${ModelProperty.PAGE}`, updateButtonStates); - - // Assemble the DOM - paginationContainer.appendChild(prevPage); - paginationContainer.appendChild(pageIndicator); - paginationContainer.appendChild(nextPage); - - pageSizeContainer.appendChild(pageSizeLabel); - pageSizeContainer.appendChild(pageSizeInput); - - footer.appendChild(rowCountLabel); - footer.appendChild(paginationContainer); - footer.appendChild(pageSizeContainer); - - el.appendChild(errorContainer); - el.appendChild(tableContainer); - el.appendChild(footer); - - // Initial render - handleTableHTMLChange(); - handleErrorMessageChange(); + el.classList.add('bigframes-widget'); + + const errorContainer = document.createElement('div'); + errorContainer.classList.add('error-message'); + + const tableContainer = document.createElement('div'); + tableContainer.classList.add('table-container'); + const footer = document.createElement('footer'); + footer.classList.add('footer'); + + /** Detects theme and applies necessary style overrides. */ + function updateTheme() { + const body = document.body; + const isDark = + body.classList.contains('vscode-dark') || + body.classList.contains('theme-dark') || + body.dataset.theme === 'dark' || + body.getAttribute('data-vscode-theme-kind') === 'vscode-dark'; + + if (isDark) { + el.classList.add('bigframes-dark-mode'); + } else { + el.classList.remove('bigframes-dark-mode'); + } + } + + updateTheme(); + // Re-check after mount to ensure parent styling is applied. + setTimeout(updateTheme, 300); + + const observer = new MutationObserver(updateTheme); + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'data-theme', 'data-vscode-theme-kind'], + }); + + // Pagination controls + const paginationContainer = document.createElement('div'); + paginationContainer.classList.add('pagination'); + const prevPage = document.createElement('button'); + const pageIndicator = document.createElement('span'); + pageIndicator.classList.add('page-indicator'); + const nextPage = document.createElement('button'); + const rowCountLabel = document.createElement('span'); + rowCountLabel.classList.add('row-count'); + + // Page size controls + const pageSizeContainer = document.createElement('div'); + pageSizeContainer.classList.add('page-size'); + const pageSizeLabel = document.createElement('label'); + const pageSizeInput = document.createElement('select'); + + prevPage.textContent = '<'; + nextPage.textContent = '>'; + pageSizeLabel.textContent = 'Page size:'; + + const pageSizes = [10, 25, 50, 100]; + for (const size of pageSizes) { + const option = document.createElement('option'); + option.value = size; + option.textContent = size; + if (size === model.get(ModelProperty.PAGE_SIZE)) { + option.selected = true; + } + pageSizeInput.appendChild(option); + } + + function updateButtonStates() { + const currentPage = model.get(ModelProperty.PAGE); + const pageSize = model.get(ModelProperty.PAGE_SIZE); + const rowCount = model.get(ModelProperty.ROW_COUNT); + + if (rowCount === null) { + rowCountLabel.textContent = 'Total rows unknown'; + pageIndicator.textContent = `Page ${(currentPage + 1).toLocaleString()} of many`; + prevPage.disabled = currentPage === 0; + nextPage.disabled = false; + } else if (rowCount === 0) { + rowCountLabel.textContent = '0 total rows'; + pageIndicator.textContent = 'Page 1 of 1'; + prevPage.disabled = true; + nextPage.disabled = true; + } else { + const totalPages = Math.ceil(rowCount / pageSize); + rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`; + pageIndicator.textContent = `Page ${(currentPage + 1).toLocaleString()} of ${totalPages.toLocaleString()}`; + prevPage.disabled = currentPage === 0; + nextPage.disabled = currentPage >= totalPages - 1; + } + pageSizeInput.value = pageSize; + } + + function handlePageChange(direction) { + const currentPage = model.get(ModelProperty.PAGE); + model.set(ModelProperty.PAGE, currentPage + direction); + model.save_changes(); + } + + function handlePageSizeChange(newSize) { + model.set(ModelProperty.PAGE_SIZE, newSize); + model.set(ModelProperty.PAGE, 0); + model.save_changes(); + } + + function handleTableHTMLChange() { + tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); + + const sortableColumns = model.get(ModelProperty.ORDERABLE_COLUMNS); + const currentSortContext = model.get(ModelProperty.SORT_CONTEXT) || []; + + const getSortIndex = (colName) => + currentSortContext.findIndex((item) => item.column === colName); + + const headers = tableContainer.querySelectorAll('th'); + headers.forEach((header) => { + const headerDiv = header.querySelector('div'); + const columnName = headerDiv.textContent.trim(); + + if (columnName && sortableColumns.includes(columnName)) { + header.style.cursor = 'pointer'; + + const indicatorSpan = document.createElement('span'); + indicatorSpan.classList.add('sort-indicator'); + indicatorSpan.style.paddingLeft = '5px'; + + // Determine sort indicator and initial visibility + let indicator = '●'; // Default: unsorted (dot) + const sortIndex = getSortIndex(columnName); + + if (sortIndex !== -1) { + const isAscending = currentSortContext[sortIndex].ascending; + indicator = isAscending ? '▲' : '▼'; + indicatorSpan.style.visibility = 'visible'; // Sorted arrows always visible + } else { + indicatorSpan.style.visibility = 'hidden'; + } + indicatorSpan.textContent = indicator; + + const existingIndicator = headerDiv.querySelector('.sort-indicator'); + if (existingIndicator) { + headerDiv.removeChild(existingIndicator); + } + headerDiv.appendChild(indicatorSpan); + + header.addEventListener('mouseover', () => { + if (getSortIndex(columnName) === -1) { + indicatorSpan.style.visibility = 'visible'; + } + }); + header.addEventListener('mouseout', () => { + if (getSortIndex(columnName) === -1) { + indicatorSpan.style.visibility = 'hidden'; + } + }); + + // Add click handler for three-state toggle + header.addEventListener(Event.CLICK, (event) => { + const sortIndex = getSortIndex(columnName); + let newContext = [...currentSortContext]; + + if (event.shiftKey) { + if (sortIndex !== -1) { + // Already sorted. Toggle or Remove. + if (newContext[sortIndex].ascending) { + // Asc -> Desc + // Clone object to avoid mutation issues + newContext[sortIndex] = { + ...newContext[sortIndex], + ascending: false, + }; + } else { + // Desc -> Remove + newContext.splice(sortIndex, 1); + } + } else { + // Not sorted -> Append Asc + newContext.push({ column: columnName, ascending: true }); + } + } else { + // No shift key. Single column mode. + if (sortIndex !== -1 && newContext.length === 1) { + // Already only this column. Toggle or Remove. + if (newContext[sortIndex].ascending) { + newContext[sortIndex] = { + ...newContext[sortIndex], + ascending: false, + }; + } else { + newContext = []; + } + } else { + // Start fresh with this column + newContext = [{ column: columnName, ascending: true }]; + } + } + + model.set(ModelProperty.SORT_CONTEXT, newContext); + model.save_changes(); + }); + } + }); + + updateButtonStates(); + } + + function handleErrorMessageChange() { + const errorMsg = model.get(ModelProperty.ERROR_MESSAGE); + if (errorMsg) { + errorContainer.textContent = errorMsg; + errorContainer.style.display = 'block'; + } else { + errorContainer.style.display = 'none'; + } + } + + prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1)); + nextPage.addEventListener(Event.CLICK, () => handlePageChange(1)); + pageSizeInput.addEventListener(Event.CHANGE, (e) => { + const newSize = Number(e.target.value); + if (newSize) { + handlePageSizeChange(newSize); + } + }); + + model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange); + model.on(`change:${ModelProperty.ROW_COUNT}`, updateButtonStates); + model.on(`change:${ModelProperty.ERROR_MESSAGE}`, handleErrorMessageChange); + model.on(`change:_initial_load_complete`, (val) => { + if (val) updateButtonStates(); + }); + model.on(`change:${ModelProperty.PAGE}`, updateButtonStates); + + paginationContainer.appendChild(prevPage); + paginationContainer.appendChild(pageIndicator); + paginationContainer.appendChild(nextPage); + pageSizeContainer.appendChild(pageSizeLabel); + pageSizeContainer.appendChild(pageSizeInput); + footer.appendChild(rowCountLabel); + footer.appendChild(paginationContainer); + footer.appendChild(pageSizeContainer); + + el.appendChild(errorContainer); + el.appendChild(tableContainer); + el.appendChild(footer); + + handleTableHTMLChange(); + handleErrorMessageChange(); } export default { render }; diff --git a/tests/js/table_widget.test.js b/tests/js/table_widget.test.js index b3796905e5..5843694617 100644 --- a/tests/js/table_widget.test.js +++ b/tests/js/table_widget.test.js @@ -14,283 +14,325 @@ * limitations under the License. */ -import { jest } from "@jest/globals"; -import { JSDOM } from "jsdom"; - -describe("TableWidget", () => { - let model; - let el; - let render; - - beforeEach(async () => { - jest.resetModules(); - document.body.innerHTML = "
"; - el = document.body.querySelector("div"); - - const tableWidget = ( - await import("../../bigframes/display/table_widget.js") - ).default; - render = tableWidget.render; - - model = { - get: jest.fn(), - set: jest.fn(), - save_changes: jest.fn(), - on: jest.fn(), - }; - }); - - it("should have a render function", () => { - expect(render).toBeDefined(); - }); - - describe("render", () => { - it("should create the basic structure", () => { - // Mock the initial state - model.get.mockImplementation((property) => { - if (property === "table_html") { - return ""; - } - if (property === "row_count") { - return 100; - } - if (property === "error_message") { - return null; - } - if (property === "page_size") { - return 10; - } - if (property === "page") { - return 0; - } - return null; - }); - - render({ model, el }); - - expect(el.classList.contains("bigframes-widget")).toBe(true); - expect(el.querySelector(".error-message")).not.toBeNull(); - expect(el.querySelector("div")).not.toBeNull(); - expect(el.querySelector("div:nth-child(3)")).not.toBeNull(); - }); - - it("should sort when a sortable column is clicked", () => { - // Mock the initial state - model.get.mockImplementation((property) => { - if (property === "table_html") { - return "col1 |
|---|
col1 |
|---|
col1 |
|---|
col1 | col2 |
|---|
col1 | col2 |
|---|
| - | value |
-
|---|---|
| 0 | -a | -
| 1 | -b | -
col1 |
|---|
col1 |
|---|
col1 |
|---|
col1 | col2 |
|---|
col1 | col2 |
|---|
| + | value |
+
|---|---|
| 0 | +a | +
| 1 | +b | +