From 06900d775049181e79e8a5cd91d3b77207ed429b Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 5 Sep 2025 14:46:00 -0500 Subject: [PATCH 1/8] first iteration of pinning tests --- test/e2e/pages/clipboard.ts | 2 +- test/e2e/pages/dataExplorer.ts | 216 ++- .../tests/data-explorer/data-columns.test.ts | 6 +- .../data-explorer-copy-paste.test.ts | 179 ++ .../data-explorer/data-explorer-pins.test.ts | 175 ++ test/e2e/tests/plots/plots.test.ts | 1568 ++++++++--------- 6 files changed, 1341 insertions(+), 805 deletions(-) create mode 100644 test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts create mode 100644 test/e2e/tests/data-explorer/data-explorer-pins.test.ts diff --git a/test/e2e/pages/clipboard.ts b/test/e2e/pages/clipboard.ts index c101be2700d5..a3975df15966 100644 --- a/test/e2e/pages/clipboard.ts +++ b/test/e2e/pages/clipboard.ts @@ -39,7 +39,7 @@ export class Clipboard { await expect(async () => { const clipboardText = await this.getClipboardText(); expect(clipboardText).toBe(expectedText); - }).toPass({ timeout: 20000 }); + }, { message: 'clipboard text to be...' }).toPass({ timeout: 20000 }); } async setClipboardText(text: string): Promise { diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 7058a166d175..0a0b0e45e00c 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -199,9 +199,10 @@ export class Filters { export class DataGrid { grid: Locator; private statusBar: Locator; + private rowHeaders = this.code.driver.page.locator('.data-grid-row-headers'); private columnHeaders = this.code.driver.page.locator(HEADER_TITLES); private rows = this.code.driver.page.locator(`${DATA_GRID_ROWS} ${DATA_GRID_ROW}`); - cell = (rowIndex: number, columnIndex: number) => this.code.driver.page.locator( + private cell = (rowIndex: number, columnIndex: number) => this.code.driver.page.locator( `${DATA_GRID_ROWS} ${DATA_GRID_ROW}:nth-child(${rowIndex + 1}) > div:nth-child(${columnIndex + 1})` ); @@ -237,7 +238,7 @@ export class DataGrid { /** * Click a cell by its visual position (Index is 0-based) - * For example, if column 0 is a pin, clicking (0,0) will click the pinned column despite its index + * If a column/row is pinned, this method finds the cell by its visual position. */ async clickCell(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by 0-based position: row ${rowIndex}, column ${columnIndex}`, async () => { @@ -256,6 +257,71 @@ export class DataGrid { }); } + async shiftClickCell(rowIndex: number, columnIndex: number) { + this.clickCell(rowIndex, columnIndex, true); + } + + // index based + async selectColumnAction(colIndex: number, action: 'Copy' | 'Select Column' | 'Pin Column' | 'Unpin Column' | 'Sort Ascending' | 'Sort Descending' | 'Clear Sorting' | 'Add Filter') { + await test.step(`Select column action: ${action}`, async () => { + await this.code.driver.page.locator(`div:nth-child(${colIndex}) > .content > .positron-button`).click(); + await this.code.driver.page.getByRole('button', { name: action }).click(); + }); + } + + async pinColumn(colIndex: number) { + await test.step(`Pin column at index ${colIndex}`, async () => { + // colIndex is 0-based, selectColumnAction is 1-based + await this.selectColumnAction(colIndex + 1, 'Pin Column') + }); + } + + async unpinColumn(colIndex = 0) { + await test.step(`Unpin column at index ${colIndex}`, async () => { + // colIndex is 0-based, selectColumnAction is 1-based + await this.selectColumnAction(colIndex + 1, 'Unpin Column') + }); + } + + async pinRow(rowIndex: number) { + await test.step(`Pin row at index ${rowIndex}`, async () => { + await this.code.driver.page + // rowIndex is 0-based, nth-child is 1-based + .locator(`.data-grid-row-headers > div:nth-child(${rowIndex + 1})`) + .click({ button: 'right' }); + await this.code.driver.page.getByRole('button', { name: 'Pin Row' }).click(); + }); + } + + async unpinRow(rowIndex = 0) { + await test.step(`Unpin row at index ${rowIndex}`, async () => { + await this.code.driver.page + .locator(`.data-grid-row-headers > div:nth-child(${rowIndex + 1})`) + .click({ button: 'right' }); + await this.code.driver.page.getByRole('button', { name: 'Unpin Row' }).click(); + }); + } + + async selectRange({ start, end }: { start: CellPosition, end: CellPosition }) { + await test.step(`Select range: [${start.row}, ${start.col}] - [${end.row}, ${end.col}]`, async () => { + await this.clickCell(start.row, start.col); + await this.shiftClickCell(end.row, end.col); + }); + } + + async clickColumnHeader(columnTitle: string) { + await test.step(`Click column header: ${columnTitle}`, async () => { + await this.columnHeaders.getByText(columnTitle).click(); + }); + } + + /** 1-based just as it is in the UI */ + async clickRowHeader(rowIndex: number) { + await test.step(`Click row header: ${rowIndex}`, async () => { + await this.rowHeaders.getByText(rowIndex.toString(), { exact: true }).click(); + }); + } + // --- Getters --- async getRowCount(): Promise { @@ -314,34 +380,53 @@ export class DataGrid { // --- Verifications --- - async verifyColumnHeaders(expectedHeaders: string[]) { - await test.step('Verify column headers', async () => { + /** + * Verify that the column headers match the expected headers. + * Note: assumes there are no duplicate column names. + * @param expectedHeaders Array of expected column headers in the correct order + */ + async expectColumnHeadersToBe(expectedHeaders: string[]) { + await test.step(`Verify column headers (title, order): ${expectedHeaders}`, async () => { await this.jumpToStart(); await this.clickCell(0, 0); - // Calculate max attempts based on expected column count - const columnCount = await this.getColumnCount(); - let maxScrollAttempts = columnCount; + const visibleHeaders: string[] = []; + const maxScrollAttempts = await this.getColumnCount(); + let scrollAttempts = 0; - for (let i = 0; i < expectedHeaders.length; i++) { - const headerIsVisible = await this.columnHeaders.getByText(expectedHeaders[i], { exact: true }).isVisible(); - if (headerIsVisible) { - await expect(this.columnHeaders.getByText(expectedHeaders[i], { exact: true })).toBeVisible(); - continue; - } + // Get initial visible headers + const initialHeaders = await this.columnHeaders.allInnerTexts(); + visibleHeaders.push(...initialHeaders); + + // Scroll right until we've collected all headers + while (scrollAttempts < maxScrollAttempts) { + // Press right arrow key to scroll horizontally + await this.code.driver.page.keyboard.press('ArrowRight'); + scrollAttempts++; + + // Get current visible headers after scrolling + const currentHeaders = await this.columnHeaders.allInnerTexts(); - if (maxScrollAttempts > 0) { - await this.code.driver.page.keyboard.press('ArrowRight'); - maxScrollAttempts--; - i--; // Try this header again after scrolling - } else { - throw new Error(`Could not find column header: ${expectedHeaders[i]}`); + // Add any new headers we haven't seen yet + for (const header of currentHeaders) { + if (!visibleHeaders.includes(header)) { + visibleHeaders.push(header); + } } } + + // Verify the length matches expected + expect(visibleHeaders.length, `Expected headers: ${expectedHeaders.length}, Actual headers: ${visibleHeaders.length}`).toBe(expectedHeaders.length); + + // Verify each header matches expected in the correct order + for (let i = 0; i < expectedHeaders.length; i++) { + expect(visibleHeaders[i], `Col ${i}: Expected "${expectedHeaders[i]}", Actual: "${visibleHeaders[i]}"`).toBe(expectedHeaders[i]); + } }); } + async verifyTableDataLength(expectedLength: number) { await test.step('Verify data explorer table data length', async () => { await expect(async () => { @@ -375,6 +460,29 @@ export class DataGrid { }); } + async expectCellContentToBe({ rowIndex, colIndex, value }: { rowIndex: number, colIndex: number, value: string | number }): Promise { + await test.step(`Verify cell content at (${rowIndex}, ${colIndex}): ${value}`, async () => { + await expect(async () => { + const cell = this.grid.locator(`#data-grid-row-cell-content-${colIndex}-${rowIndex}`); + await expect(cell).toHaveText(String(value)); + }).toPass(); + }); + } + + async expectRangeToBeSelected(expectedRange: { rows: number[], cols: number[] }): Promise { + await test.step(`Verify selection range: ${JSON.stringify(expectedRange)}`, async () => { + const selectedCells = this.grid.locator('.selection-overlay'); + await expect(selectedCells).toHaveCount((expectedRange.rows.length) * (expectedRange.cols.length)); + + for (const row of expectedRange.rows) { + for (const col of expectedRange.cols) { + const cell = this.grid.locator(`#data-grid-row-cell-content-${col}-${row}`); + await expect(cell.locator('..').locator('.selection-overlay')).toBeVisible(); + } + } + }); + } + async verifyTableData(expectedData: Array<{ [key: string]: string | number }>, timeout = 60000) { await test.step('Verify data explorer data', async () => { await expect(async () => { @@ -392,6 +500,76 @@ export class DataGrid { }); } + /** + * Assert that only the given columns are pinned, in order. + * + * @param expectedTitles Array of column titles in the expected pinned order + */ + async expectColumnsToBePinned(expectedTitles: string[]) { + await test.step(`Verify pinned columns: ${expectedTitles}`, async () => { + // Locate all pinned column headers + const pinnedColumns = this.code.driver.page.locator('.data-grid-column-header.pinned'); + + if (expectedTitles.length === 0) { + // If we expect no pinned columns, verify count is 0 + await expect(pinnedColumns).toHaveCount(0); + } else { + // Assert the count matches the expected length + await expect(pinnedColumns).toHaveCount(expectedTitles.length); + + // Assert each pinned column has the correct title, in order + for (let i = 0; i < expectedTitles.length; i++) { + const title = pinnedColumns.nth(i).locator('.title'); + await expect(title).toHaveText(expectedTitles[i]); + } + } + }); + } + + async expectRowsToBePinned(expectedRows: number[]) { + await test.step(`Verify pinned rows: ${expectedRows}`, async () => { + const pinnedRows = this.code.driver.page.locator('.data-grid-row-header.pinned'); + + if (expectedRows.length === 0) { + // If we expect no pinned rows, verify count is 0 + await expect(pinnedRows).toHaveCount(0); + return; + } + + for (let i = 0; i < expectedRows.length; i++) { + const content = pinnedRows.nth(i).locator('.content'); + await expect(content).toHaveText(String(expectedRows[i])); + } + }); + } + + async expectColumnCountToBe(expectedCount: number) { + await test.step('Verify column count', async () => { + const actualCount = await this.getColumnHeaders(); + expect(actualCount.length).toBe(expectedCount); + }); + } + + async expectRowCountToBe(expectedCount: number) { + await test.step('Verify row count', async () => { + // const actualCount = await this.getRowHeaders(); + // expect(actualCount.length).toBe(expectedCount); + }); + } + + async expectRowOrderToBe(expectedOrder: number[]) { + await test.step(`Verify row order: ${expectedOrder}`, async () => { + // const actualOrder = await this.getRowHeaders(); + // expect(actualOrder).toEqual(expectedOrder); + }); + } + + async expectCellToBeSelected(row: number, col: number) { + await test.step(`Verify cell at (${row}, ${col}) is selected`, async () => { + await expect(this.cell(row, col).locator('.border-overlay .cursor-border')).toBeVisible(); + }); + } + // --- Utils --- private normalize(value: unknown): string { diff --git a/test/e2e/tests/data-explorer/data-columns.test.ts b/test/e2e/tests/data-explorer/data-columns.test.ts index 580fa766d81e..6217d7f74dcd 100644 --- a/test/e2e/tests/data-explorer/data-columns.test.ts +++ b/test/e2e/tests/data-explorer/data-columns.test.ts @@ -16,7 +16,7 @@ test.describe('Data Explorer: Column Names', { tag: [tags.WEB, tags.WIN, tags.DA await openDataFile('data-files/data_explorer/data_columns.csv'); await dataExplorer.maximize(); - await dataExplorer.grid.verifyColumnHeaders([ + await dataExplorer.grid.expectColumnHeadersToBe([ 'normal_name', 'leading_space', 'trailing_space', @@ -39,6 +39,10 @@ test.describe('Data Explorer: Column Names', { tag: [tags.WEB, tags.WIN, tags.DA 'Número_do_Pedido', 'اسم_عربي', 'رمز_المنتج', + 'שם_עברי', + 'מספר_פריט', + 'Heizölrückstoßabdämpfung', + '100.000 pro Bevölkerung' ]); }); }); diff --git a/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts b/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts new file mode 100644 index 000000000000..fab8c302444d --- /dev/null +++ b/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Verifies Data Explorer copy and paste behavior: + * - Copying and pasting works with unsorted data + * - Copying and pasting works with sorted data + * - Copying and pasting works with pinned rows and columns + */ + +// import { join } from 'path'; +import { test, tags } from '../_test.setup'; + +test.use({ + suiteId: __filename +}); + +// const openFileWith = ['DuckDB', 'Code']; + + +test.describe('Data Explorer: Copy/Paste', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLORER] }, () => { + + test.beforeEach(async function ({ app, openDataFile }) { + // const { dataExplorer, } = app.workbench; + // await openDataFile(join('data-files', 'small_file.csv')); + + const { dataExplorer, console, variables, sessions } = app.workbench; + await sessions.start('r'); + await console.pasteCodeToConsole('df <- read.csv("data-files/small_file.csv")', true) + await variables.doubleClickVariableRow('df'); + + + + // maximize data view + await dataExplorer.maximize(); + await dataExplorer.waitForIdle(); + await dataExplorer.summaryPanel.hide(); + }); + + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }); + + test("Copy and Paste with unsorted data", async function ({ app }) { + const { dataExplorer, clipboard } = app.workbench; + + // verify copy and paste on columns + await dataExplorer.grid.clickColumnHeader('column3'); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column3\n56\n13\n41\n12\n17\n99\n89\n33\n43\n47'); + + // verify copy and paste on rows + await dataExplorer.grid.clickRowHeader(9); + await clipboard.copy(); + // TODO: bug + // await clipboard.expectClipboardTextToBe('column0\tcolumn1\tcolumn2\tcolumn3\tcolumn4\tcolumn5\tcolumn6\tcolumn7\tcolumn8\tcolumn9\n22\t9\t4\t43\t40\t73\t79\t98\t80\t24'); + + // verify copy and paste on cell + await dataExplorer.grid.clickCell(6, 2); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('8'); + + // TODO: bug - for data frame case + // verify copy and paste on range + // await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 1, col: 1 } }); + // await clipboard.copy(); + // await clipboard.expectClipboardTextToBe('column0\tcolumn1\n82\t69\n8\t79'); + }) + + test("Copy and Paste with sorted data", async function ({ app }) { + const { dataExplorer, clipboard } = app.workbench; + + // verify copy and paste on columns + await dataExplorer.grid.selectColumnAction(4, 'Sort Descending'); + await dataExplorer.grid.clickColumnHeader('column3'); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); + + // verify copy and paste on rows + await dataExplorer.grid.clickRowHeader(4); + // await clipboard.copy(); + // await clipboard.expectClipboardTextToBe('column0\tcolumn1\tcolumn2\tcolumn3\tcolumn4\tcolumn5\tcolumn6\tcolumn7\tcolumn8\tcolumn9\n22\t9\t4\t43\t40\t73\t79\t98\t80\t24'); + + // verify copy and paste on cell + await dataExplorer.grid.clickCell(6, 4); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('33'); + + // verify copy and paste on range + // await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 2 }, endCell: { row: 1, col: 3 } }); + // await clipboard.copy(); + // await clipboard.expectClipboardTextToBe('column2\tcolumn3\n47\t99\n8\t89'); + }); + + test.skip("Copy and Paste with pinned data", async function ({ app }) { + const { dataExplorer, clipboard } = app.workbench; + + // pin column 4 + await dataExplorer.grid.pinColumn(4); + await dataExplorer.grid.expectColumnsToBePinned(['column4']); + + // pin row 5 + await dataExplorer.grid.pinRow(5); + await dataExplorer.grid.expectRowsToBePinned([5]); + + // select range + await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 2, col: 2 } }); + await dataExplorer.grid.expectRangeToBeSelected({ + rows: [5, 0, 1], + cols: [4, 0, 1] + }); + + await dataExplorer.grid.clickUpperLeftCorner() + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column4\tcolumn0\tcolumn1\n50\t41\t42\n9\t82\t69\n8\t8\t79'); + }); + // test("Column sorting works with row and column pins", async function ({ app }) { + // const { dataExplorer } = app.workbench; + + // // pin column 4 + // await dataExplorer.grid.pinColumn(4); + // await dataExplorer.grid.expectColumnsToBePinned(['column4']); + + // // pin row 5 + // await dataExplorer.grid.pinRow(5); + // await dataExplorer.grid.expectRowsToBePinned([5]); + + // // sort by column 4 + // await dataExplorer.grid.sortColumn(4); + // await dataExplorer.grid.expectRowOrderToBe([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]); + // }); + + test.skip("Copy and Paste works with pinned rows and cols", async function ({ app }) { + const { dataExplorer, clipboard } = app.workbench; + + // pin column 4 + await dataExplorer.grid.pinColumn(4); + await dataExplorer.grid.expectColumnsToBePinned(['column4']); + + // pin row 5 + await dataExplorer.grid.pinRow(5); + await dataExplorer.grid.expectRowsToBePinned([5]); + + // select range + await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 2, col: 2 } }); + await dataExplorer.grid.expectRangeToBeSelected({ + rows: [5, 0, 1], + cols: [4, 0, 1] + }); + + await dataExplorer.grid.clickUpperLeftCorner() + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column4\tcolumn0\tcolumn1\n50\t41\t42\n9\t82\t69\n8\t8\t79'); + }); + + test("Copy and Paste works with sorted data", async function ({ app }) { + const { dataExplorer, clipboard } = app.workbench; + + // verify basic copy paste works on sorted data + await clipboard.expectClipboardTextToBe('column3\n56\n13\n41\n12\n17\n99\n89\n33\n43\n47'); + await dataExplorer.grid.selectColumnAction(3, 'Sort Descending'); + await dataExplorer.grid.clickColumnHeader('column3'); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); + + // pin column and confirm still sorted + await dataExplorer.grid.pinColumn(3); + await dataExplorer.grid.expectColumnsToBePinned(['column3']); + await clipboard.copy(); + await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); + + // pin row and confirm new sort order + await dataExplorer.grid.pinRow(5); + await dataExplorer.grid.expectRowsToBePinned([5]); + await clipboard.expectClipboardTextToBe('column3\n41\n99\n89\n56\n47\n43\n33\n17\n13\n12'); + }); +}) diff --git a/test/e2e/tests/data-explorer/data-explorer-pins.test.ts b/test/e2e/tests/data-explorer/data-explorer-pins.test.ts new file mode 100644 index 000000000000..4f795e78c856 --- /dev/null +++ b/test/e2e/tests/data-explorer/data-explorer-pins.test.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Verifies Data Explorer pinning behavior: + * - Column pinning persists across scroll + * - Row pinning persists across scroll + * - Pinned columns are always visible + * - Pinned rows are always visible + * + * Note: some pinning functionality (e.g., copy/pasting with pins) is covered by: data-explorer-copy-paste.test.ts + */ + +import { join } from 'path'; +import { test, tags } from '../_test.setup'; + +const columnOrder = { + default: ['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column6', 'column7', 'column8', 'column9'], + pinCol6: ['column6', 'column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8', 'column9'], + pinCol2: ['column2', 'column0', 'column1', 'column3', 'column4', 'column5', 'column6', 'column7', 'column8', 'column9'], + pinCol4: ['column4', 'column0', 'column1', 'column2', 'column3', 'column5', 'column6', 'column7', 'column8', 'column9'], + pinCol4And6: ['column4', 'column6', 'column0', 'column1', 'column2', 'column3', 'column5', 'column7', 'column8', 'column9'], +}; +const rowOrder = { + default: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + pinRow5: [5, 0, 1, 2, 3, 4, 6, 7, 8, 9], + pinRow8: [8, 0, 1, 2, 3, 4, 5, 6, 7, 9], + pinRow6And8: [6, 8, 0, 1, 2, 3, 4, 5, 7, 9] +}; + +test.use({ + suiteId: __filename +}); + +test.describe('Data Explorer: Pins', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLORER] }, () => { + + test.beforeEach(async function ({ app, openDataFile }) { + const { dataExplorer } = app.workbench; + + await openDataFile(join('data-files', 'small_file.csv')); + await dataExplorer.maximize(true); + await dataExplorer.waitForIdle(); + }); + + test.afterEach(async function ({ hotKeys }) { + await hotKeys.closeAllEditors(); + }); + + test('Row/column pinning persists across scroll and can be added/removed', async function ({ app }) { + const { dataExplorer } = app.workbench; + + // Initial state + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.default); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.default); + + // Pin "column4" + await dataExplorer.grid.pinColumn(4); + await dataExplorer.grid.expectColumnsToBePinned(['column4']); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol4); + + // Pin "column6" + await dataExplorer.grid.pinColumn(6); + await dataExplorer.grid.expectColumnsToBePinned(['column4', 'column6']); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol4And6); + + // Pin row 8 + await dataExplorer.grid.pinRow(8); + await dataExplorer.grid.expectRowsToBePinned([8]); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow8); + + // Pin row 6 + await dataExplorer.grid.pinRow(7); // after pinning row 8, row 6 is now at index 7 + await dataExplorer.grid.expectRowsToBePinned([8, 6]); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow6And8); + + // Ensure pins persist with scrolling + await dataExplorer.grid.clickLowerRightCorner(); + await dataExplorer.grid.expectColumnsToBePinned(['column4', 'column6']); + await dataExplorer.grid.expectRowsToBePinned([8, 6]); + + // Unpin columns + await dataExplorer.grid.unpinColumn(0); + await dataExplorer.grid.expectColumnsToBePinned(['column6']); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol6); + + await dataExplorer.grid.unpinColumn(0); + await dataExplorer.grid.expectColumnsToBePinned([]); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.default); + + // Unpin rows + await dataExplorer.grid.unpinRow(0); + await dataExplorer.grid.expectRowsToBePinned([6]); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow8); + await dataExplorer.grid.unpinRow(0); + await dataExplorer.grid.expectRowsToBePinned([]); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.default); + }); + + test('Range selection respects pinned columns (excludes vs includes cases)', async function ({ app }) { + const { dataExplorer } = app.workbench; + + // pin column2 + await dataExplorer.grid.pinColumn(2); + await dataExplorer.grid.expectColumnsToBePinned(['column2']); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol2); + + // select range that excludes pinned column + await dataExplorer.grid.selectRange({ + start: { row: 4, col: 4 }, + end: { row: 6, col: 1 } + }); + await dataExplorer.grid.expectRangeToBeSelected({ + rows: [4, 5, 6], + cols: [0, 1, 3, 4] + }); + + // select range that includes pinned column + await dataExplorer.grid.selectRange({ + start: { row: 3, col: 0 }, + end: { row: 5, col: 2 } + }); + await dataExplorer.grid.expectRangeToBeSelected({ + rows: [3, 4, 5], + cols: [2, 0, 1] + }); + }); + + test('Cell navigation works with pinned columns and rows', async function ({ app }) { + const { dataExplorer } = app.workbench; + const { keyboard } = app.code.driver.page; + + // pin column 2 + await dataExplorer.grid.pinColumn(2); + await dataExplorer.grid.expectColumnsToBePinned(['column2']); + + // pin row 8 + await dataExplorer.grid.pinRow(8); + await dataExplorer.grid.expectRowsToBePinned([8]); + + // verify navigation with keyboard is in right direction and doesn't skip cells + await dataExplorer.grid.clickCell(0, 0); + + await keyboard.press('ArrowDown'); + await dataExplorer.grid.expectCellToBeSelected(1, 0); + + await keyboard.press('ArrowRight'); + await dataExplorer.grid.expectCellToBeSelected(1, 1); + + await keyboard.press('ArrowRight'); + await dataExplorer.grid.expectCellToBeSelected(1, 2); + }); + + test("Column sorting doesn't impact pin locations", async function ({ app }) { + const { dataExplorer } = app.workbench; + + // pin column 4 + await dataExplorer.grid.pinColumn(4); + await dataExplorer.grid.expectColumnsToBePinned(['column4']); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol4); + + // pin row 5 + await dataExplorer.grid.pinRow(5); + await dataExplorer.grid.expectRowsToBePinned([5]); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow5); + + // sort by column 4 + await dataExplorer.grid.sortColumnBy(4, 'Sort Descending'); + await dataExplorer.grid.expectRowsToBePinned([5]); + await dataExplorer.grid.expectColumnsToBePinned(['column4']); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow5); + await dataExplorer.grid.expectColumnHeadersToBe(columnOrder.pinCol4); + }); +}) diff --git a/test/e2e/tests/plots/plots.test.ts b/test/e2e/tests/plots/plots.test.ts index 02aed1e16956..f4dc23dacc41 100644 --- a/test/e2e/tests/plots/plots.test.ts +++ b/test/e2e/tests/plots/plots.test.ts @@ -1,786 +1,786 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'path'; -import { test, expect, tags } from '../_test.setup'; -const resembleCompareImages = require('resemblejs/compareImages'); -import { ComparisonOptions } from 'resemblejs'; -import * as fs from 'fs'; -import { fail } from 'assert'; -import { Application } from '../../infra'; -import { Locator, Page } from '@playwright/test'; - -test.use({ - suiteId: __filename -}); - -test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { - test.describe('Python Plots', () => { - - test.beforeEach(async function ({ sessions, hotKeys }) { - await sessions.start('python'); - await hotKeys.stackedLayout(); - }); - - test.afterEach(async function ({ app, hotKeys }) { - await hotKeys.fullSizeSecondarySidebar(); - await app.workbench.plots.clearPlots(); - await app.workbench.plots.waitForNoPlots(); - }); - - test.afterAll(async function ({ cleanup }) { - await cleanup.removeTestFiles(['Python-scatter.jpeg', 'Python-scatter-editor.jpeg']); - }); - - test('Python - Verify basic plot functionality - Dynamic Plot', { - tag: [tags.CRITICAL, tags.WEB, tags.WIN] - }, async function ({ app, logger, headless }, testInfo) { - // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/ - logger.log('Sending code to console'); - await app.workbench.console.executeCode('Python', pythonDynamicPlot); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.toasts.closeAll(); - - const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); - await compareImages({ - app, - buffer, - diffScreenshotName: 'pythonScatterplotDiff', - masterScreenshotName: `pythonScatterplot-${process.platform}`, - testInfo: testInfo - }); - - if (!headless) { - await app.workbench.plots.copyCurrentPlotToClipboard(); - - let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); - expect(clipboardImageBuffer).not.toBeNull(); - - await app.workbench.clipboard.clearClipboard(); - clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); - expect(clipboardImageBuffer).toBeNull(); - } - - await test.step('Verify plot can be opened in editor', async () => { - await app.workbench.plots.openPlotIn('editor'); - await app.workbench.plots.waitForPlotInEditor(); - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - }); - - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - await app.workbench.plots.clearPlots(); - await app.workbench.layouts.enterLayout('stacked'); - await app.workbench.plots.waitForNoPlots(); - }); - - test('Python - Verify basic plot functionality - Static Plot', { - tag: [tags.CRITICAL, tags.WEB, tags.WIN] - }, async function ({ app, logger }, testInfo) { - logger.log('Sending code to console'); - await app.workbench.console.executeCode('Python', pythonStaticPlot); - await app.workbench.plots.waitForCurrentStaticPlot(); - - await app.workbench.toasts.closeAll(); - - const buffer = await app.workbench.plots.getCurrentStaticPlotAsBuffer(); - await compareImages({ - app, - buffer, - diffScreenshotName: 'graphvizDiff', - masterScreenshotName: `graphviz-${process.platform}`, - testInfo - }); - - await test.step('Verify plot can be opened in editor', async () => { - await app.workbench.plots.openPlotIn('editor'); - await app.workbench.plots.waitForPlotInEditor(); - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - }); - - }); - - test('Python - Verify the plots pane action bar - Plot actions', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - const plots = app.workbench.plots; - - // default plot pane state for action bar - await expect(plots.plotSizeButton).not.toBeVisible(); - await expect(plots.savePlotFromPlotsPaneButton).not.toBeVisible(); - await expect(plots.copyPlotButton).not.toBeVisible(); - await expect(plots.zoomPlotButton).not.toBeVisible(); - - // create plots separately so that the order is known - await app.workbench.console.executeCode('Python', pythonPlotActions1); - await plots.waitForCurrentStaticPlot(); - await app.workbench.console.executeCode('Python', pythonPlotActions2); - await plots.waitForCurrentPlot(); - - // expand the plot pane to show the action bar - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - await expect(plots.clearPlotsButton).not.toBeDisabled(); - await expect(plots.nextPlotButton).toBeDisabled(); - await expect(plots.previousPlotButton).not.toBeDisabled(); - await expect(plots.plotSizeButton).not.toBeDisabled(); - await expect(plots.savePlotFromPlotsPaneButton).not.toBeDisabled(); - await expect(plots.copyPlotButton).not.toBeDisabled(); - - // switch to fixed size plot - await plots.previousPlotButton.click(); - await plots.waitForCurrentStaticPlot(); - - // switching to fixed size plot changes action bar - await expect(plots.zoomPlotButton).toBeVisible(); - await expect(plots.plotSizeButton).not.toBeVisible(); - await expect(plots.clearPlotsButton).not.toBeDisabled(); - await expect(plots.nextPlotButton).not.toBeDisabled(); - await expect(plots.previousPlotButton).toBeDisabled(); - await expect(plots.zoomPlotButton).not.toBeDisabled(); - - // switch back to dynamic plot - await plots.nextPlotButton.click(); - await plots.waitForCurrentPlot(); - await expect(plots.zoomPlotButton).toBeVisible(); - await expect(plots.plotSizeButton).toBeVisible(); - await expect(plots.clearPlotsButton).not.toBeDisabled(); - await expect(plots.nextPlotButton).toBeDisabled(); - await expect(plots.previousPlotButton).not.toBeDisabled(); - await expect(plots.plotSizeButton).not.toBeDisabled(); - }); - - test('Python - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { - await verifyPlotInNewWindow(app, 'Python', pythonDynamicPlot); - }); - - test('Python - Verify saving a Python plot', { tag: [tags.WIN] }, async function ({ app }) { - await test.step('Sending code to console to create plot', async () => { - await app.workbench.console.executeCode('Python', pythonDynamicPlot); - await app.workbench.plots.waitForCurrentPlot(); - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - }); - - await test.step('Save plot', async () => { - await app.workbench.plots.savePlotFromPlotsPane({ name: 'Python-scatter', format: 'JPEG' }); - await app.workbench.layouts.enterLayout('stacked'); - await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter.jpeg']); - }); - - await test.step('Open plot in editor', async () => { - await app.workbench.plots.openPlotIn('editor'); - await app.workbench.plots.waitForPlotInEditor(); - }); - - await test.step('Save plot from editor', async () => { - await app.workbench.plots.savePlotFromEditor({ name: 'Python-scatter-editor', format: 'JPEG' }); - await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter-editor.jpeg']); - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - }); - - }); - - test('Python - Verify bqplot Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, bgplot, '.svg-figure'); - }); - - test('Python - Verify ipydatagrid Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, ipydatagrid, 'canvas:nth-child(1)'); - }); - - test('Python - Verify ipyleaflet Python widget ', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, ipyleaflet, '.leaflet-container'); - }); - - test('Python - Verify hvplot can load with plotly extension', { - tag: [tags.WEB, tags.WIN], - annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], - }, async function ({ app }) { - // run line by line due to https://github.com/posit-dev/positron/issues/5991 - await runScriptAndValidatePlot(app, plotly, '.plotly', false, true); - }); - - test('Python - Verify hvplot with plotly extension works in block execution', { - tag: [tags.WEB, tags.WIN], - annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], - }, async function ({ app }) { - // Test that our fix allows hvplot to work when executed as a block - await runScriptAndValidatePlot(app, plotly, '.plotly', false, false); - }); - - test('Python - Verify ipytree Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, ipytree, '.jstree-container-ul'); - - // fullauxbar layout needed for some smaller windows - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - - // tree should be expanded by default - const treeNodes = app.workbench.plots.getWebviewPlotLocator('.jstree-container-ul .jstree-node'); - await expect(treeNodes).toHaveCount(9); - - // collapse the tree, only parent nodes should be visible - await treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon - await expect(treeNodes).toHaveCount(3); - }); - - test('Python - Verify ipywidget.Output Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await app.workbench.console.pasteCodeToConsole(ipywidgetOutput); - await app.workbench.console.sendEnterKey(); - await app.workbench.plots.waitForWebviewPlot('.widget-output', 'attached'); - - // Redirect a print statement to the Output widget. - await app.workbench.console.pasteCodeToConsole(`with output: - print('Hello, world!') -`); // Empty line needed for the statement to be considered complete. - await app.workbench.console.sendEnterKey(); - await app.workbench.plots.waitForWebviewPlot('.widget-output .jp-OutputArea-child'); - - // The printed statement should not be shown in the console. - await app.workbench.console.waitForConsoleContents('Hello World', { expectedCount: 0 }); - - }); - - test('Python - Verify bokeh Python widget', { - tag: [tags.WEB, tags.WIN] - }, async function ({ app }) { - await app.workbench.console.executeCode('Python', bokeh); - - // selector not factored out as it is unique to bokeh - const bokehCanvas = '.bk-Canvas'; - await app.workbench.plots.waitForWebviewPlot(bokehCanvas, 'visible', app.web); - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - - // selector not factored out as it is unique to bokeh - let canvasLocator: Locator; - if (!app.web) { - await app.workbench.plots.getWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); - canvasLocator = app.workbench.plots.getWebviewPlotLocator(bokehCanvas); - } else { - await app.workbench.plots.getDeepWebWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); - canvasLocator = app.workbench.plots.getDeepWebWebviewPlotLocator(bokehCanvas); - } - const boundingBox = await canvasLocator.boundingBox(); - - // plot capture before zoom - const bufferBeforeZoom = await canvasLocator.screenshot(); - - if (boundingBox) { - await app.code.driver.clickAndDrag({ - from: { - x: boundingBox.x + boundingBox.width / 3, - y: boundingBox.y + boundingBox.height / 3 - }, - to: { - x: boundingBox.x + 2 * (boundingBox.width / 3), - y: boundingBox.y + 2 * (boundingBox.height / 3) - } - }); - } else { - fail('Bounding box not found'); - } - - // plot capture after zoom - const bufferAfterZoom = await canvasLocator.screenshot(); - - // two plot captures should be different - const data = await resembleCompareImages(bufferAfterZoom, bufferBeforeZoom, options); - expect(data.rawMisMatchPercentage).toBeGreaterThan(0.0); - }); - - test('Python - Verify Plot Zoom works (Fit vs. 200%)', { tag: [tags.WEB] }, - async function ({ app, openFile, python, page }, testInfo) { - await openFile(path.join('workspaces', 'python-plots', 'matplotlib-zoom-example.py')); - - await test.step('Run Python File in Console', async () => { - await app.workbench.editor.playButton.click(); - await app.workbench.plots.waitForCurrentPlot(); - }); - const imgLocator = page.getByRole('img', { name: /%run/ }); - - await app.workbench.plots.setThePlotZoom('Fit'); - await page.waitForTimeout(2000); - await dismissPlotZoomTooltip(page); - const bufferFit1 = await imgLocator.screenshot(); - await app.workbench.plots.setThePlotZoom('200%'); - - await page.waitForTimeout(2000); - await dismissPlotZoomTooltip(page); - const bufferZoom = await imgLocator.screenshot(); - // Compare: Fit vs 200% - const resultZoom = await resembleCompareImages(bufferFit1, bufferZoom, options); - await testInfo.attach('fit-vs-zoom', { - body: resultZoom.getBuffer(true), - contentType: 'image/png' - }); - expect(resultZoom.rawMisMatchPercentage).toBeGreaterThan(1.5); // should be large diff - - await app.workbench.plots.setThePlotZoom('Fit'); - await page.waitForTimeout(2000); - await dismissPlotZoomTooltip(page); - const bufferFit2 = await imgLocator.screenshot(); - // Compare: Fit vs Fit again - const resultBack = await resembleCompareImages(bufferFit1, bufferFit2, options); - await testInfo.attach('fit-vs-fit', { - body: resultBack.getBuffer(true), - contentType: 'image/png' - }); - expect(resultBack.rawMisMatchPercentage).toBeLessThan(0.75); // should be small diff - }); - - }); - - test.describe('R Plots', { - tag: [tags.ARK] - }, () => { - - test.beforeEach(async function ({ sessions, hotKeys }) { - await hotKeys.stackedLayout(); - await sessions.start('r'); - }); - - test.afterEach(async function ({ app, hotKeys }) { - await hotKeys.fullSizeSecondarySidebar(); - await app.workbench.plots.clearPlots(); - await app.workbench.plots.waitForNoPlots(); - }); - - test.afterAll(async function ({ cleanup }) { - await cleanup.removeTestFiles(['r-cars.svg', 'r-cars.jpeg', 'plot.png']); - }); - - test('R - Verify basic plot functionality', { - tag: [tags.CRITICAL, tags.WEB, tags.WIN] - }, async function ({ app, logger, headless }, testInfo) { - logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', rBasicPlot); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.toasts.closeAll(); - - const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); - await compareImages({ - app, - buffer, - diffScreenshotName: 'autosDiff', - masterScreenshotName: `autos-${process.platform}`, - testInfo - }); - - if (!headless) { - await app.workbench.plots.copyCurrentPlotToClipboard(); - - let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); - expect(clipboardImageBuffer).not.toBeNull(); - - await app.workbench.clipboard.clearClipboard(); - clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); - expect(clipboardImageBuffer).toBeNull(); - } - - await test.step('Verify plot can be opened in editor', async () => { - await app.workbench.plots.openPlotIn('editor'); - await app.workbench.plots.waitForPlotInEditor(); - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - }); - - await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - await app.workbench.plots.clearPlots(); - await app.workbench.layouts.enterLayout('stacked'); - await app.workbench.plots.waitForNoPlots(); - }); - - test('R - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { - await verifyPlotInNewWindow(app, 'R', rBasicPlot); - }); - - test('R - Verify saving an R plot', { tag: [tags.WIN] }, async function ({ app }) { - await test.step('Sending code to console to create plot', async () => { - await app.workbench.console.executeCode('R', rSavePlot); - await app.workbench.plots.waitForCurrentPlot(); - }); - - await test.step('Save plot as PNG', async () => { - await app.workbench.plots.savePlotFromPlotsPane({ name: 'plot', format: 'PNG' }); - await app.workbench.explorer.verifyExplorerFilesExist(['plot.png']); - }); - - await test.step('Save plot as SVG', async () => { - await app.workbench.plots.savePlotFromPlotsPane({ name: 'R-cars', format: 'SVG' }); - await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.svg']); - }); - - await test.step('Open plot in editor', async () => { - await app.workbench.plots.openPlotIn('editor'); - await app.workbench.plots.waitForPlotInEditor(); - }); - - await test.step('Save plot from editor as JPEG', async () => { - await app.workbench.plots.savePlotFromEditor({ name: 'R-cars', format: 'JPEG' }); - await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.jpeg']); - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - }); - }); - - test('R - Verify rplot plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await app.workbench.console.pasteCodeToConsole(rplot); - await app.workbench.console.sendEnterKey(); - await app.workbench.plots.waitForCurrentPlot(); - }); - - test('R - Verify highcharter plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, highcharter, 'svg', app.web); - }); - - test('R - Verify leaflet plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, leaflet, '.leaflet', app.web); - }); - - test('R - Verify plotly plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await runScriptAndValidatePlot(app, rPlotly, '.plot-container', app.web); - }); - - test('R - Two simultaneous plots', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - await app.workbench.console.pasteCodeToConsole(rTwoPlots, true); - await app.workbench.plots.waitForCurrentPlot(); - await app.workbench.plots.expectPlotThumbnailsCountToBe(2); - }); - - test('R - Plot building', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - - await app.workbench.plots.enlargePlotArea(); - - await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 2))', true); - await app.workbench.console.pasteCodeToConsole('plot(1:5)', true); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.console.pasteCodeToConsole('plot(2:6)', true); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.console.pasteCodeToConsole('plot(3:7)', true); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.console.pasteCodeToConsole('plot(4:8)', true); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.console.pasteCodeToConsole('plot(5:9)', true); - await app.workbench.plots.waitForCurrentPlot(); - await app.workbench.plots.expectPlotThumbnailsCountToBe(2); - - await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); - await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); - await app.workbench.plots.waitForCurrentPlot(); - await app.workbench.plots.expectPlotThumbnailsCountToBe(3); - - await app.workbench.plots.restorePlotArea(); - }); - - test('R - Figure margins', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - - await app.workbench.plots.enlargePlotArea(); - - await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 1))', true); - await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); - await app.workbench.console.pasteCodeToConsole('plot(2:20)', true); - await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); - await app.workbench.plots.waitForCurrentPlot(); - - await app.workbench.plots.restorePlotArea(); - }); - - test('R - plot and save in one block', { tag: [tags.WEB, tags.WIN] }, async function ({ app, runCommand }) { - - await app.workbench.console.clearButton.click(); - await app.workbench.console.restartButton.click(); - - await app.workbench.console.waitForConsoleContents('restarted', { expectedCount: 1 }); - - await app.workbench.console.pasteCodeToConsole(rPlotAndSave, true); - await app.workbench.plots.waitForCurrentPlot(); - - await runCommand('workbench.action.fullSizedAuxiliaryBar'); - - const vars = await app.workbench.variables.getFlatVariables(); - const filePath = vars.get('tempfile')?.value; - - expect(fs.existsSync(filePath?.replaceAll('"', '')!)).toBe(true); - - await app.workbench.layouts.enterLayout('stacked'); - }); - - }); -}); - -const options: ComparisonOptions = { - output: { - errorColor: { - red: 255, - green: 0, - blue: 255 - }, - errorType: 'movement', - transparency: 0.3, - largeImageThreshold: 1200, - useCrossOrigin: false - }, - scaleToSameSize: true, - ignore: 'antialiasing', -}; - -async function runScriptAndValidatePlot(app: Application, script: string, locator: string, RWeb = false, runLineByLine = false) { - await app.workbench.hotKeys.fullSizeSecondarySidebar(); - const lines: string[] = runLineByLine ? script.split('\n') : [script]; - - await expect(async () => { - for (const line of lines) { - await app.workbench.console.pasteCodeToConsole(line); - await app.workbench.console.sendEnterKey(); - } - await app.workbench.console.waitForConsoleExecution({ timeout: 15000 }); - await app.workbench.plots.waitForWebviewPlot(locator, 'visible', RWeb); - }, 'Send code to console and verify plot renders').toPass({ timeout: 60000 }); -} - -async function verifyPlotInNewWindow(app: Application, language: "Python" | "R", plotCode: string) { - const plots = app.workbench.plots; - await test.step(`Create a ${language} plot`, async () => { - await app.workbench.console.executeCode(language, plotCode); - await plots.waitForCurrentPlot(); - }); - await test.step('Open plot in new window', async () => { - await plots.openPlotIn('new window'); - await app.workbench.layouts.enterLayout('stacked'); - }); -} - -async function compareImages({ - app, - buffer, - diffScreenshotName, - masterScreenshotName, - testInfo -}: { - app: any; - buffer: Buffer; - diffScreenshotName: string; - masterScreenshotName: string; - testInfo: any; -}) { - await test.step('compare images', async () => { - if (process.env.GITHUB_ACTIONS && !app.web) { - const data = await resembleCompareImages(fs.readFileSync(path.join(__dirname, `${masterScreenshotName}.png`),), buffer, options); - - if (data.rawMisMatchPercentage > 2.0) { - if (data.getBuffer) { - await testInfo.attach(diffScreenshotName, { body: data.getBuffer(true), contentType: 'image/png' }); - } - - // Capture a new master image in CI - const newMaster = await app.workbench.plots.currentPlot.screenshot(); - await testInfo.attach(masterScreenshotName, { body: newMaster, contentType: 'image/png' }); - - // Fail the test with mismatch details - fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`); - } - } - }); -} - -const pythonDynamicPlot = `import pandas as pd -import matplotlib.pyplot as plt -data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'], - 'age': [20, 20, 21, 20, 21, 20], - 'math_marks': [100, 90, 91, 98, 92, 95], - 'physics_marks': [90, 100, 91, 92, 98, 95], - 'chem_marks': [93, 89, 99, 92, 94, 92] - } - -df = pd.DataFrame(data_dict) - -df.plot(kind='scatter', - x='math_marks', - y='physics_marks', - color='red') - -plt.title('ScatterPlot') -plt.show()`; - - -const pythonStaticPlot = `import graphviz as gv -import IPython - -h = gv.Digraph(format="svg") -names = [ - "A", - "B", - "C", -] - -# Specify edges -h.edge("A", "B") -h.edge("A", "C") - -IPython.display.display_png(h)`; - -const pythonPlotActions1 = `import graphviz as gv -import IPython - -h = gv.Digraph(format="svg") -names = [ - "A", - "B", - "C", -] - -# Specify edges -h.edge("A", "B") -h.edge("A", "C") - -IPython.display.display_png(h)`; - -const pythonPlotActions2 = `import matplotlib.pyplot as plt - -# x axis values -x = [1,2,3] -# corresponding y axis values -y = [2,4,1] - -# plotting the points -plt.plot(x, y) - -# naming the x axis -plt.xlabel('x - axis') -# naming the y axis -plt.ylabel('y - axis') - -# giving a title to my graph -plt.title('My first graph!') - -# function to show the plot -plt.show()`; - -const bgplot = `import bqplot.pyplot as bplt -import numpy as np - -x = np.linspace(-10, 10, 100) -y = np.sin(x) -axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}} - -fig = bplt.figure(title="Line Chart") -line = bplt.plot( - x=x, y=y, axes_options=axes_opts -) - -bplt.show()`; - -const ipydatagrid = `import pandas as pd -from ipydatagrid import DataGrid -data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"]) -DataGrid(data) -DataGrid(data, selection_mode="cell", editable=True)`; - -const ipyleaflet = `from ipyleaflet import Map, Marker, display -center = (52.204793, 360.121558) -map = Map(center=center, zoom=12) - -# Add a draggable marker to the map -# Dragging the marker updates the marker.location value in Python -marker = Marker(location=center, draggable=True) -map.add_control(marker) - -display(map)`; - -const plotly = `import hvplot.pandas -import pandas as pd -hvplot.extension('plotly') -pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`; +// /*--------------------------------------------------------------------------------------------- +// * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. +// * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. +// *--------------------------------------------------------------------------------------------*/ + +// import * as path from 'path'; +// import { test, expect, tags } from '../_test.setup'; +// const resembleCompareImages = require('resemblejs/compareImages'); +// import { ComparisonOptions } from 'resemblejs'; +// import * as fs from 'fs'; +// import { fail } from 'assert'; +// import { Application } from '../../infra'; +// import { Locator, Page } from '@playwright/test'; + +// test.use({ +// suiteId: __filename +// }); + +// test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { +// test.describe('Python Plots', () => { + +// test.beforeEach(async function ({ sessions, hotKeys }) { +// await sessions.start('python'); +// await hotKeys.stackedLayout(); +// }); + +// test.afterEach(async function ({ app, hotKeys }) { +// await hotKeys.fullSizeSecondarySidebar(); +// await app.workbench.plots.clearPlots(); +// await app.workbench.plots.waitForNoPlots(); +// }); + +// test.afterAll(async function ({ cleanup }) { +// await cleanup.removeTestFiles(['Python-scatter.jpeg', 'Python-scatter-editor.jpeg']); +// }); + +// test('Python - Verify basic plot functionality - Dynamic Plot', { +// tag: [tags.CRITICAL, tags.WEB, tags.WIN] +// }, async function ({ app, logger, headless }, testInfo) { +// // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/ +// logger.log('Sending code to console'); +// await app.workbench.console.executeCode('Python', pythonDynamicPlot); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.toasts.closeAll(); + +// const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); +// await compareImages({ +// app, +// buffer, +// diffScreenshotName: 'pythonScatterplotDiff', +// masterScreenshotName: `pythonScatterplot-${process.platform}`, +// testInfo: testInfo +// }); + +// if (!headless) { +// await app.workbench.plots.copyCurrentPlotToClipboard(); + +// let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); +// expect(clipboardImageBuffer).not.toBeNull(); + +// await app.workbench.clipboard.clearClipboard(); +// clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); +// expect(clipboardImageBuffer).toBeNull(); +// } + +// await test.step('Verify plot can be opened in editor', async () => { +// await app.workbench.plots.openPlotIn('editor'); +// await app.workbench.plots.waitForPlotInEditor(); +// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); +// }); + +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); +// await app.workbench.plots.clearPlots(); +// await app.workbench.layouts.enterLayout('stacked'); +// await app.workbench.plots.waitForNoPlots(); +// }); + +// test('Python - Verify basic plot functionality - Static Plot', { +// tag: [tags.CRITICAL, tags.WEB, tags.WIN] +// }, async function ({ app, logger }, testInfo) { +// logger.log('Sending code to console'); +// await app.workbench.console.executeCode('Python', pythonStaticPlot); +// await app.workbench.plots.waitForCurrentStaticPlot(); + +// await app.workbench.toasts.closeAll(); + +// const buffer = await app.workbench.plots.getCurrentStaticPlotAsBuffer(); +// await compareImages({ +// app, +// buffer, +// diffScreenshotName: 'graphvizDiff', +// masterScreenshotName: `graphviz-${process.platform}`, +// testInfo +// }); + +// await test.step('Verify plot can be opened in editor', async () => { +// await app.workbench.plots.openPlotIn('editor'); +// await app.workbench.plots.waitForPlotInEditor(); +// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); +// }); + +// }); + +// test('Python - Verify the plots pane action bar - Plot actions', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// const plots = app.workbench.plots; + +// // default plot pane state for action bar +// await expect(plots.plotSizeButton).not.toBeVisible(); +// await expect(plots.savePlotFromPlotsPaneButton).not.toBeVisible(); +// await expect(plots.copyPlotButton).not.toBeVisible(); +// await expect(plots.zoomPlotButton).not.toBeVisible(); + +// // create plots separately so that the order is known +// await app.workbench.console.executeCode('Python', pythonPlotActions1); +// await plots.waitForCurrentStaticPlot(); +// await app.workbench.console.executeCode('Python', pythonPlotActions2); +// await plots.waitForCurrentPlot(); + +// // expand the plot pane to show the action bar +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); +// await expect(plots.clearPlotsButton).not.toBeDisabled(); +// await expect(plots.nextPlotButton).toBeDisabled(); +// await expect(plots.previousPlotButton).not.toBeDisabled(); +// await expect(plots.plotSizeButton).not.toBeDisabled(); +// await expect(plots.savePlotFromPlotsPaneButton).not.toBeDisabled(); +// await expect(plots.copyPlotButton).not.toBeDisabled(); + +// // switch to fixed size plot +// await plots.previousPlotButton.click(); +// await plots.waitForCurrentStaticPlot(); + +// // switching to fixed size plot changes action bar +// await expect(plots.zoomPlotButton).toBeVisible(); +// await expect(plots.plotSizeButton).not.toBeVisible(); +// await expect(plots.clearPlotsButton).not.toBeDisabled(); +// await expect(plots.nextPlotButton).not.toBeDisabled(); +// await expect(plots.previousPlotButton).toBeDisabled(); +// await expect(plots.zoomPlotButton).not.toBeDisabled(); + +// // switch back to dynamic plot +// await plots.nextPlotButton.click(); +// await plots.waitForCurrentPlot(); +// await expect(plots.zoomPlotButton).toBeVisible(); +// await expect(plots.plotSizeButton).toBeVisible(); +// await expect(plots.clearPlotsButton).not.toBeDisabled(); +// await expect(plots.nextPlotButton).toBeDisabled(); +// await expect(plots.previousPlotButton).not.toBeDisabled(); +// await expect(plots.plotSizeButton).not.toBeDisabled(); +// }); + +// test('Python - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { +// await verifyPlotInNewWindow(app, 'Python', pythonDynamicPlot); +// }); + +// test('Python - Verify saving a Python plot', { tag: [tags.WIN] }, async function ({ app }) { +// await test.step('Sending code to console to create plot', async () => { +// await app.workbench.console.executeCode('Python', pythonDynamicPlot); +// await app.workbench.plots.waitForCurrentPlot(); +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); +// }); + +// await test.step('Save plot', async () => { +// await app.workbench.plots.savePlotFromPlotsPane({ name: 'Python-scatter', format: 'JPEG' }); +// await app.workbench.layouts.enterLayout('stacked'); +// await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter.jpeg']); +// }); + +// await test.step('Open plot in editor', async () => { +// await app.workbench.plots.openPlotIn('editor'); +// await app.workbench.plots.waitForPlotInEditor(); +// }); + +// await test.step('Save plot from editor', async () => { +// await app.workbench.plots.savePlotFromEditor({ name: 'Python-scatter-editor', format: 'JPEG' }); +// await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter-editor.jpeg']); +// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); +// }); + +// }); + +// test('Python - Verify bqplot Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, bgplot, '.svg-figure'); +// }); + +// test('Python - Verify ipydatagrid Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, ipydatagrid, 'canvas:nth-child(1)'); +// }); + +// test('Python - Verify ipyleaflet Python widget ', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, ipyleaflet, '.leaflet-container'); +// }); + +// test('Python - Verify hvplot can load with plotly extension', { +// tag: [tags.WEB, tags.WIN], +// annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], +// }, async function ({ app }) { +// // run line by line due to https://github.com/posit-dev/positron/issues/5991 +// await runScriptAndValidatePlot(app, plotly, '.plotly', false, true); +// }); + +// test('Python - Verify hvplot with plotly extension works in block execution', { +// tag: [tags.WEB, tags.WIN], +// annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], +// }, async function ({ app }) { +// // Test that our fix allows hvplot to work when executed as a block +// await runScriptAndValidatePlot(app, plotly, '.plotly', false, false); +// }); + +// test('Python - Verify ipytree Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, ipytree, '.jstree-container-ul'); + +// // fullauxbar layout needed for some smaller windows +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + +// // tree should be expanded by default +// const treeNodes = app.workbench.plots.getWebviewPlotLocator('.jstree-container-ul .jstree-node'); +// await expect(treeNodes).toHaveCount(9); + +// // collapse the tree, only parent nodes should be visible +// await treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon +// await expect(treeNodes).toHaveCount(3); +// }); + +// test('Python - Verify ipywidget.Output Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await app.workbench.console.pasteCodeToConsole(ipywidgetOutput); +// await app.workbench.console.sendEnterKey(); +// await app.workbench.plots.waitForWebviewPlot('.widget-output', 'attached'); + +// // Redirect a print statement to the Output widget. +// await app.workbench.console.pasteCodeToConsole(`with output: +// print('Hello, world!') +// `); // Empty line needed for the statement to be considered complete. +// await app.workbench.console.sendEnterKey(); +// await app.workbench.plots.waitForWebviewPlot('.widget-output .jp-OutputArea-child'); + +// // The printed statement should not be shown in the console. +// await app.workbench.console.waitForConsoleContents('Hello World', { expectedCount: 0 }); + +// }); + +// test('Python - Verify bokeh Python widget', { +// tag: [tags.WEB, tags.WIN] +// }, async function ({ app }) { +// await app.workbench.console.executeCode('Python', bokeh); + +// // selector not factored out as it is unique to bokeh +// const bokehCanvas = '.bk-Canvas'; +// await app.workbench.plots.waitForWebviewPlot(bokehCanvas, 'visible', app.web); +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + +// // selector not factored out as it is unique to bokeh +// let canvasLocator: Locator; +// if (!app.web) { +// await app.workbench.plots.getWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); +// canvasLocator = app.workbench.plots.getWebviewPlotLocator(bokehCanvas); +// } else { +// await app.workbench.plots.getDeepWebWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); +// canvasLocator = app.workbench.plots.getDeepWebWebviewPlotLocator(bokehCanvas); +// } +// const boundingBox = await canvasLocator.boundingBox(); + +// // plot capture before zoom +// const bufferBeforeZoom = await canvasLocator.screenshot(); + +// if (boundingBox) { +// await app.code.driver.clickAndDrag({ +// from: { +// x: boundingBox.x + boundingBox.width / 3, +// y: boundingBox.y + boundingBox.height / 3 +// }, +// to: { +// x: boundingBox.x + 2 * (boundingBox.width / 3), +// y: boundingBox.y + 2 * (boundingBox.height / 3) +// } +// }); +// } else { +// fail('Bounding box not found'); +// } + +// // plot capture after zoom +// const bufferAfterZoom = await canvasLocator.screenshot(); + +// // two plot captures should be different +// const data = await resembleCompareImages(bufferAfterZoom, bufferBeforeZoom, options); +// expect(data.rawMisMatchPercentage).toBeGreaterThan(0.0); +// }); + +// test('Python - Verify Plot Zoom works (Fit vs. 200%)', { tag: [tags.WEB] }, +// async function ({ app, openFile, python, page }, testInfo) { +// await openFile(path.join('workspaces', 'python-plots', 'matplotlib-zoom-example.py')); + +// await test.step('Run Python File in Console', async () => { +// await app.workbench.editor.playButton.click(); +// await app.workbench.plots.waitForCurrentPlot(); +// }); +// const imgLocator = page.getByRole('img', { name: /%run/ }); + +// await app.workbench.plots.setThePlotZoom('Fit'); +// await page.waitForTimeout(2000); +// await dismissPlotZoomTooltip(page); +// const bufferFit1 = await imgLocator.screenshot(); +// await app.workbench.plots.setThePlotZoom('200%'); + +// await page.waitForTimeout(2000); +// await dismissPlotZoomTooltip(page); +// const bufferZoom = await imgLocator.screenshot(); +// // Compare: Fit vs 200% +// const resultZoom = await resembleCompareImages(bufferFit1, bufferZoom, options); +// await testInfo.attach('fit-vs-zoom', { +// body: resultZoom.getBuffer(true), +// contentType: 'image/png' +// }); +// expect(resultZoom.rawMisMatchPercentage).toBeGreaterThan(1.5); // should be large diff + +// await app.workbench.plots.setThePlotZoom('Fit'); +// await page.waitForTimeout(2000); +// await dismissPlotZoomTooltip(page); +// const bufferFit2 = await imgLocator.screenshot(); +// // Compare: Fit vs Fit again +// const resultBack = await resembleCompareImages(bufferFit1, bufferFit2, options); +// await testInfo.attach('fit-vs-fit', { +// body: resultBack.getBuffer(true), +// contentType: 'image/png' +// }); +// expect(resultBack.rawMisMatchPercentage).toBeLessThan(0.75); // should be small diff +// }); + +// }); + +// test.describe('R Plots', { +// tag: [tags.ARK] +// }, () => { + +// test.beforeEach(async function ({ sessions, hotKeys }) { +// await hotKeys.stackedLayout(); +// await sessions.start('r'); +// }); + +// test.afterEach(async function ({ app, hotKeys }) { +// await hotKeys.fullSizeSecondarySidebar(); +// await app.workbench.plots.clearPlots(); +// await app.workbench.plots.waitForNoPlots(); +// }); + +// test.afterAll(async function ({ cleanup }) { +// await cleanup.removeTestFiles(['r-cars.svg', 'r-cars.jpeg', 'plot.png']); +// }); + +// test('R - Verify basic plot functionality', { +// tag: [tags.CRITICAL, tags.WEB, tags.WIN] +// }, async function ({ app, logger, headless }, testInfo) { +// logger.log('Sending code to console'); +// await app.workbench.console.executeCode('R', rBasicPlot); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.toasts.closeAll(); + +// const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); +// await compareImages({ +// app, +// buffer, +// diffScreenshotName: 'autosDiff', +// masterScreenshotName: `autos-${process.platform}`, +// testInfo +// }); + +// if (!headless) { +// await app.workbench.plots.copyCurrentPlotToClipboard(); + +// let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); +// expect(clipboardImageBuffer).not.toBeNull(); + +// await app.workbench.clipboard.clearClipboard(); +// clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); +// expect(clipboardImageBuffer).toBeNull(); +// } + +// await test.step('Verify plot can be opened in editor', async () => { +// await app.workbench.plots.openPlotIn('editor'); +// await app.workbench.plots.waitForPlotInEditor(); +// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); +// }); + +// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); +// await app.workbench.plots.clearPlots(); +// await app.workbench.layouts.enterLayout('stacked'); +// await app.workbench.plots.waitForNoPlots(); +// }); + +// test('R - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { +// await verifyPlotInNewWindow(app, 'R', rBasicPlot); +// }); + +// test('R - Verify saving an R plot', { tag: [tags.WIN] }, async function ({ app }) { +// await test.step('Sending code to console to create plot', async () => { +// await app.workbench.console.executeCode('R', rSavePlot); +// await app.workbench.plots.waitForCurrentPlot(); +// }); + +// await test.step('Save plot as PNG', async () => { +// await app.workbench.plots.savePlotFromPlotsPane({ name: 'plot', format: 'PNG' }); +// await app.workbench.explorer.verifyExplorerFilesExist(['plot.png']); +// }); + +// await test.step('Save plot as SVG', async () => { +// await app.workbench.plots.savePlotFromPlotsPane({ name: 'R-cars', format: 'SVG' }); +// await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.svg']); +// }); + +// await test.step('Open plot in editor', async () => { +// await app.workbench.plots.openPlotIn('editor'); +// await app.workbench.plots.waitForPlotInEditor(); +// }); + +// await test.step('Save plot from editor as JPEG', async () => { +// await app.workbench.plots.savePlotFromEditor({ name: 'R-cars', format: 'JPEG' }); +// await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.jpeg']); +// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); +// }); +// }); + +// test('R - Verify rplot plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await app.workbench.console.pasteCodeToConsole(rplot); +// await app.workbench.console.sendEnterKey(); +// await app.workbench.plots.waitForCurrentPlot(); +// }); + +// test('R - Verify highcharter plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, highcharter, 'svg', app.web); +// }); + +// test('R - Verify leaflet plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, leaflet, '.leaflet', app.web); +// }); + +// test('R - Verify plotly plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await runScriptAndValidatePlot(app, rPlotly, '.plot-container', app.web); +// }); + +// test('R - Two simultaneous plots', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { +// await app.workbench.console.pasteCodeToConsole(rTwoPlots, true); +// await app.workbench.plots.waitForCurrentPlot(); +// await app.workbench.plots.expectPlotThumbnailsCountToBe(2); +// }); + +// test('R - Plot building', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + +// await app.workbench.plots.enlargePlotArea(); + +// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 2))', true); +// await app.workbench.console.pasteCodeToConsole('plot(1:5)', true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.console.pasteCodeToConsole('plot(2:6)', true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.console.pasteCodeToConsole('plot(3:7)', true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.console.pasteCodeToConsole('plot(4:8)', true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.console.pasteCodeToConsole('plot(5:9)', true); +// await app.workbench.plots.waitForCurrentPlot(); +// await app.workbench.plots.expectPlotThumbnailsCountToBe(2); + +// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); +// await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); +// await app.workbench.plots.waitForCurrentPlot(); +// await app.workbench.plots.expectPlotThumbnailsCountToBe(3); + +// await app.workbench.plots.restorePlotArea(); +// }); + +// test('R - Figure margins', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + +// await app.workbench.plots.enlargePlotArea(); + +// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 1))', true); +// await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); +// await app.workbench.console.pasteCodeToConsole('plot(2:20)', true); +// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await app.workbench.plots.restorePlotArea(); +// }); + +// test('R - plot and save in one block', { tag: [tags.WEB, tags.WIN] }, async function ({ app, runCommand }) { + +// await app.workbench.console.clearButton.click(); +// await app.workbench.console.restartButton.click(); + +// await app.workbench.console.waitForConsoleContents('restarted', { expectedCount: 1 }); + +// await app.workbench.console.pasteCodeToConsole(rPlotAndSave, true); +// await app.workbench.plots.waitForCurrentPlot(); + +// await runCommand('workbench.action.fullSizedAuxiliaryBar'); + +// const vars = await app.workbench.variables.getFlatVariables(); +// const filePath = vars.get('tempfile')?.value; + +// expect(fs.existsSync(filePath?.replaceAll('"', '')!)).toBe(true); + +// await app.workbench.layouts.enterLayout('stacked'); +// }); + +// }); +// }); + +// const options: ComparisonOptions = { +// output: { +// errorColor: { +// red: 255, +// green: 0, +// blue: 255 +// }, +// errorType: 'movement', +// transparency: 0.3, +// largeImageThreshold: 1200, +// useCrossOrigin: false +// }, +// scaleToSameSize: true, +// ignore: 'antialiasing', +// }; + +// async function runScriptAndValidatePlot(app: Application, script: string, locator: string, RWeb = false, runLineByLine = false) { +// await app.workbench.hotKeys.fullSizeSecondarySidebar(); +// const lines: string[] = runLineByLine ? script.split('\n') : [script]; + +// await expect(async () => { +// for (const line of lines) { +// await app.workbench.console.pasteCodeToConsole(line); +// await app.workbench.console.sendEnterKey(); +// } +// await app.workbench.console.waitForConsoleExecution({ timeout: 15000 }); +// await app.workbench.plots.waitForWebviewPlot(locator, 'visible', RWeb); +// }, 'Send code to console and verify plot renders').toPass({ timeout: 60000 }); +// } + +// async function verifyPlotInNewWindow(app: Application, language: "Python" | "R", plotCode: string) { +// const plots = app.workbench.plots; +// await test.step(`Create a ${language} plot`, async () => { +// await app.workbench.console.executeCode(language, plotCode); +// await plots.waitForCurrentPlot(); +// }); +// await test.step('Open plot in new window', async () => { +// await plots.openPlotIn('new window'); +// await app.workbench.layouts.enterLayout('stacked'); +// }); +// } + +// async function compareImages({ +// app, +// buffer, +// diffScreenshotName, +// masterScreenshotName, +// testInfo +// }: { +// app: any; +// buffer: Buffer; +// diffScreenshotName: string; +// masterScreenshotName: string; +// testInfo: any; +// }) { +// await test.step('compare images', async () => { +// if (process.env.GITHUB_ACTIONS && !app.web) { +// const data = await resembleCompareImages(fs.readFileSync(path.join(__dirname, `${masterScreenshotName}.png`),), buffer, options); + +// if (data.rawMisMatchPercentage > 2.0) { +// if (data.getBuffer) { +// await testInfo.attach(diffScreenshotName, { body: data.getBuffer(true), contentType: 'image/png' }); +// } + +// // Capture a new master image in CI +// const newMaster = await app.workbench.plots.currentPlot.screenshot(); +// await testInfo.attach(masterScreenshotName, { body: newMaster, contentType: 'image/png' }); + +// // Fail the test with mismatch details +// fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`); +// } +// } +// }); +// } + +// const pythonDynamicPlot = `import pandas as pd +// import matplotlib.pyplot as plt +// data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'], +// 'age': [20, 20, 21, 20, 21, 20], +// 'math_marks': [100, 90, 91, 98, 92, 95], +// 'physics_marks': [90, 100, 91, 92, 98, 95], +// 'chem_marks': [93, 89, 99, 92, 94, 92] +// } + +// df = pd.DataFrame(data_dict) + +// df.plot(kind='scatter', +// x='math_marks', +// y='physics_marks', +// color='red') + +// plt.title('ScatterPlot') +// plt.show()`; + + +// const pythonStaticPlot = `import graphviz as gv +// import IPython + +// h = gv.Digraph(format="svg") +// names = [ +// "A", +// "B", +// "C", +// ] + +// # Specify edges +// h.edge("A", "B") +// h.edge("A", "C") + +// IPython.display.display_png(h)`; + +// const pythonPlotActions1 = `import graphviz as gv +// import IPython + +// h = gv.Digraph(format="svg") +// names = [ +// "A", +// "B", +// "C", +// ] + +// # Specify edges +// h.edge("A", "B") +// h.edge("A", "C") + +// IPython.display.display_png(h)`; + +// const pythonPlotActions2 = `import matplotlib.pyplot as plt + +// # x axis values +// x = [1,2,3] +// # corresponding y axis values +// y = [2,4,1] + +// # plotting the points +// plt.plot(x, y) + +// # naming the x axis +// plt.xlabel('x - axis') +// # naming the y axis +// plt.ylabel('y - axis') + +// # giving a title to my graph +// plt.title('My first graph!') + +// # function to show the plot +// plt.show()`; + +// const bgplot = `import bqplot.pyplot as bplt +// import numpy as np + +// x = np.linspace(-10, 10, 100) +// y = np.sin(x) +// axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}} + +// fig = bplt.figure(title="Line Chart") +// line = bplt.plot( +// x=x, y=y, axes_options=axes_opts +// ) + +// bplt.show()`; + +// const ipydatagrid = `import pandas as pd +// from ipydatagrid import DataGrid +// data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"]) +// DataGrid(data) +// DataGrid(data, selection_mode="cell", editable=True)`; + +// const ipyleaflet = `from ipyleaflet import Map, Marker, display +// center = (52.204793, 360.121558) +// map = Map(center=center, zoom=12) + +// # Add a draggable marker to the map +// # Dragging the marker updates the marker.location value in Python +// marker = Marker(location=center, draggable=True) +// map.add_control(marker) + +// display(map)`; + +// const plotly = `import hvplot.pandas +// import pandas as pd +// hvplot.extension('plotly') +// pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`; -const ipytree = `from ipytree import Tree, Node -tree = Tree(stripes=True) -tree -tree -node1 = Node('node1') -tree.add_node(node1) -node2 = Node('node2') -tree.add_node(node2) -tree.nodes = [node2, node1] -node3 = Node('node3', disabled=True) -node4 = Node('node4') -node5 = Node('node5', [Node('1'), Node('2')]) -node2.add_node(node3) -node2.add_node(node4) -node2.add_node(node5) -tree.add_node(Node('node6'), 1) -node2.add_node(Node('node7'), 2) - -tree`; - -const ipywidgetOutput = `import ipywidgets -output = ipywidgets.Output() -output`; - -const bokeh = `from bokeh.plotting import figure, output_file, show, reset_output -# Proactively reset output in case hvplot has changed anything -reset_output() - -# instantiating the figure object -graph = figure(title = "Bokeh Line Graph") - -# the points to be plotted -x = [1, 2, 3, 4, 5] -y = [5, 4, 3, 2, 1] - -# plotting the line graph -graph.line(x, y) - -# displaying the model -show(graph)`; +// const ipytree = `from ipytree import Tree, Node +// tree = Tree(stripes=True) +// tree +// tree +// node1 = Node('node1') +// tree.add_node(node1) +// node2 = Node('node2') +// tree.add_node(node2) +// tree.nodes = [node2, node1] +// node3 = Node('node3', disabled=True) +// node4 = Node('node4') +// node5 = Node('node5', [Node('1'), Node('2')]) +// node2.add_node(node3) +// node2.add_node(node4) +// node2.add_node(node5) +// tree.add_node(Node('node6'), 1) +// node2.add_node(Node('node7'), 2) + +// tree`; + +// const ipywidgetOutput = `import ipywidgets +// output = ipywidgets.Output() +// output`; + +// const bokeh = `from bokeh.plotting import figure, output_file, show, reset_output +// # Proactively reset output in case hvplot has changed anything +// reset_output() + +// # instantiating the figure object +// graph = figure(title = "Bokeh Line Graph") + +// # the points to be plotted +// x = [1, 2, 3, 4, 5] +// y = [5, 4, 3, 2, 1] + +// # plotting the line graph +// graph.line(x, y) + +// # displaying the model +// show(graph)`; -const rBasicPlot = `cars <- c(1, 3, 6, 4, 9) -plot(cars, type="o", col="blue") -title(main="Autos", col.main="red", font.main=4)`; - -const rSavePlot = `cars <- c(1, 3, 6, 4, 9) -plot(cars, type="o", col="blue") -title(main="Autos", col.main="red", font.main=4)`; - -const rplot = `library('corrr') - -x <- correlate(mtcars) -rplot(x) - -# Common use is following rearrange and shave -x <- rearrange(x, absolute = FALSE) -x <- shave(x) -rplot(x) -rplot(x, print_cor = TRUE) -rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`; - -const highcharter = `library(highcharter) - -data("mpg", "diamonds", "economics_long", package = "ggplot2") - -hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`; - -const leaflet = `library(leaflet) -m = leaflet() %>% addTiles() -m = m %>% setView(-93.65, 42.0285, zoom = 17) -m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`; - -const rPlotly = `library(plotly) -fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box") -fig`; - -const rTwoPlots = `plot(1:10) -plot(1:100)`; - -const rPlotAndSave = `plot(1:10) -tempfile <- tempfile() -grDevices::png(filename = tempfile) -plot(1:20) -dev.off()`; - -async function dismissPlotZoomTooltip(page: Page) { - const plotZoomTooltip = page.getByText('Set the plot zoom'); - if (await plotZoomTooltip.isVisible()) { - page.keyboard.press('Escape'); - } -} +// const rBasicPlot = `cars <- c(1, 3, 6, 4, 9) +// plot(cars, type="o", col="blue") +// title(main="Autos", col.main="red", font.main=4)`; + +// const rSavePlot = `cars <- c(1, 3, 6, 4, 9) +// plot(cars, type="o", col="blue") +// title(main="Autos", col.main="red", font.main=4)`; + +// const rplot = `library('corrr') + +// x <- correlate(mtcars) +// rplot(x) + +// # Common use is following rearrange and shave +// x <- rearrange(x, absolute = FALSE) +// x <- shave(x) +// rplot(x) +// rplot(x, print_cor = TRUE) +// rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`; + +// const highcharter = `library(highcharter) + +// data("mpg", "diamonds", "economics_long", package = "ggplot2") + +// hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`; + +// const leaflet = `library(leaflet) +// m = leaflet() %>% addTiles() +// m = m %>% setView(-93.65, 42.0285, zoom = 17) +// m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`; + +// const rPlotly = `library(plotly) +// fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box") +// fig`; + +// const rTwoPlots = `plot(1:10) +// plot(1:100)`; + +// const rPlotAndSave = `plot(1:10) +// tempfile <- tempfile() +// grDevices::png(filename = tempfile) +// plot(1:20) +// dev.off()`; + +// async function dismissPlotZoomTooltip(page: Page) { +// const plotZoomTooltip = page.getByText('Set the plot zoom'); +// if (await plotZoomTooltip.isVisible()) { +// page.keyboard.press('Escape'); +// } +// } From ddef8750f39a01dc7b9843475250df75df7d0996 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 5 Sep 2025 14:48:39 -0500 Subject: [PATCH 2/8] remove copy paste test for now --- .../data-explorer-copy-paste.test.ts | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts diff --git a/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts b/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts deleted file mode 100644 index fab8c302444d..000000000000 --- a/test/e2e/tests/data-explorer/data-explorer-copy-paste.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2025 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -/* - * Verifies Data Explorer copy and paste behavior: - * - Copying and pasting works with unsorted data - * - Copying and pasting works with sorted data - * - Copying and pasting works with pinned rows and columns - */ - -// import { join } from 'path'; -import { test, tags } from '../_test.setup'; - -test.use({ - suiteId: __filename -}); - -// const openFileWith = ['DuckDB', 'Code']; - - -test.describe('Data Explorer: Copy/Paste', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLORER] }, () => { - - test.beforeEach(async function ({ app, openDataFile }) { - // const { dataExplorer, } = app.workbench; - // await openDataFile(join('data-files', 'small_file.csv')); - - const { dataExplorer, console, variables, sessions } = app.workbench; - await sessions.start('r'); - await console.pasteCodeToConsole('df <- read.csv("data-files/small_file.csv")', true) - await variables.doubleClickVariableRow('df'); - - - - // maximize data view - await dataExplorer.maximize(); - await dataExplorer.waitForIdle(); - await dataExplorer.summaryPanel.hide(); - }); - - test.afterEach(async function ({ hotKeys }) { - await hotKeys.closeAllEditors(); - }); - - test("Copy and Paste with unsorted data", async function ({ app }) { - const { dataExplorer, clipboard } = app.workbench; - - // verify copy and paste on columns - await dataExplorer.grid.clickColumnHeader('column3'); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column3\n56\n13\n41\n12\n17\n99\n89\n33\n43\n47'); - - // verify copy and paste on rows - await dataExplorer.grid.clickRowHeader(9); - await clipboard.copy(); - // TODO: bug - // await clipboard.expectClipboardTextToBe('column0\tcolumn1\tcolumn2\tcolumn3\tcolumn4\tcolumn5\tcolumn6\tcolumn7\tcolumn8\tcolumn9\n22\t9\t4\t43\t40\t73\t79\t98\t80\t24'); - - // verify copy and paste on cell - await dataExplorer.grid.clickCell(6, 2); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('8'); - - // TODO: bug - for data frame case - // verify copy and paste on range - // await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 1, col: 1 } }); - // await clipboard.copy(); - // await clipboard.expectClipboardTextToBe('column0\tcolumn1\n82\t69\n8\t79'); - }) - - test("Copy and Paste with sorted data", async function ({ app }) { - const { dataExplorer, clipboard } = app.workbench; - - // verify copy and paste on columns - await dataExplorer.grid.selectColumnAction(4, 'Sort Descending'); - await dataExplorer.grid.clickColumnHeader('column3'); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); - - // verify copy and paste on rows - await dataExplorer.grid.clickRowHeader(4); - // await clipboard.copy(); - // await clipboard.expectClipboardTextToBe('column0\tcolumn1\tcolumn2\tcolumn3\tcolumn4\tcolumn5\tcolumn6\tcolumn7\tcolumn8\tcolumn9\n22\t9\t4\t43\t40\t73\t79\t98\t80\t24'); - - // verify copy and paste on cell - await dataExplorer.grid.clickCell(6, 4); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('33'); - - // verify copy and paste on range - // await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 2 }, endCell: { row: 1, col: 3 } }); - // await clipboard.copy(); - // await clipboard.expectClipboardTextToBe('column2\tcolumn3\n47\t99\n8\t89'); - }); - - test.skip("Copy and Paste with pinned data", async function ({ app }) { - const { dataExplorer, clipboard } = app.workbench; - - // pin column 4 - await dataExplorer.grid.pinColumn(4); - await dataExplorer.grid.expectColumnsToBePinned(['column4']); - - // pin row 5 - await dataExplorer.grid.pinRow(5); - await dataExplorer.grid.expectRowsToBePinned([5]); - - // select range - await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 2, col: 2 } }); - await dataExplorer.grid.expectRangeToBeSelected({ - rows: [5, 0, 1], - cols: [4, 0, 1] - }); - - await dataExplorer.grid.clickUpperLeftCorner() - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column4\tcolumn0\tcolumn1\n50\t41\t42\n9\t82\t69\n8\t8\t79'); - }); - // test("Column sorting works with row and column pins", async function ({ app }) { - // const { dataExplorer } = app.workbench; - - // // pin column 4 - // await dataExplorer.grid.pinColumn(4); - // await dataExplorer.grid.expectColumnsToBePinned(['column4']); - - // // pin row 5 - // await dataExplorer.grid.pinRow(5); - // await dataExplorer.grid.expectRowsToBePinned([5]); - - // // sort by column 4 - // await dataExplorer.grid.sortColumn(4); - // await dataExplorer.grid.expectRowOrderToBe([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]); - // }); - - test.skip("Copy and Paste works with pinned rows and cols", async function ({ app }) { - const { dataExplorer, clipboard } = app.workbench; - - // pin column 4 - await dataExplorer.grid.pinColumn(4); - await dataExplorer.grid.expectColumnsToBePinned(['column4']); - - // pin row 5 - await dataExplorer.grid.pinRow(5); - await dataExplorer.grid.expectRowsToBePinned([5]); - - // select range - await dataExplorer.grid.selectRange({ startCell: { row: 0, col: 0 }, endCell: { row: 2, col: 2 } }); - await dataExplorer.grid.expectRangeToBeSelected({ - rows: [5, 0, 1], - cols: [4, 0, 1] - }); - - await dataExplorer.grid.clickUpperLeftCorner() - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column4\tcolumn0\tcolumn1\n50\t41\t42\n9\t82\t69\n8\t8\t79'); - }); - - test("Copy and Paste works with sorted data", async function ({ app }) { - const { dataExplorer, clipboard } = app.workbench; - - // verify basic copy paste works on sorted data - await clipboard.expectClipboardTextToBe('column3\n56\n13\n41\n12\n17\n99\n89\n33\n43\n47'); - await dataExplorer.grid.selectColumnAction(3, 'Sort Descending'); - await dataExplorer.grid.clickColumnHeader('column3'); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); - - // pin column and confirm still sorted - await dataExplorer.grid.pinColumn(3); - await dataExplorer.grid.expectColumnsToBePinned(['column3']); - await clipboard.copy(); - await clipboard.expectClipboardTextToBe('column3\n99\n89\n56\n47\n43\n41\n33\n17\n13\n12'); - - // pin row and confirm new sort order - await dataExplorer.grid.pinRow(5); - await dataExplorer.grid.expectRowsToBePinned([5]); - await clipboard.expectClipboardTextToBe('column3\n41\n99\n89\n56\n47\n43\n33\n17\n13\n12'); - }); -}) From 87f3784649a9626867bd4c00a79b687f5ca64fd0 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 5 Sep 2025 14:50:18 -0500 Subject: [PATCH 3/8] oops --- test/e2e/tests/plots/plots.test.ts | 1568 ++++++++++++++-------------- 1 file changed, 784 insertions(+), 784 deletions(-) diff --git a/test/e2e/tests/plots/plots.test.ts b/test/e2e/tests/plots/plots.test.ts index f4dc23dacc41..02aed1e16956 100644 --- a/test/e2e/tests/plots/plots.test.ts +++ b/test/e2e/tests/plots/plots.test.ts @@ -1,786 +1,786 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. -// * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import * as path from 'path'; -// import { test, expect, tags } from '../_test.setup'; -// const resembleCompareImages = require('resemblejs/compareImages'); -// import { ComparisonOptions } from 'resemblejs'; -// import * as fs from 'fs'; -// import { fail } from 'assert'; -// import { Application } from '../../infra'; -// import { Locator, Page } from '@playwright/test'; - -// test.use({ -// suiteId: __filename -// }); - -// test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { -// test.describe('Python Plots', () => { - -// test.beforeEach(async function ({ sessions, hotKeys }) { -// await sessions.start('python'); -// await hotKeys.stackedLayout(); -// }); - -// test.afterEach(async function ({ app, hotKeys }) { -// await hotKeys.fullSizeSecondarySidebar(); -// await app.workbench.plots.clearPlots(); -// await app.workbench.plots.waitForNoPlots(); -// }); - -// test.afterAll(async function ({ cleanup }) { -// await cleanup.removeTestFiles(['Python-scatter.jpeg', 'Python-scatter-editor.jpeg']); -// }); - -// test('Python - Verify basic plot functionality - Dynamic Plot', { -// tag: [tags.CRITICAL, tags.WEB, tags.WIN] -// }, async function ({ app, logger, headless }, testInfo) { -// // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/ -// logger.log('Sending code to console'); -// await app.workbench.console.executeCode('Python', pythonDynamicPlot); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.toasts.closeAll(); - -// const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); -// await compareImages({ -// app, -// buffer, -// diffScreenshotName: 'pythonScatterplotDiff', -// masterScreenshotName: `pythonScatterplot-${process.platform}`, -// testInfo: testInfo -// }); - -// if (!headless) { -// await app.workbench.plots.copyCurrentPlotToClipboard(); - -// let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); -// expect(clipboardImageBuffer).not.toBeNull(); - -// await app.workbench.clipboard.clearClipboard(); -// clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); -// expect(clipboardImageBuffer).toBeNull(); -// } - -// await test.step('Verify plot can be opened in editor', async () => { -// await app.workbench.plots.openPlotIn('editor'); -// await app.workbench.plots.waitForPlotInEditor(); -// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); -// }); - -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); -// await app.workbench.plots.clearPlots(); -// await app.workbench.layouts.enterLayout('stacked'); -// await app.workbench.plots.waitForNoPlots(); -// }); - -// test('Python - Verify basic plot functionality - Static Plot', { -// tag: [tags.CRITICAL, tags.WEB, tags.WIN] -// }, async function ({ app, logger }, testInfo) { -// logger.log('Sending code to console'); -// await app.workbench.console.executeCode('Python', pythonStaticPlot); -// await app.workbench.plots.waitForCurrentStaticPlot(); - -// await app.workbench.toasts.closeAll(); - -// const buffer = await app.workbench.plots.getCurrentStaticPlotAsBuffer(); -// await compareImages({ -// app, -// buffer, -// diffScreenshotName: 'graphvizDiff', -// masterScreenshotName: `graphviz-${process.platform}`, -// testInfo -// }); - -// await test.step('Verify plot can be opened in editor', async () => { -// await app.workbench.plots.openPlotIn('editor'); -// await app.workbench.plots.waitForPlotInEditor(); -// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); -// }); - -// }); - -// test('Python - Verify the plots pane action bar - Plot actions', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// const plots = app.workbench.plots; - -// // default plot pane state for action bar -// await expect(plots.plotSizeButton).not.toBeVisible(); -// await expect(plots.savePlotFromPlotsPaneButton).not.toBeVisible(); -// await expect(plots.copyPlotButton).not.toBeVisible(); -// await expect(plots.zoomPlotButton).not.toBeVisible(); - -// // create plots separately so that the order is known -// await app.workbench.console.executeCode('Python', pythonPlotActions1); -// await plots.waitForCurrentStaticPlot(); -// await app.workbench.console.executeCode('Python', pythonPlotActions2); -// await plots.waitForCurrentPlot(); - -// // expand the plot pane to show the action bar -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); -// await expect(plots.clearPlotsButton).not.toBeDisabled(); -// await expect(plots.nextPlotButton).toBeDisabled(); -// await expect(plots.previousPlotButton).not.toBeDisabled(); -// await expect(plots.plotSizeButton).not.toBeDisabled(); -// await expect(plots.savePlotFromPlotsPaneButton).not.toBeDisabled(); -// await expect(plots.copyPlotButton).not.toBeDisabled(); - -// // switch to fixed size plot -// await plots.previousPlotButton.click(); -// await plots.waitForCurrentStaticPlot(); - -// // switching to fixed size plot changes action bar -// await expect(plots.zoomPlotButton).toBeVisible(); -// await expect(plots.plotSizeButton).not.toBeVisible(); -// await expect(plots.clearPlotsButton).not.toBeDisabled(); -// await expect(plots.nextPlotButton).not.toBeDisabled(); -// await expect(plots.previousPlotButton).toBeDisabled(); -// await expect(plots.zoomPlotButton).not.toBeDisabled(); - -// // switch back to dynamic plot -// await plots.nextPlotButton.click(); -// await plots.waitForCurrentPlot(); -// await expect(plots.zoomPlotButton).toBeVisible(); -// await expect(plots.plotSizeButton).toBeVisible(); -// await expect(plots.clearPlotsButton).not.toBeDisabled(); -// await expect(plots.nextPlotButton).toBeDisabled(); -// await expect(plots.previousPlotButton).not.toBeDisabled(); -// await expect(plots.plotSizeButton).not.toBeDisabled(); -// }); - -// test('Python - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { -// await verifyPlotInNewWindow(app, 'Python', pythonDynamicPlot); -// }); - -// test('Python - Verify saving a Python plot', { tag: [tags.WIN] }, async function ({ app }) { -// await test.step('Sending code to console to create plot', async () => { -// await app.workbench.console.executeCode('Python', pythonDynamicPlot); -// await app.workbench.plots.waitForCurrentPlot(); -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); -// }); - -// await test.step('Save plot', async () => { -// await app.workbench.plots.savePlotFromPlotsPane({ name: 'Python-scatter', format: 'JPEG' }); -// await app.workbench.layouts.enterLayout('stacked'); -// await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter.jpeg']); -// }); - -// await test.step('Open plot in editor', async () => { -// await app.workbench.plots.openPlotIn('editor'); -// await app.workbench.plots.waitForPlotInEditor(); -// }); - -// await test.step('Save plot from editor', async () => { -// await app.workbench.plots.savePlotFromEditor({ name: 'Python-scatter-editor', format: 'JPEG' }); -// await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter-editor.jpeg']); -// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); -// }); - -// }); - -// test('Python - Verify bqplot Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, bgplot, '.svg-figure'); -// }); - -// test('Python - Verify ipydatagrid Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, ipydatagrid, 'canvas:nth-child(1)'); -// }); - -// test('Python - Verify ipyleaflet Python widget ', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, ipyleaflet, '.leaflet-container'); -// }); - -// test('Python - Verify hvplot can load with plotly extension', { -// tag: [tags.WEB, tags.WIN], -// annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], -// }, async function ({ app }) { -// // run line by line due to https://github.com/posit-dev/positron/issues/5991 -// await runScriptAndValidatePlot(app, plotly, '.plotly', false, true); -// }); - -// test('Python - Verify hvplot with plotly extension works in block execution', { -// tag: [tags.WEB, tags.WIN], -// annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], -// }, async function ({ app }) { -// // Test that our fix allows hvplot to work when executed as a block -// await runScriptAndValidatePlot(app, plotly, '.plotly', false, false); -// }); - -// test('Python - Verify ipytree Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, ipytree, '.jstree-container-ul'); - -// // fullauxbar layout needed for some smaller windows -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - -// // tree should be expanded by default -// const treeNodes = app.workbench.plots.getWebviewPlotLocator('.jstree-container-ul .jstree-node'); -// await expect(treeNodes).toHaveCount(9); - -// // collapse the tree, only parent nodes should be visible -// await treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon -// await expect(treeNodes).toHaveCount(3); -// }); - -// test('Python - Verify ipywidget.Output Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await app.workbench.console.pasteCodeToConsole(ipywidgetOutput); -// await app.workbench.console.sendEnterKey(); -// await app.workbench.plots.waitForWebviewPlot('.widget-output', 'attached'); - -// // Redirect a print statement to the Output widget. -// await app.workbench.console.pasteCodeToConsole(`with output: -// print('Hello, world!') -// `); // Empty line needed for the statement to be considered complete. -// await app.workbench.console.sendEnterKey(); -// await app.workbench.plots.waitForWebviewPlot('.widget-output .jp-OutputArea-child'); - -// // The printed statement should not be shown in the console. -// await app.workbench.console.waitForConsoleContents('Hello World', { expectedCount: 0 }); - -// }); - -// test('Python - Verify bokeh Python widget', { -// tag: [tags.WEB, tags.WIN] -// }, async function ({ app }) { -// await app.workbench.console.executeCode('Python', bokeh); - -// // selector not factored out as it is unique to bokeh -// const bokehCanvas = '.bk-Canvas'; -// await app.workbench.plots.waitForWebviewPlot(bokehCanvas, 'visible', app.web); -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); - -// // selector not factored out as it is unique to bokeh -// let canvasLocator: Locator; -// if (!app.web) { -// await app.workbench.plots.getWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); -// canvasLocator = app.workbench.plots.getWebviewPlotLocator(bokehCanvas); -// } else { -// await app.workbench.plots.getDeepWebWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); -// canvasLocator = app.workbench.plots.getDeepWebWebviewPlotLocator(bokehCanvas); -// } -// const boundingBox = await canvasLocator.boundingBox(); - -// // plot capture before zoom -// const bufferBeforeZoom = await canvasLocator.screenshot(); - -// if (boundingBox) { -// await app.code.driver.clickAndDrag({ -// from: { -// x: boundingBox.x + boundingBox.width / 3, -// y: boundingBox.y + boundingBox.height / 3 -// }, -// to: { -// x: boundingBox.x + 2 * (boundingBox.width / 3), -// y: boundingBox.y + 2 * (boundingBox.height / 3) -// } -// }); -// } else { -// fail('Bounding box not found'); -// } - -// // plot capture after zoom -// const bufferAfterZoom = await canvasLocator.screenshot(); - -// // two plot captures should be different -// const data = await resembleCompareImages(bufferAfterZoom, bufferBeforeZoom, options); -// expect(data.rawMisMatchPercentage).toBeGreaterThan(0.0); -// }); - -// test('Python - Verify Plot Zoom works (Fit vs. 200%)', { tag: [tags.WEB] }, -// async function ({ app, openFile, python, page }, testInfo) { -// await openFile(path.join('workspaces', 'python-plots', 'matplotlib-zoom-example.py')); - -// await test.step('Run Python File in Console', async () => { -// await app.workbench.editor.playButton.click(); -// await app.workbench.plots.waitForCurrentPlot(); -// }); -// const imgLocator = page.getByRole('img', { name: /%run/ }); - -// await app.workbench.plots.setThePlotZoom('Fit'); -// await page.waitForTimeout(2000); -// await dismissPlotZoomTooltip(page); -// const bufferFit1 = await imgLocator.screenshot(); -// await app.workbench.plots.setThePlotZoom('200%'); - -// await page.waitForTimeout(2000); -// await dismissPlotZoomTooltip(page); -// const bufferZoom = await imgLocator.screenshot(); -// // Compare: Fit vs 200% -// const resultZoom = await resembleCompareImages(bufferFit1, bufferZoom, options); -// await testInfo.attach('fit-vs-zoom', { -// body: resultZoom.getBuffer(true), -// contentType: 'image/png' -// }); -// expect(resultZoom.rawMisMatchPercentage).toBeGreaterThan(1.5); // should be large diff - -// await app.workbench.plots.setThePlotZoom('Fit'); -// await page.waitForTimeout(2000); -// await dismissPlotZoomTooltip(page); -// const bufferFit2 = await imgLocator.screenshot(); -// // Compare: Fit vs Fit again -// const resultBack = await resembleCompareImages(bufferFit1, bufferFit2, options); -// await testInfo.attach('fit-vs-fit', { -// body: resultBack.getBuffer(true), -// contentType: 'image/png' -// }); -// expect(resultBack.rawMisMatchPercentage).toBeLessThan(0.75); // should be small diff -// }); - -// }); - -// test.describe('R Plots', { -// tag: [tags.ARK] -// }, () => { - -// test.beforeEach(async function ({ sessions, hotKeys }) { -// await hotKeys.stackedLayout(); -// await sessions.start('r'); -// }); - -// test.afterEach(async function ({ app, hotKeys }) { -// await hotKeys.fullSizeSecondarySidebar(); -// await app.workbench.plots.clearPlots(); -// await app.workbench.plots.waitForNoPlots(); -// }); - -// test.afterAll(async function ({ cleanup }) { -// await cleanup.removeTestFiles(['r-cars.svg', 'r-cars.jpeg', 'plot.png']); -// }); - -// test('R - Verify basic plot functionality', { -// tag: [tags.CRITICAL, tags.WEB, tags.WIN] -// }, async function ({ app, logger, headless }, testInfo) { -// logger.log('Sending code to console'); -// await app.workbench.console.executeCode('R', rBasicPlot); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.toasts.closeAll(); - -// const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); -// await compareImages({ -// app, -// buffer, -// diffScreenshotName: 'autosDiff', -// masterScreenshotName: `autos-${process.platform}`, -// testInfo -// }); - -// if (!headless) { -// await app.workbench.plots.copyCurrentPlotToClipboard(); - -// let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); -// expect(clipboardImageBuffer).not.toBeNull(); - -// await app.workbench.clipboard.clearClipboard(); -// clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); -// expect(clipboardImageBuffer).toBeNull(); -// } - -// await test.step('Verify plot can be opened in editor', async () => { -// await app.workbench.plots.openPlotIn('editor'); -// await app.workbench.plots.waitForPlotInEditor(); -// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); -// }); - -// await app.workbench.layouts.enterLayout('fullSizedAuxBar'); -// await app.workbench.plots.clearPlots(); -// await app.workbench.layouts.enterLayout('stacked'); -// await app.workbench.plots.waitForNoPlots(); -// }); - -// test('R - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { -// await verifyPlotInNewWindow(app, 'R', rBasicPlot); -// }); - -// test('R - Verify saving an R plot', { tag: [tags.WIN] }, async function ({ app }) { -// await test.step('Sending code to console to create plot', async () => { -// await app.workbench.console.executeCode('R', rSavePlot); -// await app.workbench.plots.waitForCurrentPlot(); -// }); - -// await test.step('Save plot as PNG', async () => { -// await app.workbench.plots.savePlotFromPlotsPane({ name: 'plot', format: 'PNG' }); -// await app.workbench.explorer.verifyExplorerFilesExist(['plot.png']); -// }); - -// await test.step('Save plot as SVG', async () => { -// await app.workbench.plots.savePlotFromPlotsPane({ name: 'R-cars', format: 'SVG' }); -// await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.svg']); -// }); - -// await test.step('Open plot in editor', async () => { -// await app.workbench.plots.openPlotIn('editor'); -// await app.workbench.plots.waitForPlotInEditor(); -// }); - -// await test.step('Save plot from editor as JPEG', async () => { -// await app.workbench.plots.savePlotFromEditor({ name: 'R-cars', format: 'JPEG' }); -// await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.jpeg']); -// await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); -// }); -// }); - -// test('R - Verify rplot plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await app.workbench.console.pasteCodeToConsole(rplot); -// await app.workbench.console.sendEnterKey(); -// await app.workbench.plots.waitForCurrentPlot(); -// }); - -// test('R - Verify highcharter plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, highcharter, 'svg', app.web); -// }); - -// test('R - Verify leaflet plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, leaflet, '.leaflet', app.web); -// }); - -// test('R - Verify plotly plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await runScriptAndValidatePlot(app, rPlotly, '.plot-container', app.web); -// }); - -// test('R - Two simultaneous plots', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { -// await app.workbench.console.pasteCodeToConsole(rTwoPlots, true); -// await app.workbench.plots.waitForCurrentPlot(); -// await app.workbench.plots.expectPlotThumbnailsCountToBe(2); -// }); - -// test('R - Plot building', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - -// await app.workbench.plots.enlargePlotArea(); - -// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 2))', true); -// await app.workbench.console.pasteCodeToConsole('plot(1:5)', true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.console.pasteCodeToConsole('plot(2:6)', true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.console.pasteCodeToConsole('plot(3:7)', true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.console.pasteCodeToConsole('plot(4:8)', true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.console.pasteCodeToConsole('plot(5:9)', true); -// await app.workbench.plots.waitForCurrentPlot(); -// await app.workbench.plots.expectPlotThumbnailsCountToBe(2); - -// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); -// await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); -// await app.workbench.plots.waitForCurrentPlot(); -// await app.workbench.plots.expectPlotThumbnailsCountToBe(3); - -// await app.workbench.plots.restorePlotArea(); -// }); - -// test('R - Figure margins', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { - -// await app.workbench.plots.enlargePlotArea(); - -// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 1))', true); -// await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); -// await app.workbench.console.pasteCodeToConsole('plot(2:20)', true); -// await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await app.workbench.plots.restorePlotArea(); -// }); - -// test('R - plot and save in one block', { tag: [tags.WEB, tags.WIN] }, async function ({ app, runCommand }) { - -// await app.workbench.console.clearButton.click(); -// await app.workbench.console.restartButton.click(); - -// await app.workbench.console.waitForConsoleContents('restarted', { expectedCount: 1 }); - -// await app.workbench.console.pasteCodeToConsole(rPlotAndSave, true); -// await app.workbench.plots.waitForCurrentPlot(); - -// await runCommand('workbench.action.fullSizedAuxiliaryBar'); - -// const vars = await app.workbench.variables.getFlatVariables(); -// const filePath = vars.get('tempfile')?.value; - -// expect(fs.existsSync(filePath?.replaceAll('"', '')!)).toBe(true); - -// await app.workbench.layouts.enterLayout('stacked'); -// }); - -// }); -// }); - -// const options: ComparisonOptions = { -// output: { -// errorColor: { -// red: 255, -// green: 0, -// blue: 255 -// }, -// errorType: 'movement', -// transparency: 0.3, -// largeImageThreshold: 1200, -// useCrossOrigin: false -// }, -// scaleToSameSize: true, -// ignore: 'antialiasing', -// }; - -// async function runScriptAndValidatePlot(app: Application, script: string, locator: string, RWeb = false, runLineByLine = false) { -// await app.workbench.hotKeys.fullSizeSecondarySidebar(); -// const lines: string[] = runLineByLine ? script.split('\n') : [script]; - -// await expect(async () => { -// for (const line of lines) { -// await app.workbench.console.pasteCodeToConsole(line); -// await app.workbench.console.sendEnterKey(); -// } -// await app.workbench.console.waitForConsoleExecution({ timeout: 15000 }); -// await app.workbench.plots.waitForWebviewPlot(locator, 'visible', RWeb); -// }, 'Send code to console and verify plot renders').toPass({ timeout: 60000 }); -// } - -// async function verifyPlotInNewWindow(app: Application, language: "Python" | "R", plotCode: string) { -// const plots = app.workbench.plots; -// await test.step(`Create a ${language} plot`, async () => { -// await app.workbench.console.executeCode(language, plotCode); -// await plots.waitForCurrentPlot(); -// }); -// await test.step('Open plot in new window', async () => { -// await plots.openPlotIn('new window'); -// await app.workbench.layouts.enterLayout('stacked'); -// }); -// } - -// async function compareImages({ -// app, -// buffer, -// diffScreenshotName, -// masterScreenshotName, -// testInfo -// }: { -// app: any; -// buffer: Buffer; -// diffScreenshotName: string; -// masterScreenshotName: string; -// testInfo: any; -// }) { -// await test.step('compare images', async () => { -// if (process.env.GITHUB_ACTIONS && !app.web) { -// const data = await resembleCompareImages(fs.readFileSync(path.join(__dirname, `${masterScreenshotName}.png`),), buffer, options); - -// if (data.rawMisMatchPercentage > 2.0) { -// if (data.getBuffer) { -// await testInfo.attach(diffScreenshotName, { body: data.getBuffer(true), contentType: 'image/png' }); -// } - -// // Capture a new master image in CI -// const newMaster = await app.workbench.plots.currentPlot.screenshot(); -// await testInfo.attach(masterScreenshotName, { body: newMaster, contentType: 'image/png' }); - -// // Fail the test with mismatch details -// fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`); -// } -// } -// }); -// } - -// const pythonDynamicPlot = `import pandas as pd -// import matplotlib.pyplot as plt -// data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'], -// 'age': [20, 20, 21, 20, 21, 20], -// 'math_marks': [100, 90, 91, 98, 92, 95], -// 'physics_marks': [90, 100, 91, 92, 98, 95], -// 'chem_marks': [93, 89, 99, 92, 94, 92] -// } - -// df = pd.DataFrame(data_dict) - -// df.plot(kind='scatter', -// x='math_marks', -// y='physics_marks', -// color='red') - -// plt.title('ScatterPlot') -// plt.show()`; - - -// const pythonStaticPlot = `import graphviz as gv -// import IPython - -// h = gv.Digraph(format="svg") -// names = [ -// "A", -// "B", -// "C", -// ] - -// # Specify edges -// h.edge("A", "B") -// h.edge("A", "C") - -// IPython.display.display_png(h)`; - -// const pythonPlotActions1 = `import graphviz as gv -// import IPython - -// h = gv.Digraph(format="svg") -// names = [ -// "A", -// "B", -// "C", -// ] - -// # Specify edges -// h.edge("A", "B") -// h.edge("A", "C") - -// IPython.display.display_png(h)`; - -// const pythonPlotActions2 = `import matplotlib.pyplot as plt - -// # x axis values -// x = [1,2,3] -// # corresponding y axis values -// y = [2,4,1] - -// # plotting the points -// plt.plot(x, y) - -// # naming the x axis -// plt.xlabel('x - axis') -// # naming the y axis -// plt.ylabel('y - axis') - -// # giving a title to my graph -// plt.title('My first graph!') - -// # function to show the plot -// plt.show()`; - -// const bgplot = `import bqplot.pyplot as bplt -// import numpy as np - -// x = np.linspace(-10, 10, 100) -// y = np.sin(x) -// axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}} - -// fig = bplt.figure(title="Line Chart") -// line = bplt.plot( -// x=x, y=y, axes_options=axes_opts -// ) - -// bplt.show()`; - -// const ipydatagrid = `import pandas as pd -// from ipydatagrid import DataGrid -// data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"]) -// DataGrid(data) -// DataGrid(data, selection_mode="cell", editable=True)`; - -// const ipyleaflet = `from ipyleaflet import Map, Marker, display -// center = (52.204793, 360.121558) -// map = Map(center=center, zoom=12) - -// # Add a draggable marker to the map -// # Dragging the marker updates the marker.location value in Python -// marker = Marker(location=center, draggable=True) -// map.add_control(marker) - -// display(map)`; - -// const plotly = `import hvplot.pandas -// import pandas as pd -// hvplot.extension('plotly') -// pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`; +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { test, expect, tags } from '../_test.setup'; +const resembleCompareImages = require('resemblejs/compareImages'); +import { ComparisonOptions } from 'resemblejs'; +import * as fs from 'fs'; +import { fail } from 'assert'; +import { Application } from '../../infra'; +import { Locator, Page } from '@playwright/test'; + +test.use({ + suiteId: __filename +}); + +test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { + test.describe('Python Plots', () => { + + test.beforeEach(async function ({ sessions, hotKeys }) { + await sessions.start('python'); + await hotKeys.stackedLayout(); + }); + + test.afterEach(async function ({ app, hotKeys }) { + await hotKeys.fullSizeSecondarySidebar(); + await app.workbench.plots.clearPlots(); + await app.workbench.plots.waitForNoPlots(); + }); + + test.afterAll(async function ({ cleanup }) { + await cleanup.removeTestFiles(['Python-scatter.jpeg', 'Python-scatter-editor.jpeg']); + }); + + test('Python - Verify basic plot functionality - Dynamic Plot', { + tag: [tags.CRITICAL, tags.WEB, tags.WIN] + }, async function ({ app, logger, headless }, testInfo) { + // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/ + logger.log('Sending code to console'); + await app.workbench.console.executeCode('Python', pythonDynamicPlot); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.toasts.closeAll(); + + const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); + await compareImages({ + app, + buffer, + diffScreenshotName: 'pythonScatterplotDiff', + masterScreenshotName: `pythonScatterplot-${process.platform}`, + testInfo: testInfo + }); + + if (!headless) { + await app.workbench.plots.copyCurrentPlotToClipboard(); + + let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); + expect(clipboardImageBuffer).not.toBeNull(); + + await app.workbench.clipboard.clearClipboard(); + clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); + expect(clipboardImageBuffer).toBeNull(); + } + + await test.step('Verify plot can be opened in editor', async () => { + await app.workbench.plots.openPlotIn('editor'); + await app.workbench.plots.waitForPlotInEditor(); + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + await app.workbench.plots.clearPlots(); + await app.workbench.layouts.enterLayout('stacked'); + await app.workbench.plots.waitForNoPlots(); + }); + + test('Python - Verify basic plot functionality - Static Plot', { + tag: [tags.CRITICAL, tags.WEB, tags.WIN] + }, async function ({ app, logger }, testInfo) { + logger.log('Sending code to console'); + await app.workbench.console.executeCode('Python', pythonStaticPlot); + await app.workbench.plots.waitForCurrentStaticPlot(); + + await app.workbench.toasts.closeAll(); + + const buffer = await app.workbench.plots.getCurrentStaticPlotAsBuffer(); + await compareImages({ + app, + buffer, + diffScreenshotName: 'graphvizDiff', + masterScreenshotName: `graphviz-${process.platform}`, + testInfo + }); + + await test.step('Verify plot can be opened in editor', async () => { + await app.workbench.plots.openPlotIn('editor'); + await app.workbench.plots.waitForPlotInEditor(); + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + + }); + + test('Python - Verify the plots pane action bar - Plot actions', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + const plots = app.workbench.plots; + + // default plot pane state for action bar + await expect(plots.plotSizeButton).not.toBeVisible(); + await expect(plots.savePlotFromPlotsPaneButton).not.toBeVisible(); + await expect(plots.copyPlotButton).not.toBeVisible(); + await expect(plots.zoomPlotButton).not.toBeVisible(); + + // create plots separately so that the order is known + await app.workbench.console.executeCode('Python', pythonPlotActions1); + await plots.waitForCurrentStaticPlot(); + await app.workbench.console.executeCode('Python', pythonPlotActions2); + await plots.waitForCurrentPlot(); + + // expand the plot pane to show the action bar + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + await expect(plots.clearPlotsButton).not.toBeDisabled(); + await expect(plots.nextPlotButton).toBeDisabled(); + await expect(plots.previousPlotButton).not.toBeDisabled(); + await expect(plots.plotSizeButton).not.toBeDisabled(); + await expect(plots.savePlotFromPlotsPaneButton).not.toBeDisabled(); + await expect(plots.copyPlotButton).not.toBeDisabled(); + + // switch to fixed size plot + await plots.previousPlotButton.click(); + await plots.waitForCurrentStaticPlot(); + + // switching to fixed size plot changes action bar + await expect(plots.zoomPlotButton).toBeVisible(); + await expect(plots.plotSizeButton).not.toBeVisible(); + await expect(plots.clearPlotsButton).not.toBeDisabled(); + await expect(plots.nextPlotButton).not.toBeDisabled(); + await expect(plots.previousPlotButton).toBeDisabled(); + await expect(plots.zoomPlotButton).not.toBeDisabled(); + + // switch back to dynamic plot + await plots.nextPlotButton.click(); + await plots.waitForCurrentPlot(); + await expect(plots.zoomPlotButton).toBeVisible(); + await expect(plots.plotSizeButton).toBeVisible(); + await expect(plots.clearPlotsButton).not.toBeDisabled(); + await expect(plots.nextPlotButton).toBeDisabled(); + await expect(plots.previousPlotButton).not.toBeDisabled(); + await expect(plots.plotSizeButton).not.toBeDisabled(); + }); + + test('Python - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { + await verifyPlotInNewWindow(app, 'Python', pythonDynamicPlot); + }); + + test('Python - Verify saving a Python plot', { tag: [tags.WIN] }, async function ({ app }) { + await test.step('Sending code to console to create plot', async () => { + await app.workbench.console.executeCode('Python', pythonDynamicPlot); + await app.workbench.plots.waitForCurrentPlot(); + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + }); + + await test.step('Save plot', async () => { + await app.workbench.plots.savePlotFromPlotsPane({ name: 'Python-scatter', format: 'JPEG' }); + await app.workbench.layouts.enterLayout('stacked'); + await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter.jpeg']); + }); + + await test.step('Open plot in editor', async () => { + await app.workbench.plots.openPlotIn('editor'); + await app.workbench.plots.waitForPlotInEditor(); + }); + + await test.step('Save plot from editor', async () => { + await app.workbench.plots.savePlotFromEditor({ name: 'Python-scatter-editor', format: 'JPEG' }); + await app.workbench.explorer.verifyExplorerFilesExist(['Python-scatter-editor.jpeg']); + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + + }); + + test('Python - Verify bqplot Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, bgplot, '.svg-figure'); + }); + + test('Python - Verify ipydatagrid Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, ipydatagrid, 'canvas:nth-child(1)'); + }); + + test('Python - Verify ipyleaflet Python widget ', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, ipyleaflet, '.leaflet-container'); + }); + + test('Python - Verify hvplot can load with plotly extension', { + tag: [tags.WEB, tags.WIN], + annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], + }, async function ({ app }) { + // run line by line due to https://github.com/posit-dev/positron/issues/5991 + await runScriptAndValidatePlot(app, plotly, '.plotly', false, true); + }); + + test('Python - Verify hvplot with plotly extension works in block execution', { + tag: [tags.WEB, tags.WIN], + annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5991' }], + }, async function ({ app }) { + // Test that our fix allows hvplot to work when executed as a block + await runScriptAndValidatePlot(app, plotly, '.plotly', false, false); + }); + + test('Python - Verify ipytree Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, ipytree, '.jstree-container-ul'); + + // fullauxbar layout needed for some smaller windows + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + + // tree should be expanded by default + const treeNodes = app.workbench.plots.getWebviewPlotLocator('.jstree-container-ul .jstree-node'); + await expect(treeNodes).toHaveCount(9); + + // collapse the tree, only parent nodes should be visible + await treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon + await expect(treeNodes).toHaveCount(3); + }); + + test('Python - Verify ipywidget.Output Python widget', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await app.workbench.console.pasteCodeToConsole(ipywidgetOutput); + await app.workbench.console.sendEnterKey(); + await app.workbench.plots.waitForWebviewPlot('.widget-output', 'attached'); + + // Redirect a print statement to the Output widget. + await app.workbench.console.pasteCodeToConsole(`with output: + print('Hello, world!') +`); // Empty line needed for the statement to be considered complete. + await app.workbench.console.sendEnterKey(); + await app.workbench.plots.waitForWebviewPlot('.widget-output .jp-OutputArea-child'); + + // The printed statement should not be shown in the console. + await app.workbench.console.waitForConsoleContents('Hello World', { expectedCount: 0 }); + + }); + + test('Python - Verify bokeh Python widget', { + tag: [tags.WEB, tags.WIN] + }, async function ({ app }) { + await app.workbench.console.executeCode('Python', bokeh); + + // selector not factored out as it is unique to bokeh + const bokehCanvas = '.bk-Canvas'; + await app.workbench.plots.waitForWebviewPlot(bokehCanvas, 'visible', app.web); + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + + // selector not factored out as it is unique to bokeh + let canvasLocator: Locator; + if (!app.web) { + await app.workbench.plots.getWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); + canvasLocator = app.workbench.plots.getWebviewPlotLocator(bokehCanvas); + } else { + await app.workbench.plots.getDeepWebWebviewPlotLocator('.bk-tool-icon-box-zoom').click(); + canvasLocator = app.workbench.plots.getDeepWebWebviewPlotLocator(bokehCanvas); + } + const boundingBox = await canvasLocator.boundingBox(); + + // plot capture before zoom + const bufferBeforeZoom = await canvasLocator.screenshot(); + + if (boundingBox) { + await app.code.driver.clickAndDrag({ + from: { + x: boundingBox.x + boundingBox.width / 3, + y: boundingBox.y + boundingBox.height / 3 + }, + to: { + x: boundingBox.x + 2 * (boundingBox.width / 3), + y: boundingBox.y + 2 * (boundingBox.height / 3) + } + }); + } else { + fail('Bounding box not found'); + } + + // plot capture after zoom + const bufferAfterZoom = await canvasLocator.screenshot(); + + // two plot captures should be different + const data = await resembleCompareImages(bufferAfterZoom, bufferBeforeZoom, options); + expect(data.rawMisMatchPercentage).toBeGreaterThan(0.0); + }); + + test('Python - Verify Plot Zoom works (Fit vs. 200%)', { tag: [tags.WEB] }, + async function ({ app, openFile, python, page }, testInfo) { + await openFile(path.join('workspaces', 'python-plots', 'matplotlib-zoom-example.py')); + + await test.step('Run Python File in Console', async () => { + await app.workbench.editor.playButton.click(); + await app.workbench.plots.waitForCurrentPlot(); + }); + const imgLocator = page.getByRole('img', { name: /%run/ }); + + await app.workbench.plots.setThePlotZoom('Fit'); + await page.waitForTimeout(2000); + await dismissPlotZoomTooltip(page); + const bufferFit1 = await imgLocator.screenshot(); + await app.workbench.plots.setThePlotZoom('200%'); + + await page.waitForTimeout(2000); + await dismissPlotZoomTooltip(page); + const bufferZoom = await imgLocator.screenshot(); + // Compare: Fit vs 200% + const resultZoom = await resembleCompareImages(bufferFit1, bufferZoom, options); + await testInfo.attach('fit-vs-zoom', { + body: resultZoom.getBuffer(true), + contentType: 'image/png' + }); + expect(resultZoom.rawMisMatchPercentage).toBeGreaterThan(1.5); // should be large diff + + await app.workbench.plots.setThePlotZoom('Fit'); + await page.waitForTimeout(2000); + await dismissPlotZoomTooltip(page); + const bufferFit2 = await imgLocator.screenshot(); + // Compare: Fit vs Fit again + const resultBack = await resembleCompareImages(bufferFit1, bufferFit2, options); + await testInfo.attach('fit-vs-fit', { + body: resultBack.getBuffer(true), + contentType: 'image/png' + }); + expect(resultBack.rawMisMatchPercentage).toBeLessThan(0.75); // should be small diff + }); + + }); + + test.describe('R Plots', { + tag: [tags.ARK] + }, () => { + + test.beforeEach(async function ({ sessions, hotKeys }) { + await hotKeys.stackedLayout(); + await sessions.start('r'); + }); + + test.afterEach(async function ({ app, hotKeys }) { + await hotKeys.fullSizeSecondarySidebar(); + await app.workbench.plots.clearPlots(); + await app.workbench.plots.waitForNoPlots(); + }); + + test.afterAll(async function ({ cleanup }) { + await cleanup.removeTestFiles(['r-cars.svg', 'r-cars.jpeg', 'plot.png']); + }); + + test('R - Verify basic plot functionality', { + tag: [tags.CRITICAL, tags.WEB, tags.WIN] + }, async function ({ app, logger, headless }, testInfo) { + logger.log('Sending code to console'); + await app.workbench.console.executeCode('R', rBasicPlot); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.toasts.closeAll(); + + const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); + await compareImages({ + app, + buffer, + diffScreenshotName: 'autosDiff', + masterScreenshotName: `autos-${process.platform}`, + testInfo + }); + + if (!headless) { + await app.workbench.plots.copyCurrentPlotToClipboard(); + + let clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); + expect(clipboardImageBuffer).not.toBeNull(); + + await app.workbench.clipboard.clearClipboard(); + clipboardImageBuffer = await app.workbench.clipboard.getClipboardImage(); + expect(clipboardImageBuffer).toBeNull(); + } + + await test.step('Verify plot can be opened in editor', async () => { + await app.workbench.plots.openPlotIn('editor'); + await app.workbench.plots.waitForPlotInEditor(); + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + + await app.workbench.layouts.enterLayout('fullSizedAuxBar'); + await app.workbench.plots.clearPlots(); + await app.workbench.layouts.enterLayout('stacked'); + await app.workbench.plots.waitForNoPlots(); + }); + + test('R - Verify opening plot in new window', { tag: [tags.WEB, tags.WIN, tags.PLOTS] }, async function ({ app }) { + await verifyPlotInNewWindow(app, 'R', rBasicPlot); + }); + + test('R - Verify saving an R plot', { tag: [tags.WIN] }, async function ({ app }) { + await test.step('Sending code to console to create plot', async () => { + await app.workbench.console.executeCode('R', rSavePlot); + await app.workbench.plots.waitForCurrentPlot(); + }); + + await test.step('Save plot as PNG', async () => { + await app.workbench.plots.savePlotFromPlotsPane({ name: 'plot', format: 'PNG' }); + await app.workbench.explorer.verifyExplorerFilesExist(['plot.png']); + }); + + await test.step('Save plot as SVG', async () => { + await app.workbench.plots.savePlotFromPlotsPane({ name: 'R-cars', format: 'SVG' }); + await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.svg']); + }); + + await test.step('Open plot in editor', async () => { + await app.workbench.plots.openPlotIn('editor'); + await app.workbench.plots.waitForPlotInEditor(); + }); + + await test.step('Save plot from editor as JPEG', async () => { + await app.workbench.plots.savePlotFromEditor({ name: 'R-cars', format: 'JPEG' }); + await app.workbench.explorer.verifyExplorerFilesExist(['R-cars.jpeg']); + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + }); + + test('R - Verify rplot plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await app.workbench.console.pasteCodeToConsole(rplot); + await app.workbench.console.sendEnterKey(); + await app.workbench.plots.waitForCurrentPlot(); + }); + + test('R - Verify highcharter plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, highcharter, 'svg', app.web); + }); + + test('R - Verify leaflet plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, leaflet, '.leaflet', app.web); + }); + + test('R - Verify plotly plot', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await runScriptAndValidatePlot(app, rPlotly, '.plot-container', app.web); + }); + + test('R - Two simultaneous plots', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + await app.workbench.console.pasteCodeToConsole(rTwoPlots, true); + await app.workbench.plots.waitForCurrentPlot(); + await app.workbench.plots.expectPlotThumbnailsCountToBe(2); + }); + + test('R - Plot building', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + + await app.workbench.plots.enlargePlotArea(); + + await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 2))', true); + await app.workbench.console.pasteCodeToConsole('plot(1:5)', true); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.console.pasteCodeToConsole('plot(2:6)', true); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.console.pasteCodeToConsole('plot(3:7)', true); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.console.pasteCodeToConsole('plot(4:8)', true); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.console.pasteCodeToConsole('plot(5:9)', true); + await app.workbench.plots.waitForCurrentPlot(); + await app.workbench.plots.expectPlotThumbnailsCountToBe(2); + + await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); + await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); + await app.workbench.plots.waitForCurrentPlot(); + await app.workbench.plots.expectPlotThumbnailsCountToBe(3); + + await app.workbench.plots.restorePlotArea(); + }); + + test('R - Figure margins', { tag: [tags.WEB, tags.WIN] }, async function ({ app }) { + + await app.workbench.plots.enlargePlotArea(); + + await app.workbench.console.pasteCodeToConsole('par(mfrow = c(2, 1))', true); + await app.workbench.console.pasteCodeToConsole('plot(1:10)', true); + await app.workbench.console.pasteCodeToConsole('plot(2:20)', true); + await app.workbench.console.pasteCodeToConsole('par(mfrow = c(1, 1))', true); + await app.workbench.plots.waitForCurrentPlot(); + + await app.workbench.plots.restorePlotArea(); + }); + + test('R - plot and save in one block', { tag: [tags.WEB, tags.WIN] }, async function ({ app, runCommand }) { + + await app.workbench.console.clearButton.click(); + await app.workbench.console.restartButton.click(); + + await app.workbench.console.waitForConsoleContents('restarted', { expectedCount: 1 }); + + await app.workbench.console.pasteCodeToConsole(rPlotAndSave, true); + await app.workbench.plots.waitForCurrentPlot(); + + await runCommand('workbench.action.fullSizedAuxiliaryBar'); + + const vars = await app.workbench.variables.getFlatVariables(); + const filePath = vars.get('tempfile')?.value; + + expect(fs.existsSync(filePath?.replaceAll('"', '')!)).toBe(true); + + await app.workbench.layouts.enterLayout('stacked'); + }); + + }); +}); + +const options: ComparisonOptions = { + output: { + errorColor: { + red: 255, + green: 0, + blue: 255 + }, + errorType: 'movement', + transparency: 0.3, + largeImageThreshold: 1200, + useCrossOrigin: false + }, + scaleToSameSize: true, + ignore: 'antialiasing', +}; + +async function runScriptAndValidatePlot(app: Application, script: string, locator: string, RWeb = false, runLineByLine = false) { + await app.workbench.hotKeys.fullSizeSecondarySidebar(); + const lines: string[] = runLineByLine ? script.split('\n') : [script]; + + await expect(async () => { + for (const line of lines) { + await app.workbench.console.pasteCodeToConsole(line); + await app.workbench.console.sendEnterKey(); + } + await app.workbench.console.waitForConsoleExecution({ timeout: 15000 }); + await app.workbench.plots.waitForWebviewPlot(locator, 'visible', RWeb); + }, 'Send code to console and verify plot renders').toPass({ timeout: 60000 }); +} + +async function verifyPlotInNewWindow(app: Application, language: "Python" | "R", plotCode: string) { + const plots = app.workbench.plots; + await test.step(`Create a ${language} plot`, async () => { + await app.workbench.console.executeCode(language, plotCode); + await plots.waitForCurrentPlot(); + }); + await test.step('Open plot in new window', async () => { + await plots.openPlotIn('new window'); + await app.workbench.layouts.enterLayout('stacked'); + }); +} + +async function compareImages({ + app, + buffer, + diffScreenshotName, + masterScreenshotName, + testInfo +}: { + app: any; + buffer: Buffer; + diffScreenshotName: string; + masterScreenshotName: string; + testInfo: any; +}) { + await test.step('compare images', async () => { + if (process.env.GITHUB_ACTIONS && !app.web) { + const data = await resembleCompareImages(fs.readFileSync(path.join(__dirname, `${masterScreenshotName}.png`),), buffer, options); + + if (data.rawMisMatchPercentage > 2.0) { + if (data.getBuffer) { + await testInfo.attach(diffScreenshotName, { body: data.getBuffer(true), contentType: 'image/png' }); + } + + // Capture a new master image in CI + const newMaster = await app.workbench.plots.currentPlot.screenshot(); + await testInfo.attach(masterScreenshotName, { body: newMaster, contentType: 'image/png' }); + + // Fail the test with mismatch details + fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`); + } + } + }); +} + +const pythonDynamicPlot = `import pandas as pd +import matplotlib.pyplot as plt +data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'], + 'age': [20, 20, 21, 20, 21, 20], + 'math_marks': [100, 90, 91, 98, 92, 95], + 'physics_marks': [90, 100, 91, 92, 98, 95], + 'chem_marks': [93, 89, 99, 92, 94, 92] + } + +df = pd.DataFrame(data_dict) + +df.plot(kind='scatter', + x='math_marks', + y='physics_marks', + color='red') + +plt.title('ScatterPlot') +plt.show()`; + + +const pythonStaticPlot = `import graphviz as gv +import IPython + +h = gv.Digraph(format="svg") +names = [ + "A", + "B", + "C", +] + +# Specify edges +h.edge("A", "B") +h.edge("A", "C") + +IPython.display.display_png(h)`; + +const pythonPlotActions1 = `import graphviz as gv +import IPython + +h = gv.Digraph(format="svg") +names = [ + "A", + "B", + "C", +] + +# Specify edges +h.edge("A", "B") +h.edge("A", "C") + +IPython.display.display_png(h)`; + +const pythonPlotActions2 = `import matplotlib.pyplot as plt + +# x axis values +x = [1,2,3] +# corresponding y axis values +y = [2,4,1] + +# plotting the points +plt.plot(x, y) + +# naming the x axis +plt.xlabel('x - axis') +# naming the y axis +plt.ylabel('y - axis') + +# giving a title to my graph +plt.title('My first graph!') + +# function to show the plot +plt.show()`; + +const bgplot = `import bqplot.pyplot as bplt +import numpy as np + +x = np.linspace(-10, 10, 100) +y = np.sin(x) +axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}} + +fig = bplt.figure(title="Line Chart") +line = bplt.plot( + x=x, y=y, axes_options=axes_opts +) + +bplt.show()`; + +const ipydatagrid = `import pandas as pd +from ipydatagrid import DataGrid +data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"]) +DataGrid(data) +DataGrid(data, selection_mode="cell", editable=True)`; + +const ipyleaflet = `from ipyleaflet import Map, Marker, display +center = (52.204793, 360.121558) +map = Map(center=center, zoom=12) + +# Add a draggable marker to the map +# Dragging the marker updates the marker.location value in Python +marker = Marker(location=center, draggable=True) +map.add_control(marker) + +display(map)`; + +const plotly = `import hvplot.pandas +import pandas as pd +hvplot.extension('plotly') +pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`; -// const ipytree = `from ipytree import Tree, Node -// tree = Tree(stripes=True) -// tree -// tree -// node1 = Node('node1') -// tree.add_node(node1) -// node2 = Node('node2') -// tree.add_node(node2) -// tree.nodes = [node2, node1] -// node3 = Node('node3', disabled=True) -// node4 = Node('node4') -// node5 = Node('node5', [Node('1'), Node('2')]) -// node2.add_node(node3) -// node2.add_node(node4) -// node2.add_node(node5) -// tree.add_node(Node('node6'), 1) -// node2.add_node(Node('node7'), 2) - -// tree`; - -// const ipywidgetOutput = `import ipywidgets -// output = ipywidgets.Output() -// output`; - -// const bokeh = `from bokeh.plotting import figure, output_file, show, reset_output -// # Proactively reset output in case hvplot has changed anything -// reset_output() - -// # instantiating the figure object -// graph = figure(title = "Bokeh Line Graph") - -// # the points to be plotted -// x = [1, 2, 3, 4, 5] -// y = [5, 4, 3, 2, 1] - -// # plotting the line graph -// graph.line(x, y) - -// # displaying the model -// show(graph)`; +const ipytree = `from ipytree import Tree, Node +tree = Tree(stripes=True) +tree +tree +node1 = Node('node1') +tree.add_node(node1) +node2 = Node('node2') +tree.add_node(node2) +tree.nodes = [node2, node1] +node3 = Node('node3', disabled=True) +node4 = Node('node4') +node5 = Node('node5', [Node('1'), Node('2')]) +node2.add_node(node3) +node2.add_node(node4) +node2.add_node(node5) +tree.add_node(Node('node6'), 1) +node2.add_node(Node('node7'), 2) + +tree`; + +const ipywidgetOutput = `import ipywidgets +output = ipywidgets.Output() +output`; + +const bokeh = `from bokeh.plotting import figure, output_file, show, reset_output +# Proactively reset output in case hvplot has changed anything +reset_output() + +# instantiating the figure object +graph = figure(title = "Bokeh Line Graph") + +# the points to be plotted +x = [1, 2, 3, 4, 5] +y = [5, 4, 3, 2, 1] + +# plotting the line graph +graph.line(x, y) + +# displaying the model +show(graph)`; -// const rBasicPlot = `cars <- c(1, 3, 6, 4, 9) -// plot(cars, type="o", col="blue") -// title(main="Autos", col.main="red", font.main=4)`; - -// const rSavePlot = `cars <- c(1, 3, 6, 4, 9) -// plot(cars, type="o", col="blue") -// title(main="Autos", col.main="red", font.main=4)`; - -// const rplot = `library('corrr') - -// x <- correlate(mtcars) -// rplot(x) - -// # Common use is following rearrange and shave -// x <- rearrange(x, absolute = FALSE) -// x <- shave(x) -// rplot(x) -// rplot(x, print_cor = TRUE) -// rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`; - -// const highcharter = `library(highcharter) - -// data("mpg", "diamonds", "economics_long", package = "ggplot2") - -// hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`; - -// const leaflet = `library(leaflet) -// m = leaflet() %>% addTiles() -// m = m %>% setView(-93.65, 42.0285, zoom = 17) -// m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`; - -// const rPlotly = `library(plotly) -// fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box") -// fig`; - -// const rTwoPlots = `plot(1:10) -// plot(1:100)`; - -// const rPlotAndSave = `plot(1:10) -// tempfile <- tempfile() -// grDevices::png(filename = tempfile) -// plot(1:20) -// dev.off()`; - -// async function dismissPlotZoomTooltip(page: Page) { -// const plotZoomTooltip = page.getByText('Set the plot zoom'); -// if (await plotZoomTooltip.isVisible()) { -// page.keyboard.press('Escape'); -// } -// } +const rBasicPlot = `cars <- c(1, 3, 6, 4, 9) +plot(cars, type="o", col="blue") +title(main="Autos", col.main="red", font.main=4)`; + +const rSavePlot = `cars <- c(1, 3, 6, 4, 9) +plot(cars, type="o", col="blue") +title(main="Autos", col.main="red", font.main=4)`; + +const rplot = `library('corrr') + +x <- correlate(mtcars) +rplot(x) + +# Common use is following rearrange and shave +x <- rearrange(x, absolute = FALSE) +x <- shave(x) +rplot(x) +rplot(x, print_cor = TRUE) +rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`; + +const highcharter = `library(highcharter) + +data("mpg", "diamonds", "economics_long", package = "ggplot2") + +hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`; + +const leaflet = `library(leaflet) +m = leaflet() %>% addTiles() +m = m %>% setView(-93.65, 42.0285, zoom = 17) +m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`; + +const rPlotly = `library(plotly) +fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box") +fig`; + +const rTwoPlots = `plot(1:10) +plot(1:100)`; + +const rPlotAndSave = `plot(1:10) +tempfile <- tempfile() +grDevices::png(filename = tempfile) +plot(1:20) +dev.off()`; + +async function dismissPlotZoomTooltip(page: Page) { + const plotZoomTooltip = page.getByText('Set the plot zoom'); + if (await plotZoomTooltip.isVisible()) { + page.keyboard.press('Escape'); + } +} From 7ea5b147903bd3e00cc2aa0af58bdb92dc393fb1 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 5 Sep 2025 15:50:42 -0500 Subject: [PATCH 4/8] small fixes for scrolling --- test/e2e/pages/dataExplorer.ts | 61 ++++++++++++------- .../data-explorer/data-explorer-pins.test.ts | 11 ++-- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 0a0b0e45e00c..84588dbed4e7 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -229,10 +229,14 @@ export class DataGrid { await this.code.driver.page.locator(DATA_GRID_TOP_LEFT).click(); } - async sortColumnBy(columnIndex: number, sortBy: string) { + /** + * + * @param columnIndex (Index is 1-based) + * @param sortBy + */ + async sortColumnBy(columnIndex: number, sortBy: 'Sort Ascending' | 'Sort Descending' | 'Clear Sorting') { await test.step(`Sort column ${columnIndex} by: ${sortBy}`, async () => { - await this.code.driver.page.locator(`.data-grid-column-header:nth-child(${columnIndex}) .sort-button`).click(); - await this.code.driver.page.locator(`.positron-modal-overlay div.title:has-text('${sortBy}')`).click(); + await this.selectColumnAction(columnIndex, sortBy); }); } @@ -247,7 +251,8 @@ export class DataGrid { } /** - * Click a cell by its index (Index is 0-based, these never change even with sorting or filtering) + * Click a cell by its index (Index is 0-based) + * These indexes never change even with sorting or filtering * If a column/row is pinned, this method finds the cell by its original row/col index values */ async clickCellByIndex(rowIndex: number, columnIndex: number, withShift = false) { @@ -262,24 +267,32 @@ export class DataGrid { } // index based - async selectColumnAction(colIndex: number, action: 'Copy' | 'Select Column' | 'Pin Column' | 'Unpin Column' | 'Sort Ascending' | 'Sort Descending' | 'Clear Sorting' | 'Add Filter') { + private async selectColumnAction(colIndex: number, action: ColumnRightMenuOption) { await test.step(`Select column action: ${action}`, async () => { await this.code.driver.page.locator(`div:nth-child(${colIndex}) > .content > .positron-button`).click(); await this.code.driver.page.getByRole('button', { name: action }).click(); }); } + /** + * Pin a column by its index + * @param colIndex (Index is 0-based) + */ async pinColumn(colIndex: number) { await test.step(`Pin column at index ${colIndex}`, async () => { - // colIndex is 0-based, selectColumnAction is 1-based - await this.selectColumnAction(colIndex + 1, 'Pin Column') + await this.jumpToStart(); // make sure we are at the start so our index is accurate + await this.selectColumnAction(colIndex + 1, 'Pin Column'); // selectColumnAction is 1-based }); } + /** + * Unpin a column by its index + * @param colIndex (Index is 0-based) + */ async unpinColumn(colIndex = 0) { await test.step(`Unpin column at index ${colIndex}`, async () => { - // colIndex is 0-based, selectColumnAction is 1-based - await this.selectColumnAction(colIndex + 1, 'Unpin Column') + await this.jumpToStart(); // make sure we are at the start so our index is accurate + await this.selectColumnAction(colIndex + 1, 'Unpin Column'); // selectColumnAction is 1-based }); } @@ -302,20 +315,27 @@ export class DataGrid { }); } - async selectRange({ start, end }: { start: CellPosition, end: CellPosition }) { + async selectRange({ start, end }: { start: CellPosition; end: CellPosition }) { await test.step(`Select range: [${start.row}, ${start.col}] - [${end.row}, ${end.col}]`, async () => { await this.clickCell(start.row, start.col); await this.shiftClickCell(end.row, end.col); }); } + /** + * Click a column header by its title + * @param columnTitle The exact title of the column to click + */ async clickColumnHeader(columnTitle: string) { await test.step(`Click column header: ${columnTitle}`, async () => { await this.columnHeaders.getByText(columnTitle).click(); }); } - /** 1-based just as it is in the UI */ + /** + * Click a row header by its visual position + * Index is 1-based to match UI + **/ async clickRowHeader(rowIndex: number) { await test.step(`Click row header: ${rowIndex}`, async () => { await this.rowHeaders.getByText(rowIndex.toString(), { exact: true }).click(); @@ -460,7 +480,7 @@ export class DataGrid { }); } - async expectCellContentToBe({ rowIndex, colIndex, value }: { rowIndex: number, colIndex: number, value: string | number }): Promise { + async expectCellContentToBe({ rowIndex, colIndex, value }: { rowIndex: number; colIndex: number; value: string | number }): Promise { await test.step(`Verify cell content at (${rowIndex}, ${colIndex}): ${value}`, async () => { await expect(async () => { const cell = this.grid.locator(`#data-grid-row-cell-content-${colIndex}-${rowIndex}`); @@ -469,7 +489,7 @@ export class DataGrid { }); } - async expectRangeToBeSelected(expectedRange: { rows: number[], cols: number[] }): Promise { + async expectRangeToBeSelected(expectedRange: { rows: number[]; cols: number[] }): Promise { await test.step(`Verify selection range: ${JSON.stringify(expectedRange)}`, async () => { const selectedCells = this.grid.locator('.selection-overlay'); await expect(selectedCells).toHaveCount((expectedRange.rows.length) * (expectedRange.cols.length)); @@ -550,17 +570,12 @@ export class DataGrid { }); } - async expectRowCountToBe(expectedCount: number) { - await test.step('Verify row count', async () => { - // const actualCount = await this.getRowHeaders(); - // expect(actualCount.length).toBe(expectedCount); - }); - } - async expectRowOrderToBe(expectedOrder: number[]) { await test.step(`Verify row order: ${expectedOrder}`, async () => { - // const actualOrder = await this.getRowHeaders(); - // expect(actualOrder).toEqual(expectedOrder); + const rowHeaders = this.code.driver.page.locator('.data-grid-row-headers > .data-grid-row-header .content'); + const actualOrder = await rowHeaders.allInnerTexts(); + const actualOrderNumbers = actualOrder.map(text => parseInt(text, 10)); + expect(actualOrderNumbers).toEqual(expectedOrder); }); } @@ -913,3 +928,5 @@ export interface ColumnProfile { } export type CellPosition = { row: number; col: number }; + +export type ColumnRightMenuOption = 'Copy' | 'Select Column' | 'Pin Column' | 'Unpin Column' | 'Sort Ascending' | 'Sort Descending' | 'Clear Sorting' | 'Add Filter'; diff --git a/test/e2e/tests/data-explorer/data-explorer-pins.test.ts b/test/e2e/tests/data-explorer/data-explorer-pins.test.ts index 4f795e78c856..ada2e52d21f5 100644 --- a/test/e2e/tests/data-explorer/data-explorer-pins.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-pins.test.ts @@ -18,16 +18,17 @@ import { test, tags } from '../_test.setup'; const columnOrder = { default: ['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column6', 'column7', 'column8', 'column9'], - pinCol6: ['column6', 'column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8', 'column9'], pinCol2: ['column2', 'column0', 'column1', 'column3', 'column4', 'column5', 'column6', 'column7', 'column8', 'column9'], pinCol4: ['column4', 'column0', 'column1', 'column2', 'column3', 'column5', 'column6', 'column7', 'column8', 'column9'], + pinCol6: ['column6', 'column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8', 'column9'], pinCol4And6: ['column4', 'column6', 'column0', 'column1', 'column2', 'column3', 'column5', 'column7', 'column8', 'column9'], }; const rowOrder = { default: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], pinRow5: [5, 0, 1, 2, 3, 4, 6, 7, 8, 9], + pinRow6: [6, 0, 1, 2, 3, 4, 5, 7, 8, 9], pinRow8: [8, 0, 1, 2, 3, 4, 5, 6, 7, 9], - pinRow6And8: [6, 8, 0, 1, 2, 3, 4, 5, 7, 9] + pinRow8And6: [8, 6, 0, 1, 2, 3, 4, 5, 7, 9] }; test.use({ @@ -48,7 +49,7 @@ test.describe('Data Explorer: Pins', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLO await hotKeys.closeAllEditors(); }); - test('Row/column pinning persists across scroll and can be added/removed', async function ({ app }) { + test('Rows and columns can be pinned, unpinned and persist with scrolling', async function ({ app }) { const { dataExplorer } = app.workbench; // Initial state @@ -73,7 +74,7 @@ test.describe('Data Explorer: Pins', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLO // Pin row 6 await dataExplorer.grid.pinRow(7); // after pinning row 8, row 6 is now at index 7 await dataExplorer.grid.expectRowsToBePinned([8, 6]); - await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow6And8); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow8And6); // Ensure pins persist with scrolling await dataExplorer.grid.clickLowerRightCorner(); @@ -92,7 +93,7 @@ test.describe('Data Explorer: Pins', { tag: [tags.WIN, tags.WEB, tags.DATA_EXPLO // Unpin rows await dataExplorer.grid.unpinRow(0); await dataExplorer.grid.expectRowsToBePinned([6]); - await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow8); + await dataExplorer.grid.expectRowOrderToBe(rowOrder.pinRow6); await dataExplorer.grid.unpinRow(0); await dataExplorer.grid.expectRowsToBePinned([]); await dataExplorer.grid.expectRowOrderToBe(rowOrder.default); From b1b21406475be94d9261ac0175b3ad139dc1feea Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 5 Sep 2025 17:31:22 -0500 Subject: [PATCH 5/8] try keyboard down --- test/e2e/pages/dataExplorer.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 84588dbed4e7..efb95494d104 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -246,7 +246,20 @@ export class DataGrid { */ async clickCell(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by 0-based position: row ${rowIndex}, column ${columnIndex}`, async () => { - await this.cell(rowIndex, columnIndex).click({ modifiers: withShift ? ['Shift'] : [] }); + const target = this.cell(rowIndex, columnIndex); + await target.scrollIntoViewIfNeeded(); + + if (withShift) { + // Use keyboard down/up so Shift state is recognized consistently on macOS/Windows/Linux + await this.code.driver.page.keyboard.down('Shift'); + try { + await target.click(); + } finally { + await this.code.driver.page.keyboard.up('Shift'); + } + } else { + await target.click(); + } }); } @@ -258,12 +271,22 @@ export class DataGrid { async clickCellByIndex(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by index: row ${rowIndex}, column ${columnIndex}`, async () => { const cell = this.grid.locator(`#data-grid-row-cell-content-${columnIndex}-${rowIndex}`); - await cell.click({ modifiers: withShift ? ['Shift'] : [] }); + await cell.scrollIntoViewIfNeeded(); + if (withShift) { + await this.code.driver.page.keyboard.down('Shift'); + try { + await cell.click(); + } finally { + await this.code.driver.page.keyboard.up('Shift'); + } + } else { + await cell.click(); + } }); } async shiftClickCell(rowIndex: number, columnIndex: number) { - this.clickCell(rowIndex, columnIndex, true); + await this.clickCell(rowIndex, columnIndex, true); } // index based From 91c9d8ed9a1cf97cf413692c2e46a90562b6a785 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Sat, 6 Sep 2025 06:44:36 -0500 Subject: [PATCH 6/8] jump to start before range selection --- test/e2e/pages/dataExplorer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index efb95494d104..6ce413fddad6 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -340,6 +340,7 @@ export class DataGrid { async selectRange({ start, end }: { start: CellPosition; end: CellPosition }) { await test.step(`Select range: [${start.row}, ${start.col}] - [${end.row}, ${end.col}]`, async () => { + await this.jumpToStart(); await this.clickCell(start.row, start.col); await this.shiftClickCell(end.row, end.col); }); From 58bdfb260fc60c53202f8a115f52a6adc85af2ff Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 8 Sep 2025 07:16:44 -0500 Subject: [PATCH 7/8] cleanup --- test/e2e/pages/dataExplorer.ts | 56 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 6ce413fddad6..86b34384c396 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -230,7 +230,7 @@ export class DataGrid { } /** - * + * Sort the specified column by the given sort option. * @param columnIndex (Index is 1-based) * @param sortBy */ @@ -242,31 +242,19 @@ export class DataGrid { /** * Click a cell by its visual position (Index is 0-based) - * If a column/row is pinned, this method finds the cell by its visual position. + * For example, if a column/row is pinned, the position would be index 0. */ async clickCell(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by 0-based position: row ${rowIndex}, column ${columnIndex}`, async () => { - const target = this.cell(rowIndex, columnIndex); - await target.scrollIntoViewIfNeeded(); - - if (withShift) { - // Use keyboard down/up so Shift state is recognized consistently on macOS/Windows/Linux - await this.code.driver.page.keyboard.down('Shift'); - try { - await target.click(); - } finally { - await this.code.driver.page.keyboard.up('Shift'); - } - } else { - await target.click(); - } + withShift + ? await this.cell(rowIndex, columnIndex).click({ modifiers: ['Shift'] }) + : await this.cell(rowIndex, columnIndex).click(); }); } /** * Click a cell by its index (Index is 0-based) - * These indexes never change even with sorting or filtering - * If a column/row is pinned, this method finds the cell by its original row/col index values + * These indexes never change even with sorting, filtering, or pinning. */ async clickCellByIndex(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by index: row ${rowIndex}, column ${columnIndex}`, async () => { @@ -285,11 +273,21 @@ export class DataGrid { }); } + /** + * Shift-click a cell by its visual position (Index is 0-based) + * For example, if a column/row is pinned, the position would be index 0. + * @param rowIndex + * @param columnIndex + */ async shiftClickCell(rowIndex: number, columnIndex: number) { await this.clickCell(rowIndex, columnIndex, true); } - // index based + /** + * Select a column action from the right-click menu. + * @param colIndex (Index is 1-based) + * @param action menu action to select + */ private async selectColumnAction(colIndex: number, action: ColumnRightMenuOption) { await test.step(`Select column action: ${action}`, async () => { await this.code.driver.page.locator(`div:nth-child(${colIndex}) > .content > .positron-button`).click(); @@ -298,7 +296,7 @@ export class DataGrid { } /** - * Pin a column by its index + * Pin a column by its visual index * @param colIndex (Index is 0-based) */ async pinColumn(colIndex: number) { @@ -309,7 +307,7 @@ export class DataGrid { } /** - * Unpin a column by its index + * Unpin a column by its visual index * @param colIndex (Index is 0-based) */ async unpinColumn(colIndex = 0) { @@ -319,6 +317,10 @@ export class DataGrid { }); } + /** + * Pin a row by its visual index + * @param rowIndex (Index is 0-based) + */ async pinRow(rowIndex: number) { await test.step(`Pin row at index ${rowIndex}`, async () => { await this.code.driver.page @@ -329,6 +331,10 @@ export class DataGrid { }); } + /** + * Unpin a row by its visual index + * @param rowIndex (Index is 0-based) + */ async unpinRow(rowIndex = 0) { await test.step(`Unpin row at index ${rowIndex}`, async () => { await this.code.driver.page @@ -338,6 +344,11 @@ export class DataGrid { }); } + /** + * Select a range of cells + * @param start The starting cell position + * @param end The ending cell position + */ async selectRange({ start, end }: { start: CellPosition; end: CellPosition }) { await test.step(`Select range: [${start.row}, ${start.col}] - [${end.row}, ${end.col}]`, async () => { await this.jumpToStart(); @@ -551,14 +562,11 @@ export class DataGrid { */ async expectColumnsToBePinned(expectedTitles: string[]) { await test.step(`Verify pinned columns: ${expectedTitles}`, async () => { - // Locate all pinned column headers const pinnedColumns = this.code.driver.page.locator('.data-grid-column-header.pinned'); if (expectedTitles.length === 0) { - // If we expect no pinned columns, verify count is 0 await expect(pinnedColumns).toHaveCount(0); } else { - // Assert the count matches the expected length await expect(pinnedColumns).toHaveCount(expectedTitles.length); // Assert each pinned column has the correct title, in order From 52525ef4cf202f6922a2c2591df93465b4829cb1 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 8 Sep 2025 08:43:26 -0500 Subject: [PATCH 8/8] dry up cell --- test/e2e/pages/dataExplorer.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 86b34384c396..c8c0f35f9341 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -202,9 +202,10 @@ export class DataGrid { private rowHeaders = this.code.driver.page.locator('.data-grid-row-headers'); private columnHeaders = this.code.driver.page.locator(HEADER_TITLES); private rows = this.code.driver.page.locator(`${DATA_GRID_ROWS} ${DATA_GRID_ROW}`); - private cell = (rowIndex: number, columnIndex: number) => this.code.driver.page.locator( + private cellByPosition = (rowIndex: number, columnIndex: number) => this.code.driver.page.locator( `${DATA_GRID_ROWS} ${DATA_GRID_ROW}:nth-child(${rowIndex + 1}) > div:nth-child(${columnIndex + 1})` ); + private cellByIndex = (rowIndex: number, columnIndex: number) => this.grid.locator(`#data-grid-row-cell-content-${columnIndex}-${rowIndex}`); constructor(private code: Code, private dataExplorer: DataExplorer) { this.grid = this.code.driver.page.locator('.data-explorer .right-column'); @@ -247,8 +248,8 @@ export class DataGrid { async clickCell(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by 0-based position: row ${rowIndex}, column ${columnIndex}`, async () => { withShift - ? await this.cell(rowIndex, columnIndex).click({ modifiers: ['Shift'] }) - : await this.cell(rowIndex, columnIndex).click(); + ? await this.cellByPosition(rowIndex, columnIndex).click({ modifiers: ['Shift'] }) + : await this.cellByPosition(rowIndex, columnIndex).click(); }); } @@ -258,18 +259,9 @@ export class DataGrid { */ async clickCellByIndex(rowIndex: number, columnIndex: number, withShift = false) { await test.step(`Click cell by index: row ${rowIndex}, column ${columnIndex}`, async () => { - const cell = this.grid.locator(`#data-grid-row-cell-content-${columnIndex}-${rowIndex}`); - await cell.scrollIntoViewIfNeeded(); - if (withShift) { - await this.code.driver.page.keyboard.down('Shift'); - try { - await cell.click(); - } finally { - await this.code.driver.page.keyboard.up('Shift'); - } - } else { - await cell.click(); - } + withShift + ? await this.cellByIndex(rowIndex, columnIndex).click({ modifiers: ['Shift'] }) + : await this.cellByIndex(rowIndex, columnIndex).click(); }); } @@ -613,7 +605,7 @@ export class DataGrid { async expectCellToBeSelected(row: number, col: number) { await test.step(`Verify cell at (${row}, ${col}) is selected`, async () => { - await expect(this.cell(row, col).locator('.border-overlay .cursor-border')).toBeVisible(); + await expect(this.cellByPosition(row, col).locator('.border-overlay .cursor-border')).toBeVisible(); }); }