From 4d1cb10856b03775bb205f7a5d57bad1db01cd03 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 09:02:25 -0600 Subject: [PATCH 01/21] add test --- test/e2e/infra/test-runner/test-tags.ts | 2 +- test/e2e/tests/_test.setup.ts | 17 +++ .../data-explorer-action-bar.test.ts | 136 ++++++++++++++++++ .../action-bar/editor-action-bar.test.ts | 38 ++--- 4 files changed, 164 insertions(+), 29 deletions(-) create mode 100644 test/e2e/tests/action-bar/data-explorer-action-bar.test.ts diff --git a/test/e2e/infra/test-runner/test-tags.ts b/test/e2e/infra/test-runner/test-tags.ts index 26a4fe91d36..9fbad4b1eaf 100644 --- a/test/e2e/infra/test-runner/test-tags.ts +++ b/test/e2e/infra/test-runner/test-tags.ts @@ -18,7 +18,7 @@ export enum TestTags { // feature tags - EDITOR_ACTION_BAR = '@:editor-action-bar', + ACTION_BAR = '@:action-bar', APPS = '@:apps', CONNECTIONS = '@:connections', CONSOLE = '@:console', diff --git a/test/e2e/tests/_test.setup.ts b/test/e2e/tests/_test.setup.ts index c792ff1876d..a9066af92d0 100644 --- a/test/e2e/tests/_test.setup.ts +++ b/test/e2e/tests/_test.setup.ts @@ -149,6 +149,20 @@ export const test = base.extend({ { scope: 'test' }], + // example usage: await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); + openFile: async ({ app }, use) => { + await use(async (filePath: string) => { + await app.workbench.quickaccess.openFile(path.join(app.workspacePathOrFolder, filePath)); + }); + }, + + // example usage: await openDataFile(app, 'workspaces/large_r_notebook/spotify.ipynb'); + openDataFile: async ({ app }, use) => { + await use(async (filePath: string) => { + await app.workbench.quickaccess.openDataFile(path.join(app.workspacePathOrFolder, filePath)); + }); + }, + userSettings: [async ({ app }, use) => { const userSettings = new UserSettingsFixtures(app); @@ -340,6 +354,9 @@ interface TestFixtures { packages: PackageManager; autoTestFixture: any; devTools: void; + openFile: (filePath: string) => Promise; + openDataFile: (filePath: string) => Promise; + // runCommand: (command: string) => Promise; } interface WorkerFixtures { diff --git a/test/e2e/tests/action-bar/data-explorer-action-bar.test.ts b/test/e2e/tests/action-bar/data-explorer-action-bar.test.ts new file mode 100644 index 00000000000..322642037e0 --- /dev/null +++ b/test/e2e/tests/action-bar/data-explorer-action-bar.test.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { test, expect, tags } from '../_test.setup'; +import { Application } from '../../infra'; +import { Page } from '@playwright/test'; + +test.use({ + suiteId: __filename +}); + +test.describe('Action Bar: Data Explorer', { + tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.EDITOR] +}, () => { + + test.beforeAll(async function ({ userSettings }) { + await userSettings.set([['editor.actionBar.enabled', 'true']], false); + }); + + test.afterEach(async function ({ app }) { + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + }); + + test('Python Pandas [...]', { + tag: [tags.R_MARKDOWN] + }, async function ({ app, page, openFile, python }) { + + // load data in data explorer + await openFile('workspaces/polars-dataframe-py/polars_basic.py'); + await app.workbench.quickaccess.runCommand('python.execInConsole'); + await app.workbench.variables.doubleClickVariableRow('df'); + await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); + await expect(page.getByText('Data: df', { exact: true })).toBeVisible(); + + await verifySummaryPosition(app, 'Left'); + await verifySummaryPosition(app, 'Right'); + await verifySplitEditor(page, 'Data: df'); + // await verifyOpenInNewWindow(page, 'Data: df — qa-example-content'); + }); + + test('R [...]', { + tag: [tags.R_MARKDOWN] + }, async function ({ app, page, openFile, r }) { + + // load data in data explorer + await app.workbench.console.executeCode('R', rScript, '>'); + await app.workbench.variables.doubleClickVariableRow('Data_Frame'); + await expect(app.code.driver.page.getByText('Data: Data_Frame', { exact: true })).toBeVisible(); + + await verifySummaryPosition(app, 'Left'); + await verifySummaryPosition(app, 'Right'); + await verifySplitEditor(page, 'Data: df'); + }); +}); + + +async function verifySplitEditor(page: Page, tabName: string) { + await test.step(`verify "split editor" opens another tab`, async () => { + // Split editor right + await page.getByLabel('Split Editor Right', { exact: true }).click(); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + // Close one tab + await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); + + // Split editor down + await page.keyboard.down('Alt'); + await page.getByLabel('Split Editor Down').click(); + await page.keyboard.up('Alt'); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + }); +} + +async function verifyOpenInNewWindow(page, expectedText: string) { + await test.step(`verify "open new window" contains: ${expectedText}`, async () => { + const [newPage] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByLabel('Move into new window').first().click(), + ]); + await newPage.waitForLoadState(); + await expect(newPage.getByText(expectedText)).toBeVisible(); + }); +} + +async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { + const page = app.code.driver.page; + + await test.step(`verify summary position: ${position}`, async () => { + // Toggle the summary position + // * Web: Via the action bar + // * Desktop: Via the command palette + if (app.web) { + await page.getByLabel('More actions', { exact: true }).click(); + await page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); + await page.keyboard.press('Enter'); + } + else { + await app.workbench.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); + } + + // Locator for the summary element + const summaryLocator = page.locator('div.column-summary').first(); + const tableLocator = page.locator('div.data-grid-column-headers'); + + // Ensure both the summary and table elements are visible + await Promise.all([ + expect(summaryLocator).toBeVisible(), + expect(tableLocator).toBeVisible(), + ]); + + // Get the bounding boxes for both elements + const summaryBox = await summaryLocator.boundingBox(); + const tableBox = await tableLocator.boundingBox(); + + // Validate bounding boxes are available + if (!summaryBox || !tableBox) { + throw new Error('Bounding boxes could not be retrieved for summary or table.'); + } + + // Validate positions based on the expected position + position === 'Left' + ? expect(summaryBox.x).toBeLessThan(tableBox.x) + : expect(summaryBox.x).toBeGreaterThan(tableBox.x); + }); +} + +// snippet from https://www.w3schools.com/r/r_data_frames.asp +const rScript = `Data_Frame <- data.frame ( + Training = c("Strength", "Stamina", "Other"), + Pulse = c(100, NA, 120), + Duration = c(60, 30, 45), + Note = c(NA, NA, "Note") +)`; diff --git a/test/e2e/tests/action-bar/editor-action-bar.test.ts b/test/e2e/tests/action-bar/editor-action-bar.test.ts index 943a61164ae..634fed5e822 100644 --- a/test/e2e/tests/action-bar/editor-action-bar.test.ts +++ b/test/e2e/tests/action-bar/editor-action-bar.test.ts @@ -6,15 +6,13 @@ import { test, expect, tags } from '../_test.setup'; import { Application } from '../../infra'; import { Page } from '@playwright/test'; -import path = require('path'); - test.use({ suiteId: __filename }); -test.describe('Editor Action Bar', { - tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.EDITOR] +test.describe('Action Bar: Editor', { + tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.EDITOR] }, () => { test.beforeAll(async function ({ userSettings }) { @@ -27,8 +25,8 @@ test.describe('Editor Action Bar', { test('R Markdown Document [C1080703]', { tag: [tags.R_MARKDOWN] - }, async function ({ app, page }) { - await openFile(app, 'workspaces/basic-rmd-file/basicRmd.rmd'); + }, async function ({ app, page, openFile }) { + await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); await verifyPreviewRendersHtml(app, 'Getting startedAnchor'); await verifySplitEditor(page, 'basicRmd.rmd'); await verifyOpenInNewWindow(page, 'This post examines the features'); @@ -36,16 +34,16 @@ test.describe('Editor Action Bar', { test('Quarto Document [C1080700]', { tag: [tags.QUARTO] - }, async function ({ app, page }) { - await openFile(app, 'workspaces/quarto_basic/quarto_basic.qmd'); + }, async function ({ app, page, openFile }) { + openFile('workspaces/quarto_basic/quarto_basic.qmd'); await verifyPreviewRendersHtml(app, 'Diamond sizes'); await verifyOpenChanges(page); await verifySplitEditor(page, 'quarto_basic.qmd'); await verifyOpenInNewWindow(page, 'Diamond sizes'); }); - test('HTML Document [C1080701]', { tag: [tags.HTML] }, async function ({ app, page }) { - await openFile(app, 'workspaces/dash-py-example/data/OilandGasMetadata.html'); + test('HTML Document [C1080701]', { tag: [tags.HTML] }, async function ({ app, page, openFile }) { + openFile('workspaces/dash-py-example/data/OilandGasMetadata.html'); await verifyOpenViewerRendersHtml(app); await verifySplitEditor(page, 'OilandGasMetadata.html'); await verifyOpenInNewWindow(page, ' Oil & Gas Wells - Metadata'); @@ -54,8 +52,8 @@ test.describe('Editor Action Bar', { test('Jupyter Notebook [C1080702]', { tag: [tags.NOTEBOOKS], annotation: [{ type: 'info', description: 'electron test unable to interact with dropdown native menu' }], - }, async function ({ app, page }) { - await openNotebook(app, 'workspaces/large_r_notebook/spotify.ipynb'); + }, async function ({ app, page, openDataFile }) { + await openDataFile('workspaces/large_r_notebook/spotify.ipynb'); if (app.web) { await verifyToggleLineNumbers(page); @@ -67,22 +65,6 @@ test.describe('Editor Action Bar', { }); -// Helper functions -async function openFile(app, filePath: string) { - const fileName = path.basename(filePath); - await test.step(`open file: ${fileName}`, async () => { - await app.workbench.quickaccess.openFile(path.join(app.workspacePathOrFolder, filePath)); - }); -} - -async function openNotebook(app: Application, filePath: string) { - await test.step('open jupyter notebook', async () => { - await app.workbench.quickaccess.openDataFile( - path.join(app.workspacePathOrFolder, filePath) - ); - }); -} - async function verifySplitEditor(page, tabName: string) { await test.step(`verify "split editor" opens another tab`, async () => { // Split editor right From 1312a2cdfae7e5a402ce33986a0420c79de0fd99 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 09:03:03 -0600 Subject: [PATCH 02/21] rename tests --- ...plorer-action-bar.test.ts => action-bar-data-explorer.test.ts} | 0 .../{editor-action-bar.test.ts => action-bar-editor.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/e2e/tests/action-bar/{data-explorer-action-bar.test.ts => action-bar-data-explorer.test.ts} (100%) rename test/e2e/tests/action-bar/{editor-action-bar.test.ts => action-bar-editor.test.ts} (100%) diff --git a/test/e2e/tests/action-bar/data-explorer-action-bar.test.ts b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts similarity index 100% rename from test/e2e/tests/action-bar/data-explorer-action-bar.test.ts rename to test/e2e/tests/action-bar/action-bar-data-explorer.test.ts diff --git a/test/e2e/tests/action-bar/editor-action-bar.test.ts b/test/e2e/tests/action-bar/action-bar-editor.test.ts similarity index 100% rename from test/e2e/tests/action-bar/editor-action-bar.test.ts rename to test/e2e/tests/action-bar/action-bar-editor.test.ts From e7e46f5e9b7a29b03dd51cf9e496de637705d04d Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 09:34:04 -0600 Subject: [PATCH 03/21] cleanup --- test/e2e/pages/console.ts | 42 ++++++++------- test/e2e/pages/variables.ts | 8 +-- test/e2e/tests/_test.setup.ts | 4 +- .../action-bar-data-explorer.test.ts | 52 +++---------------- .../action-bar/action-bar-editor.test.ts | 36 +------------ test/e2e/tests/action-bar/helpers.ts | 36 +++++++++++++ 6 files changed, 73 insertions(+), 105 deletions(-) create mode 100644 test/e2e/tests/action-bar/helpers.ts diff --git a/test/e2e/pages/console.ts b/test/e2e/pages/console.ts index c8930df805b..9a6c3720231 100644 --- a/test/e2e/pages/console.ts +++ b/test/e2e/pages/console.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { expect, Locator } from '@playwright/test'; +import test, { expect, Locator } from '@playwright/test'; import { Code } from '../infra/code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickInput'; @@ -78,30 +78,32 @@ export class Console { } async executeCode(languageName: string, code: string, prompt: string): Promise { + await test.step(`Execute ${languageName} code in console: ${code}`, async () => { - await expect(async () => { - // Kind of hacky, but activate console in case focus was previously lost - await this.activeConsole.click(); - await this.quickaccess.runCommand('workbench.action.executeCode.console', { keepOpen: true }); + await expect(async () => { + // Kind of hacky, but activate console in case focus was previously lost + await this.activeConsole.click(); + await this.quickaccess.runCommand('workbench.action.executeCode.console', { keepOpen: true }); - }).toPass(); + }).toPass(); - await this.quickinput.waitForQuickInputOpened(); - await this.quickinput.type(languageName); - await this.quickinput.waitForQuickInputElements(e => e.length === 1 && e[0] === languageName); - await this.code.driver.page.keyboard.press('Enter'); + await this.quickinput.waitForQuickInputOpened(); + await this.quickinput.type(languageName); + await this.quickinput.waitForQuickInputElements(e => e.length === 1 && e[0] === languageName); + await this.code.driver.page.keyboard.press('Enter'); - await this.quickinput.waitForQuickInputOpened(); - const unescapedCode = code - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r'); - await this.quickinput.type(unescapedCode); - await this.code.driver.page.keyboard.press('Enter'); - await this.quickinput.waitForQuickInputClosed(); + await this.quickinput.waitForQuickInputOpened(); + const unescapedCode = code + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); + await this.quickinput.type(unescapedCode); + await this.code.driver.page.keyboard.press('Enter'); + await this.quickinput.waitForQuickInputClosed(); - // The console will show the prompt after the code is done executing. - await this.waitForReady(prompt); - await this.maximizeConsole(); + // The console will show the prompt after the code is done executing. + await this.waitForReady(prompt); + await this.maximizeConsole(); + }); } async logConsoleContents() { diff --git a/test/e2e/pages/variables.ts b/test/e2e/pages/variables.ts index 2f230684c48..50be07a24c7 100644 --- a/test/e2e/pages/variables.ts +++ b/test/e2e/pages/variables.ts @@ -6,7 +6,7 @@ import { Code } from '../infra/code'; import * as os from 'os'; -import { expect, Locator } from '@playwright/test'; +import test, { expect, Locator } from '@playwright/test'; interface FlatVariables { value: string; @@ -64,8 +64,10 @@ export class Variables { } async doubleClickVariableRow(variableName: string) { - const desiredRow = await this.waitForVariableRow(variableName); - await desiredRow.dblclick(); + await test.step(`Double click variable row: ${variableName}`, async () => { + const desiredRow = await this.waitForVariableRow(variableName); + await desiredRow.dblclick(); + }); } async toggleVariablesView() { diff --git a/test/e2e/tests/_test.setup.ts b/test/e2e/tests/_test.setup.ts index a9066af92d0..0e133bc499c 100644 --- a/test/e2e/tests/_test.setup.ts +++ b/test/e2e/tests/_test.setup.ts @@ -145,9 +145,7 @@ export const test = base.extend({ devTools: [async ({ app }, use) => { await app.workbench.quickaccess.runCommand('workbench.action.toggleDevTools'); await use(); - }, - - { scope: 'test' }], + }, { scope: 'test' }], // example usage: await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); openFile: async ({ app }, use) => { diff --git a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts index 322642037e0..7598ecc4d0b 100644 --- a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts +++ b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts @@ -5,14 +5,14 @@ import { test, expect, tags } from '../_test.setup'; import { Application } from '../../infra'; -import { Page } from '@playwright/test'; +import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; test.use({ suiteId: __filename }); test.describe('Action Bar: Data Explorer', { - tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.EDITOR] + tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.DATA_EXPLORER] }, () => { test.beforeAll(async function ({ userSettings }) { @@ -23,10 +23,7 @@ test.describe('Action Bar: Data Explorer', { await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); }); - test('Python Pandas [...]', { - tag: [tags.R_MARKDOWN] - }, async function ({ app, page, openFile, python }) { - + test('Python Pandas [...]', async function ({ app, page, openFile, python }) { // load data in data explorer await openFile('workspaces/polars-dataframe-py/polars_basic.py'); await app.workbench.quickaccess.runCommand('python.execInConsole'); @@ -34,64 +31,31 @@ test.describe('Action Bar: Data Explorer', { await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); await expect(page.getByText('Data: df', { exact: true })).toBeVisible(); + // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); await verifySplitEditor(page, 'Data: df'); // await verifyOpenInNewWindow(page, 'Data: df — qa-example-content'); }); - test('R [...]', { - tag: [tags.R_MARKDOWN] - }, async function ({ app, page, openFile, r }) { - + test('R [...]', async function ({ app, page, openFile, r }) { // load data in data explorer await app.workbench.console.executeCode('R', rScript, '>'); await app.workbench.variables.doubleClickVariableRow('Data_Frame'); await expect(app.code.driver.page.getByText('Data: Data_Frame', { exact: true })).toBeVisible(); + // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, 'Data: df'); + await verifySplitEditor(page, 'Data: Data_Frame'); }); }); - -async function verifySplitEditor(page: Page, tabName: string) { - await test.step(`verify "split editor" opens another tab`, async () => { - // Split editor right - await page.getByLabel('Split Editor Right', { exact: true }).click(); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - // Close one tab - await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); - - // Split editor down - await page.keyboard.down('Alt'); - await page.getByLabel('Split Editor Down').click(); - await page.keyboard.up('Alt'); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - }); -} - -async function verifyOpenInNewWindow(page, expectedText: string) { - await test.step(`verify "open new window" contains: ${expectedText}`, async () => { - const [newPage] = await Promise.all([ - page.context().waitForEvent('page'), - page.getByLabel('Move into new window').first().click(), - ]); - await newPage.waitForLoadState(); - await expect(newPage.getByText(expectedText)).toBeVisible(); - }); -} - async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { const page = app.code.driver.page; - await test.step(`verify summary position: ${position}`, async () => { + await test.step(`Verify summary position: ${position}`, async () => { // Toggle the summary position - // * Web: Via the action bar - // * Desktop: Via the command palette if (app.web) { await page.getByLabel('More actions', { exact: true }).click(); await page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); diff --git a/test/e2e/tests/action-bar/action-bar-editor.test.ts b/test/e2e/tests/action-bar/action-bar-editor.test.ts index 634fed5e822..51abf72d8dc 100644 --- a/test/e2e/tests/action-bar/action-bar-editor.test.ts +++ b/test/e2e/tests/action-bar/action-bar-editor.test.ts @@ -6,6 +6,7 @@ import { test, expect, tags } from '../_test.setup'; import { Application } from '../../infra'; import { Page } from '@playwright/test'; +import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; test.use({ suiteId: __filename @@ -65,41 +66,6 @@ test.describe('Action Bar: Editor', { }); -async function verifySplitEditor(page, tabName: string) { - await test.step(`verify "split editor" opens another tab`, async () => { - // Split editor right - // Sometimes in CI the click doesn't register, wrapping these actions to reduce flake - await expect(async () => { - await page.getByLabel('Split Editor Right', { exact: true }).click(); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - }).toPass({ timeout: 10000 }); - - // Close one tab - await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); - - // Split editor down - // Sometimes in CI the click doesn't register, wrapping these actions to reduce flake - await expect(async () => { - await page.keyboard.down('Alt'); - await page.getByLabel('Split Editor Down').click(); - await page.keyboard.up('Alt'); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - }).toPass({ timeout: 10000 }); - - }); -} - -async function verifyOpenInNewWindow(page, expectedText: string) { - await test.step(`verify "open new window" contains: ${expectedText}`, async () => { - const [newPage] = await Promise.all([ - page.context().waitForEvent('page'), - page.getByLabel('Move into new window').first().click(), - ]); - await newPage.waitForLoadState(); - await expect(newPage.getByText(expectedText)).toBeVisible(); - }); -} - async function clickCustomizeNotebookMenuItem(page, menuItem: string) { const role = menuItem.includes('Line Numbers') ? 'menuitemcheckbox' : 'menuitem'; const dropdownButton = page.getByLabel('Customize Notebook...'); diff --git a/test/e2e/tests/action-bar/helpers.ts b/test/e2e/tests/action-bar/helpers.ts new file mode 100644 index 00000000000..816d69263e6 --- /dev/null +++ b/test/e2e/tests/action-bar/helpers.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import test, { expect } from '@playwright/test'; + +export async function verifySplitEditor(page, tabName: string) { + await test.step(`Verify "split editor" opens another tab`, async () => { + + // Split editor right + await page.getByLabel('Split Editor Right', { exact: true }).click(); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + // Close one tab + await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); + + // Split editor down + await page.keyboard.down('Alt'); + await page.getByLabel('Split Editor Down').click(); + await page.keyboard.up('Alt'); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + }); +} + +export async function verifyOpenInNewWindow(page, expectedText: string) { + await test.step(`Verify "open new window" contains: ${expectedText}`, async () => { + const [newPage] = await Promise.all([ + page.context().waitForEvent('page'), + page.getByLabel('Move into new window').first().click(), + ]); + await newPage.waitForLoadState(); + await expect(newPage.getByText(expectedText)).toBeVisible(); + }); +} From 9df783234c3fc3c46c873b0922f1e112845beb70 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 09:38:25 -0600 Subject: [PATCH 04/21] missing awaits --- test/e2e/tests/_test.setup.ts | 1 - test/e2e/tests/action-bar/action-bar-data-explorer.test.ts | 4 ++-- test/e2e/tests/action-bar/action-bar-editor.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/test/e2e/tests/_test.setup.ts b/test/e2e/tests/_test.setup.ts index 0e133bc499c..2502b3a7328 100644 --- a/test/e2e/tests/_test.setup.ts +++ b/test/e2e/tests/_test.setup.ts @@ -354,7 +354,6 @@ interface TestFixtures { devTools: void; openFile: (filePath: string) => Promise; openDataFile: (filePath: string) => Promise; - // runCommand: (command: string) => Promise; } interface WorkerFixtures { diff --git a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts index 7598ecc4d0b..9d00cb2280e 100644 --- a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts +++ b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts @@ -23,7 +23,7 @@ test.describe('Action Bar: Data Explorer', { await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); }); - test('Python Pandas [...]', async function ({ app, page, openFile, python }) { + test('Python Pandas Data [...]', async function ({ app, page, openFile, python }) { // load data in data explorer await openFile('workspaces/polars-dataframe-py/polars_basic.py'); await app.workbench.quickaccess.runCommand('python.execInConsole'); @@ -38,7 +38,7 @@ test.describe('Action Bar: Data Explorer', { // await verifyOpenInNewWindow(page, 'Data: df — qa-example-content'); }); - test('R [...]', async function ({ app, page, openFile, r }) { + test('R Data [...]', async function ({ app, page, openFile, r }) { // load data in data explorer await app.workbench.console.executeCode('R', rScript, '>'); await app.workbench.variables.doubleClickVariableRow('Data_Frame'); diff --git a/test/e2e/tests/action-bar/action-bar-editor.test.ts b/test/e2e/tests/action-bar/action-bar-editor.test.ts index 51abf72d8dc..13da14dd2f5 100644 --- a/test/e2e/tests/action-bar/action-bar-editor.test.ts +++ b/test/e2e/tests/action-bar/action-bar-editor.test.ts @@ -36,7 +36,7 @@ test.describe('Action Bar: Editor', { test('Quarto Document [C1080700]', { tag: [tags.QUARTO] }, async function ({ app, page, openFile }) { - openFile('workspaces/quarto_basic/quarto_basic.qmd'); + await openFile('workspaces/quarto_basic/quarto_basic.qmd'); await verifyPreviewRendersHtml(app, 'Diamond sizes'); await verifyOpenChanges(page); await verifySplitEditor(page, 'quarto_basic.qmd'); @@ -44,7 +44,7 @@ test.describe('Action Bar: Editor', { }); test('HTML Document [C1080701]', { tag: [tags.HTML] }, async function ({ app, page, openFile }) { - openFile('workspaces/dash-py-example/data/OilandGasMetadata.html'); + await openFile('workspaces/dash-py-example/data/OilandGasMetadata.html'); await verifyOpenViewerRendersHtml(app); await verifySplitEditor(page, 'OilandGasMetadata.html'); await verifyOpenInNewWindow(page, ' Oil & Gas Wells - Metadata'); From 20bb1d033bcab63c6eba9aed6324645b4a96ffa4 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 10:07:57 -0600 Subject: [PATCH 05/21] parquet and csv test --- .../action-bar-data-explorer.test.ts | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts index 9d00cb2280e..4ef8d5f4bee 100644 --- a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts +++ b/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts @@ -5,7 +5,7 @@ import { test, expect, tags } from '../_test.setup'; import { Application } from '../../infra'; -import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; +import { verifySplitEditor } from './helpers'; test.use({ suiteId: __filename @@ -23,31 +23,58 @@ test.describe('Action Bar: Data Explorer', { await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); }); - test('Python Pandas Data [...]', async function ({ app, page, openFile, python }) { + test('Python Data [...]', async function ({ app, page, openFile, python }) { // load data in data explorer + const title = 'Data: df'; await openFile('workspaces/polars-dataframe-py/polars_basic.py'); await app.workbench.quickaccess.runCommand('python.execInConsole'); await app.workbench.variables.doubleClickVariableRow('df'); await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); - await expect(page.getByText('Data: df', { exact: true })).toBeVisible(); + await expect(page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, 'Data: df'); - // await verifyOpenInNewWindow(page, 'Data: df — qa-example-content'); + await verifySplitEditor(page, title); + // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); }); - test('R Data [...]', async function ({ app, page, openFile, r }) { + test('R Data [...]', async function ({ app, page, r }) { // load data in data explorer + const title = 'Data: Data_Frame'; await app.workbench.console.executeCode('R', rScript, '>'); await app.workbench.variables.doubleClickVariableRow('Data_Frame'); - await expect(app.code.driver.page.getByText('Data: Data_Frame', { exact: true })).toBeVisible(); + await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, 'Data: Data_Frame'); + await verifySplitEditor(page, title); + // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + }); + + test('Parquet Data [...]', async function ({ app, page, openDataFile }) { + // load data in data explorer + const title = 'Data: 100x100.parquet'; + await openDataFile('data-files/100x100/100x100.parquet'); + + // verify action bar behavior + await verifySummaryPosition(app, 'Left'); + await verifySummaryPosition(app, 'Right'); + await verifySplitEditor(page, title); + // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + }); + + test('CSV Data [...]', async function ({ app, page, openDataFile }) { + // load data in data explorer + const title = 'Data: data.csv'; + await openDataFile('data-files/spotify_data/data.csv'); + + // verify action bar behavior + await verifySummaryPosition(app, 'Left'); + await verifySummaryPosition(app, 'Right'); + await verifySplitEditor(page, title); + // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); }); }); From 1b11827ab857502de772512bbc4d55a26f4ae69d Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 13:27:49 -0600 Subject: [PATCH 06/21] use variables pane --- test/e2e/infra/test-runner/test-tags.ts | 2 +- test/e2e/pages/console.ts | 4 +- test/e2e/pages/settings.ts | 1 + test/e2e/pages/utils/packageManager.ts | 3 +- test/e2e/tests/action-bar/helpers.ts | 36 --------- .../tests/connections/connections-db.test.ts | 2 - .../data-explorer/100x100-pandas.test.ts | 1 - .../data-explorer/100x100-polars.test.ts | 1 - .../e2e/tests/data-explorer/100x100-r.test.ts | 1 - .../data-explorer-python-pandas.test.ts | 8 +- .../data-explorer/data-explorer-r.test.ts | 6 +- .../tests/data-explorer/helpers/100x100.ts | 2 - .../tests/data-explorer/sparklines.test.ts | 4 +- .../editor-action-bar-data-explorer.test.ts} | 71 +++++++++++------ .../editor-action-bar-documents.test.ts} | 11 ++- test/e2e/tests/editor-action-bar/helpers.ts | 79 +++++++++++++++++++ test/e2e/tests/help/help.test.ts | 4 +- test/e2e/tests/plots/plots.test.ts | 14 ++-- .../variables/variables-expanded.test.ts | 4 +- .../tests/variables/variables-pane.test.ts | 4 +- test/e2e/tests/viewer/viewer.test.ts | 6 +- 21 files changed, 162 insertions(+), 102 deletions(-) delete mode 100644 test/e2e/tests/action-bar/helpers.ts rename test/e2e/tests/{action-bar/action-bar-data-explorer.test.ts => editor-action-bar/editor-action-bar-data-explorer.test.ts} (66%) rename test/e2e/tests/{action-bar/action-bar-editor.test.ts => editor-action-bar/editor-action-bar-documents.test.ts} (92%) create mode 100644 test/e2e/tests/editor-action-bar/helpers.ts diff --git a/test/e2e/infra/test-runner/test-tags.ts b/test/e2e/infra/test-runner/test-tags.ts index 9fbad4b1eaf..a7b33ec58c9 100644 --- a/test/e2e/infra/test-runner/test-tags.ts +++ b/test/e2e/infra/test-runner/test-tags.ts @@ -18,13 +18,13 @@ export enum TestTags { // feature tags - ACTION_BAR = '@:action-bar', APPS = '@:apps', CONNECTIONS = '@:connections', CONSOLE = '@:console', CRITICAL = '@:critical', DATA_EXPLORER = '@:data-explorer', DUCK_DB = '@:duck-db', + EDITOR_ACTION_BAR = '@:editor-action-bar', HELP = '@:help', HTML = '@:html', INTERPRETER = '@:interpreter', diff --git a/test/e2e/pages/console.ts b/test/e2e/pages/console.ts index 9a6c3720231..4902a226631 100644 --- a/test/e2e/pages/console.ts +++ b/test/e2e/pages/console.ts @@ -77,7 +77,7 @@ export class Console { return; } - async executeCode(languageName: string, code: string, prompt: string): Promise { + async executeCode(languageName: string, code: string): Promise { await test.step(`Execute ${languageName} code in console: ${code}`, async () => { await expect(async () => { @@ -101,7 +101,7 @@ export class Console { await this.quickinput.waitForQuickInputClosed(); // The console will show the prompt after the code is done executing. - await this.waitForReady(prompt); + await this.waitForReady(languageName === 'Python' ? '>>>' : '>'); await this.maximizeConsole(); }); } diff --git a/test/e2e/pages/settings.ts b/test/e2e/pages/settings.ts index 489e9b2bd6e..e49b9cb5f78 100644 --- a/test/e2e/pages/settings.ts +++ b/test/e2e/pages/settings.ts @@ -16,6 +16,7 @@ export class Settings { async addUserSettings(settings: [key: string, value: string][]): Promise { await this.openUserSettingsFile(); const file = 'settings.json'; + await this.editors.saveOpenedFile(); await this.code.driver.page.keyboard.press('ArrowRight'); await this.editor.waitForTypeInEditor(file, settings.map(v => `"${v[0]}": ${v[1]},`).join('')); await this.editors.saveOpenedFile(); diff --git a/test/e2e/pages/utils/packageManager.ts b/test/e2e/pages/utils/packageManager.ts index 9e55aa6770d..415b139fd1a 100644 --- a/test/e2e/pages/utils/packageManager.ts +++ b/test/e2e/pages/utils/packageManager.ts @@ -36,9 +36,8 @@ export class PackageManager { await test.step(`${action}: ${packageName}`, async () => { const command = this.getCommand(packageInfo.type, packageName, action); const expectedOutput = this.getExpectedOutput(packageName, action); - const prompt = packageInfo.type === 'Python' ? '>>> ' : '> '; - await this.app.workbench.console.executeCode(packageInfo.type, command, prompt); + await this.app.workbench.console.executeCode(packageInfo.type, command); await expect(this.app.code.driver.page.getByText(expectedOutput)).toBeVisible(); }); } diff --git a/test/e2e/tests/action-bar/helpers.ts b/test/e2e/tests/action-bar/helpers.ts deleted file mode 100644 index 816d69263e6..00000000000 --- a/test/e2e/tests/action-bar/helpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import test, { expect } from '@playwright/test'; - -export async function verifySplitEditor(page, tabName: string) { - await test.step(`Verify "split editor" opens another tab`, async () => { - - // Split editor right - await page.getByLabel('Split Editor Right', { exact: true }).click(); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - // Close one tab - await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); - - // Split editor down - await page.keyboard.down('Alt'); - await page.getByLabel('Split Editor Down').click(); - await page.keyboard.up('Alt'); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - }); -} - -export async function verifyOpenInNewWindow(page, expectedText: string) { - await test.step(`Verify "open new window" contains: ${expectedText}`, async () => { - const [newPage] = await Promise.all([ - page.context().waitForEvent('page'), - page.getByLabel('Move into new window').first().click(), - ]); - await newPage.waitForLoadState(); - await expect(newPage.getByText(expectedText)).toBeVisible(); - }); -} diff --git a/test/e2e/tests/connections/connections-db.test.ts b/test/e2e/tests/connections/connections-db.test.ts index abc7b9df355..b2d586ab8d0 100644 --- a/test/e2e/tests/connections/connections-db.test.ts +++ b/test/e2e/tests/connections/connections-db.test.ts @@ -91,7 +91,6 @@ test.describe('SQLite DB Connection', { await app.workbench.console.executeCode( 'R', `con <- connections::connection_open(RSQLite::SQLite(), tempfile())`, - '>' ); }); @@ -111,7 +110,6 @@ test.describe('SQLite DB Connection', { await app.workbench.console.executeCode( 'R', `DBI::dbWriteTable(con, 'mtcars', mtcars)`, - '>' ); // refresh and mtcars should exist diff --git a/test/e2e/tests/data-explorer/100x100-pandas.test.ts b/test/e2e/tests/data-explorer/100x100-pandas.test.ts index e8e3945245e..e9355b9e5b8 100644 --- a/test/e2e/tests/data-explorer/100x100-pandas.test.ts +++ b/test/e2e/tests/data-explorer/100x100-pandas.test.ts @@ -20,7 +20,6 @@ test('Data Explorer 100x100 - Python - Pandas [C557563]', { await testDataExplorer( app, 'Python', - '>>>', [ 'import pandas as pd', `${dataFrameName} = pd.read_parquet("${parquetFilePath(app)}")`, diff --git a/test/e2e/tests/data-explorer/100x100-polars.test.ts b/test/e2e/tests/data-explorer/100x100-polars.test.ts index 6c7de9559b8..3162fea0403 100644 --- a/test/e2e/tests/data-explorer/100x100-polars.test.ts +++ b/test/e2e/tests/data-explorer/100x100-polars.test.ts @@ -20,7 +20,6 @@ test('Data Explorer 100x100 - Python - Polars [C674520]', { await testDataExplorer( app, 'Python', - '>>>', [ 'import polars', `${dataFrameName} = polars.read_parquet("${parquetFilePath(app)}")`, diff --git a/test/e2e/tests/data-explorer/100x100-r.test.ts b/test/e2e/tests/data-explorer/100x100-r.test.ts index 1a4142487d2..00a448767f2 100644 --- a/test/e2e/tests/data-explorer/100x100-r.test.ts +++ b/test/e2e/tests/data-explorer/100x100-r.test.ts @@ -21,7 +21,6 @@ test('Data Explorer 100x100 - R [C674521]', { await testDataExplorer( app, 'R', - '>', [ 'library(arrow)', `${dataFrameName} <- read_parquet("${parquetFilePath(app)}")`, diff --git a/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts b/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts index ae3180275f1..ce52d44667f 100644 --- a/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts @@ -22,7 +22,7 @@ data = {'Name':['Jai', 'Princi', 'Gaurav', 'Anuj'], df = pd.DataFrame(data)`; logger.log('Sending code to console'); - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); logger.log('Opening data grid'); await expect(async () => { @@ -62,7 +62,7 @@ data = { df2 = pd.DataFrame(data)`; logger.log('Sending code to console'); - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); logger.log('Opening data grid'); await expect(async () => { @@ -181,7 +181,7 @@ df2 = pd.DataFrame(data)`; const script = `import pandas as pd from pydataset import data Data_Frame = data('mtcars')`; - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus'); await expect(async () => { @@ -206,7 +206,7 @@ Data_Frame = data('mtcars')`; const script = `import pandas as pd df = pd.DataFrame({'x': ["a ", "a", " ", ""]})`; - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); await expect(async () => { await app.workbench.variables.doubleClickVariableRow('df'); diff --git a/test/e2e/tests/data-explorer/data-explorer-r.test.ts b/test/e2e/tests/data-explorer/data-explorer-r.test.ts index 93abe3bc3a0..7bfbdfd5be8 100644 --- a/test/e2e/tests/data-explorer/data-explorer-r.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-r.test.ts @@ -22,7 +22,7 @@ test.describe('Data Explorer - R ', { )`; logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', script, '>'); + await app.workbench.console.executeCode('R', script); logger.log('Opening data grid'); await expect(async () => { @@ -82,7 +82,7 @@ test.describe('Data Explorer - R ', { // Regression test for https://github.com/posit-dev/positron/issues/4197 // and https://github.com/posit-dev/positron/issues/5714 const script = `Data_Frame <- mtcars`; - await app.workbench.console.executeCode('R', script, '>'); + await app.workbench.console.executeCode('R', script); await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus'); await expect(async () => { @@ -105,7 +105,7 @@ test.describe('Data Explorer - R ', { test('R - Check blank spaces in data explorer [C1078834]', async function ({ app, r }) { const script = `df = data.frame(x = c("a ", "a", " ", ""))`; - await app.workbench.console.executeCode('R', script, '>'); + await app.workbench.console.executeCode('R', script); await expect(async () => { await app.workbench.variables.doubleClickVariableRow('df'); diff --git a/test/e2e/tests/data-explorer/helpers/100x100.ts b/test/e2e/tests/data-explorer/helpers/100x100.ts index 681fc8e643d..5777224f7f4 100644 --- a/test/e2e/tests/data-explorer/helpers/100x100.ts +++ b/test/e2e/tests/data-explorer/helpers/100x100.ts @@ -11,7 +11,6 @@ import { Application } from '../../../infra'; export const testDataExplorer = async ( app: Application, language: 'Python' | 'R', - prompt: string, commands: string[], dataFrameName: string, tsvFilePath: string @@ -21,7 +20,6 @@ export const testDataExplorer = async ( await app.workbench.console.executeCode( language, commands[i], - prompt ); } diff --git a/test/e2e/tests/data-explorer/sparklines.test.ts b/test/e2e/tests/data-explorer/sparklines.test.ts index dcda21658ae..9c32b76bd26 100644 --- a/test/e2e/tests/data-explorer/sparklines.test.ts +++ b/test/e2e/tests/data-explorer/sparklines.test.ts @@ -23,14 +23,14 @@ test.describe('Data Explorer - Sparklines', { }); test('Python Pandas - Verifies downward trending graph [C830552]', async ({ app, python }) => { - await app.workbench.console.executeCode('Python', pythonScript, '>>>'); + await app.workbench.console.executeCode('Python', pythonScript); await openDataExplorerColumnProfile(app, 'pythonData'); await verifyGraphBarHeights(app); }); test('R - Verifies downward trending graph [C830553]', async ({ app, r }) => { - await app.workbench.console.executeCode('R', rScript, '>'); + await app.workbench.console.executeCode('R', rScript); await openDataExplorerColumnProfile(app, 'rData'); await verifyGraphBarHeights(app); }); diff --git a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts similarity index 66% rename from test/e2e/tests/action-bar/action-bar-data-explorer.test.ts rename to test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 4ef8d5f4bee..03143f90bb4 100644 --- a/test/e2e/tests/action-bar/action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -5,14 +5,14 @@ import { test, expect, tags } from '../_test.setup'; import { Application } from '../../infra'; -import { verifySplitEditor } from './helpers'; +import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; test.use({ suiteId: __filename }); -test.describe('Action Bar: Data Explorer', { - tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.DATA_EXPLORER] +test.describe('Editor Action Bar: Data Explorer', { + tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.DATA_EXPLORER] }, () => { test.beforeAll(async function ({ userSettings }) { @@ -23,58 +23,62 @@ test.describe('Action Bar: Data Explorer', { await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); }); - test('Python Data [...]', async function ({ app, page, openFile, python }) { + test('Python Pandas (Parquet) - Variables Pane [C...]', async function ({ app, page, python }) { // load data in data explorer const title = 'Data: df'; - await openFile('workspaces/polars-dataframe-py/polars_basic.py'); - await app.workbench.quickaccess.runCommand('python.execInConsole'); + await app.workbench.console.executeCode('Python', parquetScript); await app.workbench.variables.doubleClickVariableRow('df'); - await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); - await expect(page.getByText(title, { exact: true })).toBeVisible(); + await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); await verifySplitEditor(page, title); - // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + await verifyOpenInNewWindow(app, `${title} — qa-example-content`); }); - test('R Data [...]', async function ({ app, page, r }) { + test('Python Pandas (CSV Data) - Variables Pane [C...]', async function ({ app, page, python }) { // load data in data explorer - const title = 'Data: Data_Frame'; - await app.workbench.console.executeCode('R', rScript, '>'); - await app.workbench.variables.doubleClickVariableRow('Data_Frame'); + const title = 'Data: df'; + await app.workbench.console.executeCode('Python', csvScript); + await app.workbench.variables.doubleClickVariableRow('df'); await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); await verifySplitEditor(page, title); - // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + await verifyOpenInNewWindow(app, `${title} — qa-example-content`); }); - test('Parquet Data [...]', async function ({ app, page, openDataFile }) { + test('Python Polars - Variables Pane [C...]', async function ({ app, page, openFile, python }) { // load data in data explorer - const title = 'Data: 100x100.parquet'; - await openDataFile('data-files/100x100/100x100.parquet'); + const title = 'Data: df'; + await openFile('workspaces/polars-dataframe-py/polars_basic.py'); + await app.workbench.quickaccess.runCommand('python.execInConsole'); + await app.workbench.variables.doubleClickVariableRow('df'); + await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); + await expect(page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); await verifySplitEditor(page, title); - // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + await verifyOpenInNewWindow(app, `${title} — qa-example-content`); }); - test('CSV Data [...]', async function ({ app, page, openDataFile }) { + test('R - Variables Pane [C...]', async function ({ app, page, r }) { // load data in data explorer - const title = 'Data: data.csv'; - await openDataFile('data-files/spotify_data/data.csv'); + const title = 'Data: Data_Frame'; + await app.workbench.console.executeCode('R', rScript); + await app.workbench.variables.doubleClickVariableRow('Data_Frame'); + await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); // verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); await verifySplitEditor(page, title); - // await verifyOpenInNewWindow(page, `${title} — qa-example-content`); + await verifyOpenInNewWindow(app, `${title} — qa-example-content`); }); }); @@ -118,10 +122,31 @@ async function verifySummaryPosition(app: Application, position: 'Left' | 'Right }); } -// snippet from https://www.w3schools.com/r/r_data_frames.asp const rScript = `Data_Frame <- data.frame ( Training = c("Strength", "Stamina", "Other"), Pulse = c(100, NA, 120), Duration = c(60, 30, 45), Note = c(NA, NA, "Note") )`; + +const parquetScript = `import pandas as pd +import os + +file_path = os.path.join(os.getcwd(), 'data-files', '100x100', '100x100.parquet') + +# Read the Parquet file into a pandas DataFrame +df = pd.read_parquet(file_path) + +# Display the DataFrame +print(df)`; + +const csvScript = `import pandas as pd +import os + +file_path = os.path.join(os.getcwd(), 'data-files', 'spotify_data', 'data.csv') + +# Read the CSV file into a pandas DataFrame +df = pd.read_csv(file_path) + +# Display the DataFrame +print(df)`; diff --git a/test/e2e/tests/action-bar/action-bar-editor.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts similarity index 92% rename from test/e2e/tests/action-bar/action-bar-editor.test.ts rename to test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts index 13da14dd2f5..838b3607e68 100644 --- a/test/e2e/tests/action-bar/action-bar-editor.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts @@ -12,8 +12,8 @@ test.use({ suiteId: __filename }); -test.describe('Action Bar: Editor', { - tag: [tags.WEB, tags.WIN, tags.ACTION_BAR, tags.EDITOR] +test.describe('Editor Action Bar: Documents', { + tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.EDITOR] }, () => { test.beforeAll(async function ({ userSettings }) { @@ -30,7 +30,7 @@ test.describe('Action Bar: Editor', { await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); await verifyPreviewRendersHtml(app, 'Getting startedAnchor'); await verifySplitEditor(page, 'basicRmd.rmd'); - await verifyOpenInNewWindow(page, 'This post examines the features'); + await verifyOpenInNewWindow(app, 'This post examines the features'); }); test('Quarto Document [C1080700]', { @@ -40,19 +40,18 @@ test.describe('Action Bar: Editor', { await verifyPreviewRendersHtml(app, 'Diamond sizes'); await verifyOpenChanges(page); await verifySplitEditor(page, 'quarto_basic.qmd'); - await verifyOpenInNewWindow(page, 'Diamond sizes'); + await verifyOpenInNewWindow(app, 'Diamond sizes'); }); test('HTML Document [C1080701]', { tag: [tags.HTML] }, async function ({ app, page, openFile }) { await openFile('workspaces/dash-py-example/data/OilandGasMetadata.html'); await verifyOpenViewerRendersHtml(app); await verifySplitEditor(page, 'OilandGasMetadata.html'); - await verifyOpenInNewWindow(page, ' Oil & Gas Wells - Metadata'); + await verifyOpenInNewWindow(app, ' Oil & Gas Wells - Metadata'); }); test('Jupyter Notebook [C1080702]', { tag: [tags.NOTEBOOKS], - annotation: [{ type: 'info', description: 'electron test unable to interact with dropdown native menu' }], }, async function ({ app, page, openDataFile }) { await openDataFile('workspaces/large_r_notebook/spotify.ipynb'); diff --git a/test/e2e/tests/editor-action-bar/helpers.ts b/test/e2e/tests/editor-action-bar/helpers.ts new file mode 100644 index 00000000000..b9750cc5ce6 --- /dev/null +++ b/test/e2e/tests/editor-action-bar/helpers.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import test, { expect } from '@playwright/test'; +import { Application } from '../../infra'; + +export async function verifySplitEditor(page, tabName: string) { + await test.step(`Verify "split editor" opens another tab`, async () => { + + // Split editor right + await page.getByLabel('Split Editor Right', { exact: true }).click(); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + // Close one tab + await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); + + // Split editor down + await page.keyboard.down('Alt'); + await page.getByLabel('Split Editor Down').click(); + await page.keyboard.up('Alt'); + await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + + }); +} + +export async function verifyOpenInNewWindow(app: Application, expectedText: string) { + if (!app.web) { + await test.step(`Verify "open new window" contains: ${expectedText}`, async () => { + const [newPage] = await Promise.all([ + app.code.driver.page.context().waitForEvent('page'), + app.code.driver.page.getByLabel('Move into new window').first().click(), + ]); + await newPage.waitForLoadState(); + await expect(newPage.getByText(expectedText)).toBeVisible(); + }); + } +} + +export async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { + const page = app.code.driver.page; + + await test.step(`Verify summary position: ${position}`, async () => { + // Toggle the summary position + if (app.web) { + await page.getByLabel('More actions', { exact: true }).click(); + await page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); + await page.keyboard.press('Enter'); + } + else { + await app.workbench.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); + } + + // Locator for the summary element + const summaryLocator = page.locator('div.column-summary').first(); + const tableLocator = page.locator('div.data-grid-column-headers'); + + // Ensure both the summary and table elements are visible + await Promise.all([ + expect(summaryLocator).toBeVisible(), + expect(tableLocator).toBeVisible(), + ]); + + // Get the bounding boxes for both elements + const summaryBox = await summaryLocator.boundingBox(); + const tableBox = await tableLocator.boundingBox(); + + // Validate bounding boxes are available + if (!summaryBox || !tableBox) { + throw new Error('Bounding boxes could not be retrieved for summary or table.'); + } + + // Validate positions based on the expected position + position === 'Left' + ? expect(summaryBox.x).toBeLessThan(tableBox.x) + : expect(summaryBox.x).toBeGreaterThan(tableBox.x); + }); +} diff --git a/test/e2e/tests/help/help.test.ts b/test/e2e/tests/help/help.test.ts index f2f6c00f926..d1ce16f6e04 100644 --- a/test/e2e/tests/help/help.test.ts +++ b/test/e2e/tests/help/help.test.ts @@ -18,7 +18,7 @@ test.describe('Help', { tag: [tags.HELP, tags.WEB] }, () => { }); test('Python - Verifies basic help functionality [C633814]', { tag: [tags.WIN] }, async function ({ app, python }) { - await app.workbench.console.executeCode('Python', `?load`, '>>>'); + await app.workbench.console.executeCode('Python', `?load`); await expect(async () => { const helpFrame = await app.workbench.help.getHelpFrame(0); @@ -28,7 +28,7 @@ test.describe('Help', { tag: [tags.HELP, tags.WEB] }, () => { }); test('R - Verifies basic help functionality [C633813]', { tag: [tags.WIN] }, async function ({ app, r }) { - await app.workbench.console.executeCode('R', `?load()`, '>'); + await app.workbench.console.executeCode('R', `?load()`); await expect(async () => { const helpFrame = await app.workbench.help.getHelpFrame(1); diff --git a/test/e2e/tests/plots/plots.test.ts b/test/e2e/tests/plots/plots.test.ts index 3de680581c7..8d324e2207b 100644 --- a/test/e2e/tests/plots/plots.test.ts +++ b/test/e2e/tests/plots/plots.test.ts @@ -44,7 +44,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { }, async function ({ app, logger, headless, logsPath }, 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.console.executeCode('Python', pythonDynamicPlot); await app.workbench.plots.waitForCurrentPlot(); const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); @@ -83,7 +83,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { tag: [tags.CRITICAL, tags.WEB, tags.WIN] }, async function ({ app, logger, logsPath }, testInfo) { logger.log('Sending code to console'); - await app.workbench.console.executeCode('Python', pythonStaticPlot, '>>>'); + await app.workbench.console.executeCode('Python', pythonStaticPlot); await app.workbench.plots.waitForCurrentStaticPlot(); const buffer = await app.workbench.plots.getCurrentStaticPlotAsBuffer(); @@ -113,9 +113,9 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { await expect(plots.zoomPlotButton).not.toBeVisible(); // create plots separately so that the order is known - await app.workbench.console.executeCode('Python', pythonPlotActions1, '>>>'); + await app.workbench.console.executeCode('Python', pythonPlotActions1); await plots.waitForCurrentStaticPlot(); - await app.workbench.console.executeCode('Python', pythonPlotActions2, '>>>'); + await app.workbench.console.executeCode('Python', pythonPlotActions2); await plots.waitForCurrentPlot(); // expand the plot pane to show the action bar @@ -152,7 +152,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { test('Python - Verifies saving a Python plot [C557005]', { tag: [tags.WIN] }, async function ({ app, logger }) { await test.step('Sending code to console to create plot', async () => { - await app.workbench.console.executeCode('Python', pythonDynamicPlot, '>>>'); + await app.workbench.console.executeCode('Python', pythonDynamicPlot); await app.workbench.plots.waitForCurrentPlot(); await app.workbench.layouts.enterLayout('fullSizedAuxBar'); }); @@ -289,7 +289,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { tag: [tags.CRITICAL, tags.WEB, tags.WIN] }, async function ({ app, logger, headless, logsPath }, testInfo) { logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', rBasicPlot, '>'); + await app.workbench.console.executeCode('R', rBasicPlot); await app.workbench.plots.waitForCurrentPlot(); const buffer = await app.workbench.plots.getCurrentPlotAsBuffer(); @@ -326,7 +326,7 @@ test.describe('Plots', { tag: [tags.PLOTS, tags.EDITOR] }, () => { test('R - Verifies saving an R plot [C557006]', { tag: [tags.WIN] }, async function ({ app, logger }) { await test.step('Sending code to console to create plot', async () => { - await app.workbench.console.executeCode('R', rSavePlot, '>'); + await app.workbench.console.executeCode('R', rSavePlot); await app.workbench.plots.waitForCurrentPlot(); }); diff --git a/test/e2e/tests/variables/variables-expanded.test.ts b/test/e2e/tests/variables/variables-expanded.test.ts index b2fab9f1494..0097ae22b6e 100644 --- a/test/e2e/tests/variables/variables-expanded.test.ts +++ b/test/e2e/tests/variables/variables-expanded.test.ts @@ -18,7 +18,7 @@ test.describe('Variables - Expanded View', { tag: [tags.WEB, tags.VARIABLES] }, test('Python - should display children values and types when variable is expanded [C1078836]', async function ({ app, python }) { const variables = app.workbench.variables; - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); await app.workbench.layouts.enterLayout('fullSizedAuxBar'); await variables.expandVariable('df'); @@ -34,7 +34,7 @@ test.describe('Variables - Expanded View', { tag: [tags.WEB, tags.VARIABLES] }, // workaround for https://github.com/posit-dev/positron/issues/5718 await app.workbench.popups.closeAllToasts(); - await app.workbench.console.executeCode('R', 'df2 <- data.frame(b=rep(1:1000000))', '>'); + await app.workbench.console.executeCode('R', 'df2 <- data.frame(b=rep(1:1000000))'); await app.workbench.layouts.enterLayout('fullSizedAuxBar'); await variables.expandVariable('df2'); diff --git a/test/e2e/tests/variables/variables-pane.test.ts b/test/e2e/tests/variables/variables-pane.test.ts index fc5a3d68b2f..b1533eba72b 100644 --- a/test/e2e/tests/variables/variables-pane.test.ts +++ b/test/e2e/tests/variables/variables-pane.test.ts @@ -18,7 +18,7 @@ test.describe('Variables Pane', { test('Python - Verifies Variables pane basic function [C628634]', async function ({ app, logger, python }) { const executeCode = async (code: string) => { - await app.workbench.console.executeCode('Python', code, '>>>'); + await app.workbench.console.executeCode('Python', code); }; await executeCode('x=1'); @@ -38,7 +38,7 @@ test.describe('Variables Pane', { test('R - Verifies Variables pane basic function [C628635]', async function ({ app, logger, r }) { const executeCode = async (code: string) => { - await app.workbench.console.executeCode('R', code, '>'); + await app.workbench.console.executeCode('R', code); }; await executeCode('x=1'); diff --git a/test/e2e/tests/viewer/viewer.test.ts b/test/e2e/tests/viewer/viewer.test.ts index 343e35dcbea..e367b56c102 100644 --- a/test/e2e/tests/viewer/viewer.test.ts +++ b/test/e2e/tests/viewer/viewer.test.ts @@ -43,7 +43,7 @@ test.describe('Viewer', { tag: [tags.VIEWER] }, () => { test('R - Verify Viewer functionality with modelsummary [C784889]', { tag: [tags.WEB] }, async function ({ app, logger, r }) { logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', rModelSummaryScript, '>'); + await app.workbench.console.executeCode('R', rModelSummaryScript); let billDepthLocator; if (!app.web) { billDepthLocator = app.workbench.viewer.getViewerLocator('tr').filter({ hasText: 'bill_depth_mm' }); @@ -59,7 +59,7 @@ test.describe('Viewer', { tag: [tags.VIEWER] }, () => { }, async function ({ app, logger, r }) { logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', rReactableScript, '>'); + await app.workbench.console.executeCode('R', rReactableScript); const datsun710 = app.workbench.viewer.getViewerLocator('div.rt-td-inner').filter({ hasText: 'Datsun 710' }); @@ -72,7 +72,7 @@ test.describe('Viewer', { tag: [tags.VIEWER] }, () => { }, async function ({ app, logger, r }) { logger.log('Sending code to console'); - await app.workbench.console.executeCode('R', rReprexScript, '>'); + await app.workbench.console.executeCode('R', rReprexScript); const rnorm = app.workbench.viewer.getViewerLocator('code.sourceCode').filter({ hasText: 'rbinom' }); From 1a4437db9d2ba18c2b7e9bbb54453434a81b6bd0 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 15:40:25 -0600 Subject: [PATCH 07/21] waitForReady --- test/e2e/infra/fixtures/interpreter.ts | 8 ++++---- test/e2e/pages/console.ts | 18 +++++++++++++----- test/e2e/tests/console/console-python.test.ts | 6 ++---- test/e2e/tests/console/console-r.test.ts | 6 ++---- .../new-project-python.test.ts | 2 +- .../r-pkg-development.test.ts | 2 +- test/e2e/tests/reticulate/reticulate.test.ts | 6 +++--- .../tests/test-explorer/test-explorer.test.ts | 2 +- 8 files changed, 27 insertions(+), 23 deletions(-) diff --git a/test/e2e/infra/fixtures/interpreter.ts b/test/e2e/infra/fixtures/interpreter.ts index b247cea065c..15dba94d84d 100644 --- a/test/e2e/infra/fixtures/interpreter.ts +++ b/test/e2e/infra/fixtures/interpreter.ts @@ -87,8 +87,8 @@ export class Interpreter { if (waitForReady) { interpreterType === 'Python' - ? await this.console.waitForReady('>>>', 30000) - : await this.console.waitForReady('>', 30000); + ? await this.console.waitForReadyAndStarted('>>>', 30000) + : await this.console.waitForReadyAndStarted('>', 30000); } }); } @@ -306,8 +306,8 @@ export class Interpreter { await this.console.waitForConsoleContents('restarted'); interpreterType === 'Python' - ? await this.console.waitForReady('>>>', 10000) - : await this.console.waitForReady('>', 10000); + ? await this.console.waitForReadyAndStarted('>>>', 10000) + : await this.console.waitForReadyAndStarted('>', 10000); }); } diff --git a/test/e2e/pages/console.ts b/test/e2e/pages/console.ts index 4902a226631..8bbaec7be22 100644 --- a/test/e2e/pages/console.ts +++ b/test/e2e/pages/console.ts @@ -71,13 +71,13 @@ export class Console { if (waitForReady) { desiredInterpreterType === InterpreterType.Python - ? await this.waitForReady('>>>', 40000) - : await this.waitForReady('>', 40000); + ? await this.waitForReadyAndStarted('>>>', 40000) + : await this.waitForReadyAndStarted('>', 40000); } return; } - async executeCode(languageName: string, code: string): Promise { + async executeCode(languageName: 'Python' | 'R', code: string): Promise { await test.step(`Execute ${languageName} code in console: ${code}`, async () => { await expect(async () => { @@ -131,9 +131,17 @@ export class Console { async waitForReady(prompt: string, timeout = 30000): Promise { const activeLine = this.code.driver.page.locator(`${ACTIVE_CONSOLE_INSTANCE} .active-line-number`); - await expect(activeLine).toHaveText(prompt, { timeout }); - await this.waitForConsoleContents('started', { timeout }); + } + + async waitForReadyAndStarted(prompt: string, timeout = 30000): Promise { + this.waitForReady(prompt, timeout); + await this.waitForConsoleContents('started'); + } + + async waitForReadyAndRestarted(prompt: string, timeout = 30000): Promise { + this.waitForReady(prompt, timeout); + await this.waitForConsoleContents('restarted'); } /** diff --git a/test/e2e/tests/console/console-python.test.ts b/test/e2e/tests/console/console-python.test.ts index a529ce8928c..5a92b22f864 100644 --- a/test/e2e/tests/console/console-python.test.ts +++ b/test/e2e/tests/console/console-python.test.ts @@ -22,8 +22,7 @@ test.describe('Console Pane: Python', { tag: [tags.WEB, tags.WIN, tags.CONSOLE] await app.workbench.console.consoleRestartButton.click(); await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); - await app.workbench.console.waitForReady('>>>'); - await app.workbench.console.waitForConsoleContents('restarted'); + await app.workbench.console.waitForReadyAndRestarted('>>>'); await expect(app.workbench.console.consoleRestartButton).not.toBeVisible(); }).toPass(); }); @@ -39,8 +38,7 @@ test.describe('Console Pane: Python', { tag: [tags.WEB, tags.WIN, tags.CONSOLE] await app.workbench.console.barRestartButton.click(); await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar'); - await app.workbench.console.waitForReady('>>>'); - await app.workbench.console.waitForConsoleContents('restarted'); + await app.workbench.console.waitForReadyAndStarted('>>>'); }); test('Verify cancel button on console bar [C...]', { diff --git a/test/e2e/tests/console/console-r.test.ts b/test/e2e/tests/console/console-r.test.ts index 632151d9624..57587d0b68f 100644 --- a/test/e2e/tests/console/console-r.test.ts +++ b/test/e2e/tests/console/console-r.test.ts @@ -22,8 +22,7 @@ test.describe('Console Pane: R', { await app.workbench.console.barClearButton.click(); await app.workbench.console.barPowerButton.click(); await app.workbench.console.consoleRestartButton.click(); - await app.workbench.console.waitForReady('>'); - await app.workbench.console.waitForConsoleContents('restarted'); + await app.workbench.console.waitForReadyAndRestarted('>'); await expect(app.workbench.console.consoleRestartButton).not.toBeVisible(); }).toPass(); }); @@ -32,8 +31,7 @@ test.describe('Console Pane: R', { await expect(async () => { await app.workbench.console.barClearButton.click(); await app.workbench.console.barRestartButton.click(); - await app.workbench.console.waitForReady('>'); - await app.workbench.console.waitForConsoleContents('restarted'); + await app.workbench.console.waitForReadyAndRestarted('>'); }).toPass(); }); diff --git a/test/e2e/tests/new-project-wizard/new-project-python.test.ts b/test/e2e/tests/new-project-wizard/new-project-python.test.ts index f7121f5d230..3a9019b6711 100644 --- a/test/e2e/tests/new-project-wizard/new-project-python.test.ts +++ b/test/e2e/tests/new-project-wizard/new-project-python.test.ts @@ -104,7 +104,7 @@ async function createNewProject(app: Application, options: CreateProjectOptions) async function verifyProjectCreation(app: Application, projectTitle: string) { await test.step(`Verify project created`, async () => { await expect(app.code.driver.page.getByLabel('Folder Commands')).toHaveText(projectTitle, { timeout: 60000 }); // this is really slow on windows CI for some reason - await app.workbench.console.waitForReady('>>>', 90000); + await app.workbench.console.waitForReadyAndStarted('>>>', 90000); }); } diff --git a/test/e2e/tests/r-pkg-development/r-pkg-development.test.ts b/test/e2e/tests/r-pkg-development/r-pkg-development.test.ts index 222fa9417f5..10fe860e620 100644 --- a/test/e2e/tests/r-pkg-development/r-pkg-development.test.ts +++ b/test/e2e/tests/r-pkg-development/r-pkg-development.test.ts @@ -36,7 +36,7 @@ test.describe('R Package Development', { tag: [tags.WEB, tags.R_PKG_DEVELOPMENT] await app.workbench.quickInput.clickOkOnQuickInput(); // Wait for the console to be ready - await app.workbench.console.waitForReady('>', 45000); + await app.workbench.console.waitForReadyAndStarted('>', 45000); }); await test.step('Test R Package', async () => { diff --git a/test/e2e/tests/reticulate/reticulate.test.ts b/test/e2e/tests/reticulate/reticulate.test.ts index d33bdbcc9af..5c1019cc24f 100644 --- a/test/e2e/tests/reticulate/reticulate.test.ts +++ b/test/e2e/tests/reticulate/reticulate.test.ts @@ -46,7 +46,7 @@ test.describe('Reticulate', { // Prompt did not appear } - await app.workbench.console.waitForReady('>>>'); + await app.workbench.console.waitForReadyAndStarted('>>>'); await verifyReticulateFunctionality(app, interpreter, false); @@ -73,7 +73,7 @@ test.describe('Reticulate', { await app.code.driver.page.locator('.positron-console').getByRole('button', { name: 'Restart R' }).click(); - await app.workbench.console.waitForReady('>'); + await app.workbench.console.waitForReadyAndStarted('>'); await app.code.driver.page.locator('.positron-console').locator('.action-bar-button-drop-down-arrow').click(); @@ -83,7 +83,7 @@ test.describe('Reticulate', { await app.code.driver.page.locator('.positron-console').getByRole('button', { name: 'Restart Python' }).click(); - await app.workbench.console.waitForReady('>>>'); + await app.workbench.console.waitForReadyAndStarted('>>>'); await verifyReticulateFunctionality(app, interpreter, sequential); diff --git a/test/e2e/tests/test-explorer/test-explorer.test.ts b/test/e2e/tests/test-explorer/test-explorer.test.ts index ed58436d3f6..acdcc8c56ba 100644 --- a/test/e2e/tests/test-explorer/test-explorer.test.ts +++ b/test/e2e/tests/test-explorer/test-explorer.test.ts @@ -36,7 +36,7 @@ test.describe('Test Explorer', { tag: [tags.TEST_EXPLORER, tags.WEB] }, () => { await app.workbench.quickInput.waitForQuickInputOpened(); await app.workbench.quickInput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing')); await app.workbench.quickInput.clickOkOnQuickInput(); - await app.workbench.console.waitForReady('>', 10000); + await app.workbench.console.waitForReadyAndStarted('>', 10000); }).toPass({ timeout: 50000 }); await app.workbench.testExplorer.clickTestExplorerIcon(); From 6b9f150c87cfcd03a3a66cac1cef5c11bde77c6f Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 15:41:10 -0600 Subject: [PATCH 08/21] parameterize test --- .../editor-action-bar-data-explorer.test.ts | 166 ++++-------------- .../editor-action-bar-documents.test.ts | 88 +--------- test/e2e/tests/editor-action-bar/helpers.ts | 92 +++++++++- test/e2e/tests/editor-action-bar/scripts.ts | 66 +++++++ 4 files changed, 197 insertions(+), 215 deletions(-) create mode 100644 test/e2e/tests/editor-action-bar/scripts.ts diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 03143f90bb4..1d40a814e46 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -4,8 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import { test, expect, tags } from '../_test.setup'; -import { Application } from '../../infra'; -import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; +import { verifyOpenInNewWindow, verifySplitEditor, verifySummaryPosition } from './helpers'; +import { pandasCsvScript, pandasParquetScript, polarsTsvScript, rScript } from './scripts'; + +const testCases = [ + { + name: 'Python Pandas (Parquet) - access via variables', + script: pandasParquetScript, + }, + { + name: 'Python Pandas (CSV Data) - access via variables', + script: pandasCsvScript, + }, { + name: 'Python Pandas - access via variables', + script: polarsTsvScript, + }, { + name: 'R - access via variables', + script: rScript, + }, +]; test.use({ suiteId: __filename @@ -21,132 +38,25 @@ test.describe('Editor Action Bar: Data Explorer', { test.afterEach(async function ({ app }) { await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + await app.workbench.quickaccess.runCommand('Console: Clear Console'); }); - test('Python Pandas (Parquet) - Variables Pane [C...]', async function ({ app, page, python }) { - // load data in data explorer - const title = 'Data: df'; - await app.workbench.console.executeCode('Python', parquetScript); - await app.workbench.variables.doubleClickVariableRow('df'); - await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); - - // verify action bar behavior - await verifySummaryPosition(app, 'Left'); - await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, title); - await verifyOpenInNewWindow(app, `${title} — qa-example-content`); - }); - - test('Python Pandas (CSV Data) - Variables Pane [C...]', async function ({ app, page, python }) { - // load data in data explorer - const title = 'Data: df'; - await app.workbench.console.executeCode('Python', csvScript); - await app.workbench.variables.doubleClickVariableRow('df'); - await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); - - // verify action bar behavior - await verifySummaryPosition(app, 'Left'); - await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, title); - await verifyOpenInNewWindow(app, `${title} — qa-example-content`); - }); - - test('Python Polars - Variables Pane [C...]', async function ({ app, page, openFile, python }) { - // load data in data explorer - const title = 'Data: df'; - await openFile('workspaces/polars-dataframe-py/polars_basic.py'); - await app.workbench.quickaccess.runCommand('python.execInConsole'); - await app.workbench.variables.doubleClickVariableRow('df'); - await page.getByRole('tab', { name: 'polars_basic.py' }).getByLabel('Close').click(); - await expect(page.getByText(title, { exact: true })).toBeVisible(); - - // verify action bar behavior - await verifySummaryPosition(app, 'Left'); - await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, title); - await verifyOpenInNewWindow(app, `${title} — qa-example-content`); - }); - - test('R - Variables Pane [C...]', async function ({ app, page, r }) { - // load data in data explorer - const title = 'Data: Data_Frame'; - await app.workbench.console.executeCode('R', rScript); - await app.workbench.variables.doubleClickVariableRow('Data_Frame'); - await expect(app.code.driver.page.getByText(title, { exact: true })).toBeVisible(); - - // verify action bar behavior - await verifySummaryPosition(app, 'Left'); - await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, title); - await verifyOpenInNewWindow(app, `${title} — qa-example-content`); - }); + for (const testCase of testCases) { + test(testCase.name, async function ({ app, page, interpreter }) { + // Set interpreter + const language = testCase.name.startsWith('Python') ? 'Python' : 'R'; + await interpreter.set(language); + + // View data in data explorer via variables pane + await app.workbench.console.executeCode(language, testCase.script); + await app.workbench.variables.doubleClickVariableRow('df'); + await expect(app.code.driver.page.getByText('Data: df', { exact: true })).toBeVisible(); + + // Verify action bar behavior + await verifySummaryPosition(app, 'Left'); + await verifySummaryPosition(app, 'Right'); + await verifySplitEditor(page, 'Data: df'); + await verifyOpenInNewWindow(app, 'Data: df — qa-example-content'); + }); + } }); - -async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { - const page = app.code.driver.page; - - await test.step(`Verify summary position: ${position}`, async () => { - // Toggle the summary position - if (app.web) { - await page.getByLabel('More actions', { exact: true }).click(); - await page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); - await page.keyboard.press('Enter'); - } - else { - await app.workbench.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); - } - - // Locator for the summary element - const summaryLocator = page.locator('div.column-summary').first(); - const tableLocator = page.locator('div.data-grid-column-headers'); - - // Ensure both the summary and table elements are visible - await Promise.all([ - expect(summaryLocator).toBeVisible(), - expect(tableLocator).toBeVisible(), - ]); - - // Get the bounding boxes for both elements - const summaryBox = await summaryLocator.boundingBox(); - const tableBox = await tableLocator.boundingBox(); - - // Validate bounding boxes are available - if (!summaryBox || !tableBox) { - throw new Error('Bounding boxes could not be retrieved for summary or table.'); - } - - // Validate positions based on the expected position - position === 'Left' - ? expect(summaryBox.x).toBeLessThan(tableBox.x) - : expect(summaryBox.x).toBeGreaterThan(tableBox.x); - }); -} - -const rScript = `Data_Frame <- data.frame ( - Training = c("Strength", "Stamina", "Other"), - Pulse = c(100, NA, 120), - Duration = c(60, 30, 45), - Note = c(NA, NA, "Note") -)`; - -const parquetScript = `import pandas as pd -import os - -file_path = os.path.join(os.getcwd(), 'data-files', '100x100', '100x100.parquet') - -# Read the Parquet file into a pandas DataFrame -df = pd.read_parquet(file_path) - -# Display the DataFrame -print(df)`; - -const csvScript = `import pandas as pd -import os - -file_path = os.path.join(os.getcwd(), 'data-files', 'spotify_data', 'data.csv') - -# Read the CSV file into a pandas DataFrame -df = pd.read_csv(file_path) - -# Display the DataFrame -print(df)`; diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts index 838b3607e68..e45c220e279 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts @@ -3,10 +3,8 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import { test, expect, tags } from '../_test.setup'; -import { Application } from '../../infra'; -import { Page } from '@playwright/test'; -import { verifyOpenInNewWindow, verifySplitEditor } from './helpers'; +import { test, tags } from '../_test.setup'; +import { verifyOpenChanges, verifyOpenInNewWindow, verifyOpenViewerRendersHtml, verifyPreviewRendersHtml, verifySplitEditor, verifyToggleBreadcrumb, verifyToggleLineNumbers } from './helpers'; test.use({ suiteId: __filename @@ -64,85 +62,3 @@ test.describe('Editor Action Bar: Documents', { }); }); - -async function clickCustomizeNotebookMenuItem(page, menuItem: string) { - const role = menuItem.includes('Line Numbers') ? 'menuitemcheckbox' : 'menuitem'; - const dropdownButton = page.getByLabel('Customize Notebook...'); - await dropdownButton.evaluate((button) => { - (button as HTMLElement).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); - }); - - const toggleMenuItem = page.getByRole(role, { name: menuItem }); - await toggleMenuItem.hover(); - await page.waitForTimeout(500); - await toggleMenuItem.click(); -} - -async function verifyLineNumbersVisibility(page, isVisible: boolean) { - for (const lineNum of [1, 2, 3, 4, 5]) { - const lineNumbers = expect(page.locator('.line-numbers').getByText(lineNum.toString(), { exact: true })); - isVisible ? await lineNumbers.toBeVisible() : await lineNumbers.not.toBeVisible(); - } -} - -async function verifyOpenChanges(page: Page) { - await test.step('verify "open changes" shows diff', async () => { - - // make change & save - await page.getByText('date', { exact: true }).click(); - await page.keyboard.press('X'); - await bindPlatformHotkey(page, 'S'); - - // click open changes & verify - await page.getByLabel('Open Changes').click(); - await expect(page.getByLabel('Revert Block')).toBeVisible(); - await expect(page.getByLabel('Stage Block')).toBeVisible(); - await page.getByRole('tab', { name: 'quarto_basic.qmd (Working' }).getByLabel('Close').click(); - - // undo changes & save - await bindPlatformHotkey(page, 'Z'); - await bindPlatformHotkey(page, 'S'); - }); -} - -async function bindPlatformHotkey(page: Page, key: string) { - await page.keyboard.press(process.platform === 'darwin' ? `Meta+${key}` : `Control+${key}`); -} - -async function verifyOpenViewerRendersHtml(app: Application) { - await test.step('verify "open in viewer" renders html', async () => { - await app.code.driver.page.getByLabel('Open in Viewer').click(); - const viewerFrame = app.code.driver.page.locator('iframe.webview').contentFrame().locator('#active-frame').contentFrame(); - const cellLocator = app.web - ? viewerFrame.frameLocator('iframe').getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }) - : viewerFrame.getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }); - - await expect(cellLocator).toBeVisible({ timeout: 30000 }); - }); -} - -async function verifyPreviewRendersHtml(app: Application, heading: string) { - await test.step('verify "preview" renders html', async () => { - await app.code.driver.page.getByLabel('Preview', { exact: true }).click(); - const viewerFrame = app.workbench.viewer.getViewerFrame().frameLocator('iframe'); - await expect(viewerFrame.getByRole('heading', { name: heading })).toBeVisible({ timeout: 30000 }); - }); -} - -async function verifyToggleLineNumbers(page: Page) { - await test.step('verify "customize notebook > toggle line numbers" (web only)', async () => { - await verifyLineNumbersVisibility(page, false); - await clickCustomizeNotebookMenuItem(page, 'Toggle Notebook Line Numbers'); - await verifyLineNumbersVisibility(page, true); - }); -} - -async function verifyToggleBreadcrumb(page: Page) { - await test.step('verify "customize notebook > toggle breadcrumbs" (web only)', async () => { - const breadcrumbs = page.locator('.monaco-breadcrumbs'); - - await expect(breadcrumbs).toBeVisible(); - await clickCustomizeNotebookMenuItem(page, 'Toggle Breadcrumbs'); - await expect(breadcrumbs).not.toBeVisible(); - }); -} diff --git a/test/e2e/tests/editor-action-bar/helpers.ts b/test/e2e/tests/editor-action-bar/helpers.ts index b9750cc5ce6..79deb05147c 100644 --- a/test/e2e/tests/editor-action-bar/helpers.ts +++ b/test/e2e/tests/editor-action-bar/helpers.ts @@ -3,9 +3,11 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import test, { expect } from '@playwright/test'; +import test, { expect, Page } from '@playwright/test'; import { Application } from '../../infra'; +// --- SHARED HELPERS --- + export async function verifySplitEditor(page, tabName: string) { await test.step(`Verify "split editor" opens another tab`, async () => { @@ -38,6 +40,9 @@ export async function verifyOpenInNewWindow(app: Application, expectedText: stri } } + +// --- editor-action-bar-data-explorer.test.ts HELPERS --- + export async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { const page = app.code.driver.page; @@ -77,3 +82,88 @@ export async function verifySummaryPosition(app: Application, position: 'Left' | : expect(summaryBox.x).toBeGreaterThan(tableBox.x); }); } + +// --- editor-action-bar-documents.test.ts HELPERS --- + + +export async function clickCustomizeNotebookMenuItem(page, menuItem: string) { + const role = menuItem.includes('Line Numbers') ? 'menuitemcheckbox' : 'menuitem'; + const dropdownButton = page.getByLabel('Customize Notebook...'); + await dropdownButton.evaluate((button) => { + (button as HTMLElement).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); + }); + + const toggleMenuItem = page.getByRole(role, { name: menuItem }); + await toggleMenuItem.hover(); + await page.waitForTimeout(500); + await toggleMenuItem.click(); +} + +export async function verifyLineNumbersVisibility(page, isVisible: boolean) { + for (const lineNum of [1, 2, 3, 4, 5]) { + const lineNumbers = expect(page.locator('.line-numbers').getByText(lineNum.toString(), { exact: true })); + isVisible ? await lineNumbers.toBeVisible() : await lineNumbers.not.toBeVisible(); + } +} + +export async function verifyOpenChanges(page: Page) { + await test.step('verify "open changes" shows diff', async () => { + + // make change & save + await page.getByText('date', { exact: true }).click(); + await page.keyboard.press('X'); + await bindPlatformHotkey(page, 'S'); + + // click open changes & verify + await page.getByLabel('Open Changes').click(); + await expect(page.getByLabel('Revert Block')).toBeVisible(); + await expect(page.getByLabel('Stage Block')).toBeVisible(); + await page.getByRole('tab', { name: 'quarto_basic.qmd (Working' }).getByLabel('Close').click(); + + // undo changes & save + await bindPlatformHotkey(page, 'Z'); + await bindPlatformHotkey(page, 'S'); + }); +} + +export async function bindPlatformHotkey(page: Page, key: string) { + await page.keyboard.press(process.platform === 'darwin' ? `Meta+${key}` : `Control+${key}`); +} + +export async function verifyOpenViewerRendersHtml(app: Application) { + await test.step('verify "open in viewer" renders html', async () => { + await app.code.driver.page.getByLabel('Open in Viewer').click(); + const viewerFrame = app.code.driver.page.locator('iframe.webview').contentFrame().locator('#active-frame').contentFrame(); + const cellLocator = app.web + ? viewerFrame.frameLocator('iframe').getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }) + : viewerFrame.getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }); + + await expect(cellLocator).toBeVisible({ timeout: 30000 }); + }); +} + +export async function verifyPreviewRendersHtml(app: Application, heading: string) { + await test.step('verify "preview" renders html', async () => { + await app.code.driver.page.getByLabel('Preview', { exact: true }).click(); + const viewerFrame = app.workbench.viewer.getViewerFrame().frameLocator('iframe'); + await expect(viewerFrame.getByRole('heading', { name: heading })).toBeVisible({ timeout: 30000 }); + }); +} + +export async function verifyToggleLineNumbers(page: Page) { + await test.step('verify "customize notebook > toggle line numbers" (web only)', async () => { + await verifyLineNumbersVisibility(page, false); + await clickCustomizeNotebookMenuItem(page, 'Toggle Notebook Line Numbers'); + await verifyLineNumbersVisibility(page, true); + }); +} + +export async function verifyToggleBreadcrumb(page: Page) { + await test.step('verify "customize notebook > toggle breadcrumbs" (web only)', async () => { + const breadcrumbs = page.locator('.monaco-breadcrumbs'); + + await expect(breadcrumbs).toBeVisible(); + await clickCustomizeNotebookMenuItem(page, 'Toggle Breadcrumbs'); + await expect(breadcrumbs).not.toBeVisible(); + }); +} diff --git a/test/e2e/tests/editor-action-bar/scripts.ts b/test/e2e/tests/editor-action-bar/scripts.ts new file mode 100644 index 00000000000..83327068ad9 --- /dev/null +++ b/test/e2e/tests/editor-action-bar/scripts.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Script to load a Parquet file using Python Pandas. + * Reads a Parquet file located in the `data-files/100x100` directory + * and loads it into a Pandas DataFrame. + */ +export const pandasParquetScript = `import pandas as pd +import os + +file_path = os.path.join(os.getcwd(), 'data-files', '100x100', '100x100.parquet') + +# Read the Parquet file into a pandas DataFrame +df = pd.read_parquet(file_path) + +# Display the DataFrame +print(df)`; + +/** + * Script to load a CSV file using Python Pandas. + * Reads a CSV file located in the `data-files/spotify_data` directory + * and loads it into a Pandas DataFrame. + */ +export const pandasCsvScript = `import pandas as pd +import os + +file_path = os.path.join(os.getcwd(), 'data-files', 'spotify_data', 'data.csv') + +# Read the CSV file into a pandas DataFrame +df = pd.read_csv(file_path) + +# Display the DataFrame +print(df)`; + +/** + * Script to load a TSV file using Python Polars. + * Reads a TSV file located in the `data-files/100x100` directory, + * converts it from a Pandas DataFrame to a Polars DataFrame, + * and displays the resulting data. + */ +export const polarsTsvScript = `import pandas as pd +import polars as pl +import os + +file_path = os.path.join(os.getcwd(), 'data-files', '100x100', 'polars-100x100.tsv') + +pandas_dataframe = pd.read_csv(file_path, delimiter='\\t') + +# Convert to Polars DataFrame +df = pl.from_pandas(pandas_dataframe) + +# Display the DataFrame +print(df)`; + +/** + * Script to create a sample data frame in R. + */ +export const rScript = `df <- data.frame ( + Training = c("Strength", "Stamina", "Other"), + Pulse = c(100, NA, 120), + Duration = c(60, 30, 45), + Note = c(NA, NA, "Note") +)`; From d96f42e891b7b0ec18a41b6fbacae28320ea3705 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 15:50:07 -0600 Subject: [PATCH 09/21] update scripts --- .../editor-action-bar-data-explorer.test.ts | 2 +- test/e2e/tests/editor-action-bar/scripts.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 1d40a814e46..bd9f249c169 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -16,7 +16,7 @@ const testCases = [ name: 'Python Pandas (CSV Data) - access via variables', script: pandasCsvScript, }, { - name: 'Python Pandas - access via variables', + name: 'Python Polars - access via variables', script: polarsTsvScript, }, { name: 'R - access via variables', diff --git a/test/e2e/tests/editor-action-bar/scripts.ts b/test/e2e/tests/editor-action-bar/scripts.ts index 83327068ad9..23e8a75a2d1 100644 --- a/test/e2e/tests/editor-action-bar/scripts.ts +++ b/test/e2e/tests/editor-action-bar/scripts.ts @@ -37,20 +37,16 @@ print(df)`; /** * Script to load a TSV file using Python Polars. - * Reads a TSV file located in the `data-files/100x100` directory, - * converts it from a Pandas DataFrame to a Polars DataFrame, - * and displays the resulting data. + * Reads a TSV file located in the `data-files/100x100` directory + * and loads it into a Polars DataFrame. */ -export const polarsTsvScript = `import pandas as pd -import polars as pl +export const polarsTsvScript = `import polars as pl import os file_path = os.path.join(os.getcwd(), 'data-files', '100x100', 'polars-100x100.tsv') -pandas_dataframe = pd.read_csv(file_path, delimiter='\\t') - -# Convert to Polars DataFrame -df = pl.from_pandas(pandas_dataframe) +# Read the TSV file directly into a Polars DataFrame +df = pl.read_csv(file_path, separator='\t', ignore_errors=True) # Display the DataFrame print(df)`; From 3ab2329e4a65c5b1fc78be83d0faa724180b0783 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 15:54:39 -0600 Subject: [PATCH 10/21] nit --- .../editor-action-bar-data-explorer.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index bd9f249c169..d7f74c1dc74 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -9,17 +9,17 @@ import { pandasCsvScript, pandasParquetScript, polarsTsvScript, rScript } from ' const testCases = [ { - name: 'Python Pandas (Parquet) - access via variables', + title: 'Python Pandas (Parquet) - access via variables', script: pandasParquetScript, }, { - name: 'Python Pandas (CSV Data) - access via variables', + title: 'Python Pandas (CSV Data) - access via variables', script: pandasCsvScript, }, { - name: 'Python Polars - access via variables', + title: 'Python Polars - access via variables', script: polarsTsvScript, }, { - name: 'R - access via variables', + title: 'R - access via variables', script: rScript, }, ]; @@ -42,9 +42,9 @@ test.describe('Editor Action Bar: Data Explorer', { }); for (const testCase of testCases) { - test(testCase.name, async function ({ app, page, interpreter }) { + test(testCase.title, async function ({ app, page, interpreter }) { // Set interpreter - const language = testCase.name.startsWith('Python') ? 'Python' : 'R'; + const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; await interpreter.set(language); // View data in data explorer via variables pane From ba42338b49c84d26e2113cfe76a92f4b76d4b237 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 16:09:58 -0600 Subject: [PATCH 11/21] better split editor verification --- test/e2e/tests/editor-action-bar/helpers.ts | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/helpers.ts b/test/e2e/tests/editor-action-bar/helpers.ts index 79deb05147c..6b40cab980b 100644 --- a/test/e2e/tests/editor-action-bar/helpers.ts +++ b/test/e2e/tests/editor-action-bar/helpers.ts @@ -9,14 +9,23 @@ import { Application } from '../../infra'; // --- SHARED HELPERS --- export async function verifySplitEditor(page, tabName: string) { - await test.step(`Verify "split editor" opens another tab`, async () => { - + await test.step(`Verify "split editor" opens another tab and ensure tabs are correctly aligned`, async () => { // Split editor right await page.getByLabel('Split Editor Right', { exact: true }).click(); await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + // Verify tabs are on the same X plane + const rightSplitTabs = page.getByRole('tab', { name: tabName }); + const firstTabBox = await rightSplitTabs.nth(0).boundingBox(); + const secondTabBox = await rightSplitTabs.nth(1).boundingBox(); + + expect(firstTabBox).not.toBeNull(); + expect(secondTabBox).not.toBeNull(); + expect(firstTabBox!.y).toBeCloseTo(secondTabBox!.y, 1); + expect(firstTabBox!.x).not.toBeCloseTo(secondTabBox!.x, 1); + // Close one tab - await page.getByRole('tab', { name: tabName }).getByLabel('Close').first().click(); + await rightSplitTabs.first().getByLabel('Close').click(); // Split editor down await page.keyboard.down('Alt'); @@ -24,6 +33,15 @@ export async function verifySplitEditor(page, tabName: string) { await page.keyboard.up('Alt'); await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); + // Verify tabs are on the same Y plane + const downSplitTabs = page.getByRole('tab', { name: tabName }); + const firstDownTabBox = await downSplitTabs.nth(0).boundingBox(); + const secondDownTabBox = await downSplitTabs.nth(1).boundingBox(); + + expect(firstDownTabBox).not.toBeNull(); + expect(secondDownTabBox).not.toBeNull(); + expect(firstDownTabBox!.x).toBeCloseTo(secondDownTabBox!.x, 1); + expect(firstDownTabBox!.y).not.toBeCloseTo(secondDownTabBox!.y, 1); }); } From f169a75ad45a87502b7a4b75164978e8de7e13c7 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Thu, 16 Jan 2025 18:02:16 -0600 Subject: [PATCH 12/21] direct open --- .../editor-action-bar-data-explorer.test.ts | 31 ++++++++++++------- test/e2e/tests/editor-action-bar/scripts.ts | 16 ---------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index d7f74c1dc74..80d2aca0558 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -5,24 +5,25 @@ import { test, expect, tags } from '../_test.setup'; import { verifyOpenInNewWindow, verifySplitEditor, verifySummaryPosition } from './helpers'; -import { pandasCsvScript, pandasParquetScript, polarsTsvScript, rScript } from './scripts'; +import { pandasCsvScript, pandasParquetScript, rScript } from './scripts'; const testCases = [ { - title: 'Python Pandas (Parquet) - access via variables', + title: 'R - access via variables', + script: rScript, + }, + { + title: 'Parquet - access via variables', script: pandasParquetScript, }, { - title: 'Python Pandas (CSV Data) - access via variables', + title: 'CSV - access via variables', script: pandasCsvScript, - }, { - title: 'Python Polars - access via variables', - script: polarsTsvScript, - }, { - title: 'R - access via variables', - script: rScript, }, -]; + { + title: 'Parquet - access via DuckDB', + openFile: 'data-files/100x100/100x100.parquet', + }]; test.use({ suiteId: __filename @@ -47,8 +48,14 @@ test.describe('Editor Action Bar: Data Explorer', { const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; await interpreter.set(language); - // View data in data explorer via variables pane - await app.workbench.console.executeCode(language, testCase.script); + // Execute script or open file + if (testCase.script) { + await app.workbench.console.executeCode(language, testCase.script); + } else if (testCase.openFile) { + await app.workbench.quickaccess.openFile(testCase.openFile); + } + + // Open data explorer await app.workbench.variables.doubleClickVariableRow('df'); await expect(app.code.driver.page.getByText('Data: df', { exact: true })).toBeVisible(); diff --git a/test/e2e/tests/editor-action-bar/scripts.ts b/test/e2e/tests/editor-action-bar/scripts.ts index 23e8a75a2d1..e9973c4105b 100644 --- a/test/e2e/tests/editor-action-bar/scripts.ts +++ b/test/e2e/tests/editor-action-bar/scripts.ts @@ -35,22 +35,6 @@ df = pd.read_csv(file_path) # Display the DataFrame print(df)`; -/** - * Script to load a TSV file using Python Polars. - * Reads a TSV file located in the `data-files/100x100` directory - * and loads it into a Polars DataFrame. - */ -export const polarsTsvScript = `import polars as pl -import os - -file_path = os.path.join(os.getcwd(), 'data-files', '100x100', 'polars-100x100.tsv') - -# Read the TSV file directly into a Polars DataFrame -df = pl.read_csv(file_path, separator='\t', ignore_errors=True) - -# Display the DataFrame -print(df)`; - /** * Script to create a sample data frame in R. */ From 44be05f77cbe2a92274e7de1d474cbace22c92af Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 06:00:21 -0600 Subject: [PATCH 13/21] remove csv test --- .../editor-action-bar-data-explorer.test.ts | 42 +++++++++---------- test/e2e/tests/editor-action-bar/scripts.ts | 33 +++++---------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 80d2aca0558..3e32bad8043 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -5,24 +5,25 @@ import { test, expect, tags } from '../_test.setup'; import { verifyOpenInNewWindow, verifySplitEditor, verifySummaryPosition } from './helpers'; -import { pandasCsvScript, pandasParquetScript, rScript } from './scripts'; +import { pandasDataFrame, rDataFrame } from './scripts'; const testCases = [ { - title: 'R - access via variables', - script: rScript, + title: 'R - Load data frame via variables pane', + script: rDataFrame, + variable: 'df', + tabTitle: 'Data: df', }, { - title: 'Parquet - access via variables', - script: pandasParquetScript, + title: 'Python - Load data frame via variables pane', + script: pandasDataFrame, + variable: 'df', + tabTitle: 'Data: df', }, { - title: 'CSV - access via variables', - script: pandasCsvScript, - }, - { - title: 'Parquet - access via DuckDB', - openFile: 'data-files/100x100/100x100.parquet', + title: 'Python - Open parquet file via DuckDB into data explorer', + openDataFile: 'data-files/100x100/100x100.parquet', + tabTitle: 'Data: 100x100.parquet', }]; test.use({ @@ -43,27 +44,26 @@ test.describe('Editor Action Bar: Data Explorer', { }); for (const testCase of testCases) { - test(testCase.title, async function ({ app, page, interpreter }) { + test(testCase.title, async function ({ app, page, interpreter, openDataFile }) { // Set interpreter const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; await interpreter.set(language); - // Execute script or open file if (testCase.script) { + // Execute script and open via variables pane await app.workbench.console.executeCode(language, testCase.script); - } else if (testCase.openFile) { - await app.workbench.quickaccess.openFile(testCase.openFile); + await app.workbench.variables.doubleClickVariableRow(testCase.variable); + await expect(app.code.driver.page.getByText(testCase.tabTitle, { exact: true })).toBeVisible(); + } else if (testCase.openDataFile) { + // Open fila directly with duck db + await openDataFile(testCase.openDataFile!); } - // Open data explorer - await app.workbench.variables.doubleClickVariableRow('df'); - await expect(app.code.driver.page.getByText('Data: df', { exact: true })).toBeVisible(); - // Verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, 'Data: df'); - await verifyOpenInNewWindow(app, 'Data: df — qa-example-content'); + await verifySplitEditor(page, testCase.tabTitle); + await verifyOpenInNewWindow(app, `${testCase.tabTitle} — qa-example-content`); }); } }); diff --git a/test/e2e/tests/editor-action-bar/scripts.ts b/test/e2e/tests/editor-action-bar/scripts.ts index e9973c4105b..f5bee464d88 100644 --- a/test/e2e/tests/editor-action-bar/scripts.ts +++ b/test/e2e/tests/editor-action-bar/scripts.ts @@ -3,34 +3,21 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -/** - * Script to load a Parquet file using Python Pandas. - * Reads a Parquet file located in the `data-files/100x100` directory - * and loads it into a Pandas DataFrame. - */ -export const pandasParquetScript = `import pandas as pd -import os - -file_path = os.path.join(os.getcwd(), 'data-files', '100x100', '100x100.parquet') - -# Read the Parquet file into a pandas DataFrame -df = pd.read_parquet(file_path) - -# Display the DataFrame -print(df)`; - /** * Script to load a CSV file using Python Pandas. * Reads a CSV file located in the `data-files/spotify_data` directory * and loads it into a Pandas DataFrame. */ -export const pandasCsvScript = `import pandas as pd -import os - -file_path = os.path.join(os.getcwd(), 'data-files', 'spotify_data', 'data.csv') +export const pandasDataFrame = `import pandas as pd +import numpy as np -# Read the CSV file into a pandas DataFrame -df = pd.read_csv(file_path) +# Create the DataFrame +df = pd.DataFrame({ + 'Training': ['Strength', 'Stamina', 'Other'], + 'Pulse': [100, np.nan, 120], # Use np.nan for missing values + 'Duration': [60, 30, 45], + 'Note': [np.nan, np.nan, 'Note'] # Use np.nan for missing values +}) # Display the DataFrame print(df)`; @@ -38,7 +25,7 @@ print(df)`; /** * Script to create a sample data frame in R. */ -export const rScript = `df <- data.frame ( +export const rDataFrame = `df <- data.frame ( Training = c("Strength", "Stamina", "Other"), Pulse = c(100, NA, 120), Duration = c(60, 30, 45), From 9ed4b6fa975360631dd7227ae1d5a36598f8a444 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 06:06:54 -0600 Subject: [PATCH 14/21] nit --- test/e2e/tests/editor-action-bar/helpers.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/helpers.ts b/test/e2e/tests/editor-action-bar/helpers.ts index 6b40cab980b..8a303812b32 100644 --- a/test/e2e/tests/editor-action-bar/helpers.ts +++ b/test/e2e/tests/editor-action-bar/helpers.ts @@ -9,7 +9,7 @@ import { Application } from '../../infra'; // --- SHARED HELPERS --- export async function verifySplitEditor(page, tabName: string) { - await test.step(`Verify "split editor" opens another tab and ensure tabs are correctly aligned`, async () => { + await test.step(`Verify "split editor" behavior`, async () => { // Split editor right await page.getByLabel('Split Editor Right', { exact: true }).click(); await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); @@ -75,15 +75,13 @@ export async function verifySummaryPosition(app: Application, position: 'Left' | await app.workbench.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); } - // Locator for the summary element + // Get the summary and table locators. const summaryLocator = page.locator('div.column-summary').first(); const tableLocator = page.locator('div.data-grid-column-headers'); // Ensure both the summary and table elements are visible - await Promise.all([ - expect(summaryLocator).toBeVisible(), - expect(tableLocator).toBeVisible(), - ]); + await expect(summaryLocator).toBeVisible(); + await expect(tableLocator).toBeVisible(); // Get the bounding boxes for both elements const summaryBox = await summaryLocator.boundingBox(); From 7cb1f60f1e2b57f9678cd0d081965bac5dbc2067 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 06:45:27 -0600 Subject: [PATCH 15/21] replace script with files --- test/e2e/pages/editor.ts | 3 +- .../editor-action-bar-data-explorer.test.ts | 31 +++++++++-------- test/e2e/tests/editor-action-bar/scripts.ts | 33 ------------------- 3 files changed, 20 insertions(+), 47 deletions(-) delete mode 100644 test/e2e/tests/editor-action-bar/scripts.ts diff --git a/test/e2e/pages/editor.ts b/test/e2e/pages/editor.ts index 050337a95d0..f67630fb9c7 100644 --- a/test/e2e/pages/editor.ts +++ b/test/e2e/pages/editor.ts @@ -6,7 +6,7 @@ import { expect, FrameLocator, Locator } from '@playwright/test'; import { Code } from '../infra/code'; -// currently a dupe of declaration in ../editor.ts but trying not to modifiy that file +// currently a dupe of declaration in ../editor.ts but trying not to modify that file const EDITOR = (filename: string) => `.monaco-editor[data-uri$="${filename}"]`; const CURRENT_LINE = '.view-overlays .current-line'; const PLAY_BUTTON = '.codicon-play'; @@ -17,6 +17,7 @@ const INNER_FRAME = '#active-frame'; export class Editor { viewerFrame = this.code.driver.page.frameLocator(OUTER_FRAME).frameLocator(INNER_FRAME); + playButton = this.code.driver.page.locator(PLAY_BUTTON); getEditorViewerLocator(locator: string,): Locator { return this.viewerFrame.locator(locator); diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 3e32bad8043..0ec06925855 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -5,23 +5,22 @@ import { test, expect, tags } from '../_test.setup'; import { verifyOpenInNewWindow, verifySplitEditor, verifySummaryPosition } from './helpers'; -import { pandasDataFrame, rDataFrame } from './scripts'; const testCases = [ { title: 'R - Load data frame via variables pane', - script: rDataFrame, + openFile: 'workspaces/generate-data-frames-r/simple-data-frames.r', variable: 'df', tabTitle: 'Data: df', }, { title: 'Python - Load data frame via variables pane', - script: pandasDataFrame, + openFile: 'workspaces/generate-data-frames-py/simple-data-frames.py', variable: 'df', tabTitle: 'Data: df', }, { - title: 'Python - Open parquet file via DuckDB into data explorer', + title: 'Python - Open parquet file via DuckDB', openDataFile: 'data-files/100x100/100x100.parquet', tabTitle: 'Data: 100x100.parquet', }]; @@ -44,19 +43,25 @@ test.describe('Editor Action Bar: Data Explorer', { }); for (const testCase of testCases) { - test(testCase.title, async function ({ app, page, interpreter, openDataFile }) { + test(testCase.title, async function ({ app, page, interpreter, openDataFile, openFile }) { // Set interpreter const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; await interpreter.set(language); - if (testCase.script) { - // Execute script and open via variables pane - await app.workbench.console.executeCode(language, testCase.script); - await app.workbench.variables.doubleClickVariableRow(testCase.variable); - await expect(app.code.driver.page.getByText(testCase.tabTitle, { exact: true })).toBeVisible(); - } else if (testCase.openDataFile) { - // Open fila directly with duck db - await openDataFile(testCase.openDataFile!); + // Open file + testCase.openFile + ? await openFile(testCase.openFile) + : await openDataFile(testCase.openDataFile!); + + // Open data explorer via variable pane + if (testCase.variable) { + await test.step('Open data explorer via variable pane', async () => { + await app.workbench.editor.playButton.click(); + await app.workbench.variables.doubleClickVariableRow(testCase.variable); + await app.code.driver.page.getByRole('tablist').locator('.tab').first().click(); + await app.code.driver.page.getByLabel('Close').first().click(); + await expect(app.code.driver.page.getByText(testCase.tabTitle, { exact: true })).toBeVisible(); + }); } // Verify action bar behavior diff --git a/test/e2e/tests/editor-action-bar/scripts.ts b/test/e2e/tests/editor-action-bar/scripts.ts deleted file mode 100644 index f5bee464d88..00000000000 --- a/test/e2e/tests/editor-action-bar/scripts.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Script to load a CSV file using Python Pandas. - * Reads a CSV file located in the `data-files/spotify_data` directory - * and loads it into a Pandas DataFrame. - */ -export const pandasDataFrame = `import pandas as pd -import numpy as np - -# Create the DataFrame -df = pd.DataFrame({ - 'Training': ['Strength', 'Stamina', 'Other'], - 'Pulse': [100, np.nan, 120], # Use np.nan for missing values - 'Duration': [60, 30, 45], - 'Note': [np.nan, np.nan, 'Note'] # Use np.nan for missing values -}) - -# Display the DataFrame -print(df)`; - -/** - * Script to create a sample data frame in R. - */ -export const rDataFrame = `df <- data.frame ( - Training = c("Strength", "Stamina", "Other"), - Pulse = c(100, NA, 120), - Duration = c(60, 30, 45), - Note = c(NA, NA, "Note") -)`; From 8aaee99bcae44b27349fb7747b5f507d18020602 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 07:03:12 -0600 Subject: [PATCH 16/21] tabName --- .../editor-action-bar-data-explorer.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index 0ec06925855..dd1b95038a7 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -11,18 +11,18 @@ const testCases = [ title: 'R - Load data frame via variables pane', openFile: 'workspaces/generate-data-frames-r/simple-data-frames.r', variable: 'df', - tabTitle: 'Data: df', + tabName: 'Data: df', }, { title: 'Python - Load data frame via variables pane', openFile: 'workspaces/generate-data-frames-py/simple-data-frames.py', variable: 'df', - tabTitle: 'Data: df', + tabName: 'Data: df', }, { title: 'Python - Open parquet file via DuckDB', openDataFile: 'data-files/100x100/100x100.parquet', - tabTitle: 'Data: 100x100.parquet', + tabName: 'Data: 100x100.parquet', }]; test.use({ @@ -60,15 +60,15 @@ test.describe('Editor Action Bar: Data Explorer', { await app.workbench.variables.doubleClickVariableRow(testCase.variable); await app.code.driver.page.getByRole('tablist').locator('.tab').first().click(); await app.code.driver.page.getByLabel('Close').first().click(); - await expect(app.code.driver.page.getByText(testCase.tabTitle, { exact: true })).toBeVisible(); + await expect(app.code.driver.page.getByText(testCase.tabName, { exact: true })).toBeVisible(); }); } // Verify action bar behavior await verifySummaryPosition(app, 'Left'); await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, testCase.tabTitle); - await verifyOpenInNewWindow(app, `${testCase.tabTitle} — qa-example-content`); + await verifySplitEditor(page, testCase.tabName); + await verifyOpenInNewWindow(app, `${testCase.tabName} — qa-example-content`); }); } }); From 56fb452f173a8e13c569017e1747311cf371bfab Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 10:16:57 -0600 Subject: [PATCH 17/21] cleanup --- test/e2e/infra/workbench.ts | 3 + test/e2e/pages/editorActionBar.ts | 182 +++++++++++++++++ test/e2e/tests/_test.setup.ts | 23 ++- .../editor-action-bar-data-explorer.test.ts | 52 +++-- .../editor-action-bar-documents.test.ts | 101 ++++++++-- test/e2e/tests/editor-action-bar/helpers.ts | 185 ------------------ 6 files changed, 328 insertions(+), 218 deletions(-) create mode 100644 test/e2e/pages/editorActionBar.ts delete mode 100644 test/e2e/tests/editor-action-bar/helpers.ts diff --git a/test/e2e/infra/workbench.ts b/test/e2e/infra/workbench.ts index 04584da65f0..33e5a5aca76 100644 --- a/test/e2e/infra/workbench.ts +++ b/test/e2e/infra/workbench.ts @@ -31,6 +31,7 @@ import { Clipboard } from '../pages/clipboard'; import { QuickInput } from '../pages/quickInput'; import { Extensions } from '../pages/extensions'; import { Settings } from '../pages/settings'; +import { EditorActionBar } from '../pages/editorActionBar'; export interface Commands { runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise; @@ -65,6 +66,7 @@ export class Workbench { readonly extensions: Extensions; readonly editors: Editors; readonly settings: Settings; + readonly editorActionBar: EditorActionBar; constructor(code: Code) { @@ -96,6 +98,7 @@ export class Workbench { this.clipboard = new Clipboard(code); this.extensions = new Extensions(code, this.quickaccess); this.settings = new Settings(code, this.editors, this.editor, this.quickaccess); + this.editorActionBar = new EditorActionBar(code.driver.page, this.viewer, this.quickaccess); } } diff --git a/test/e2e/pages/editorActionBar.ts b/test/e2e/pages/editorActionBar.ts new file mode 100644 index 00000000000..15366c21e4e --- /dev/null +++ b/test/e2e/pages/editorActionBar.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import test, { expect, Page } from '@playwright/test'; +import { Viewer } from './viewer'; +import { QuickAccess } from './quickaccess'; + + +export class EditorActionBar { + + previewButton = this.page.getByLabel('Preview', { exact: true }); + openChangesButton = this.page.getByLabel('Open Changes'); + splitEditorRightButton = this.page.getByLabel('Split Editor Right', { exact: true }); + splitEditorDownButton = this.page.getByLabel('Split Editor Down', { exact: true }); + openInViewerButton = this.page.getByLabel('Open in Viewer'); + + constructor(private page: Page, private viewer: Viewer, private quickaccess: QuickAccess) { + } + + // --- Actions --- + + /** + * Action: Click the "Split Editor" button. Handles pressing the 'Alt' key for 'down' direction. + * @param direction 'down' or 'right' + */ + async clickSplitEditorButton(direction: 'down' | 'right') { + if (direction === 'down') { + await this.page.keyboard.down('Alt'); + await this.page.getByLabel('Split Editor Down').click(); + await this.page.keyboard.up('Alt'); + } + else { + this.splitEditorRightButton.click(); + } + } + + /** + * Action: Set the summary position to the specified side. + * @param isWeb whether the test is running in the web or desktop app + * @param position select 'Left' or 'Right' to position the summary + */ + async selectSummaryOn(isWeb: boolean, position: 'Left' | 'Right') { + if (isWeb) { + await this.page.getByLabel('More actions', { exact: true }).click(); + await this.page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); + await this.page.keyboard.press('Enter'); + } + else { + await this.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); + } + } + + /** + * Action: Click a menu item in the "Customize Notebook" dropdown. + * @param menuItem a menu item to click in the "Customize Notebook" dropdown + */ + async clickCustomizeNotebookMenuItem(menuItem: string) { + const role = menuItem.includes('Line Numbers') ? 'menuitemcheckbox' : 'menuitem'; + const dropdownButton = this.page.getByLabel('Customize Notebook...'); + await dropdownButton.evaluate((button) => { + (button as HTMLElement).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); + }); + + const toggleMenuItem = this.page.getByRole(role, { name: menuItem }); + await toggleMenuItem.hover(); + await this.page.waitForTimeout(500); + await toggleMenuItem.click(); + } + + // --- Verifications --- + + /** + * Verify: Check that the editor is split in the specified direction (on the correct plane) + * @param direction the direction the editor was split + * @param tabName the name of the tab to verify + */ + async verifySplitEditor(direction: 'down' | 'right', tabName: string,) { + await test.step(`Verify split editor: ${direction}`, async () => { + // Verify 2 tabs + await expect(this.page.getByRole('tab', { name: tabName })).toHaveCount(2); + const splitTabs = this.page.getByRole('tab', { name: tabName }); + const firstTabBox = await splitTabs.nth(0).boundingBox(); + const secondTabBox = await splitTabs.nth(1).boundingBox(); + + if (direction === 'right') { + // Verify tabs are on the same X plane + expect(firstTabBox).not.toBeNull(); + expect(secondTabBox).not.toBeNull(); + expect(firstTabBox!.y).toBeCloseTo(secondTabBox!.y, 1); + expect(firstTabBox!.x).not.toBeCloseTo(secondTabBox!.x, 1); + } + else { + // Verify tabs are on the same Y plane + expect(firstTabBox).not.toBeNull(); + expect(secondTabBox).not.toBeNull(); + expect(firstTabBox!.x).toBeCloseTo(secondTabBox!.x, 1); + expect(firstTabBox!.y).not.toBeCloseTo(secondTabBox!.y, 1); + } + + // Close one tab + await splitTabs.first().getByLabel('Close').click(); + }); + } + + /** + * Verify: Check that the "open in new window" contains the specified title + * @param isWeb whether the test is running in the web or desktop app + * @param windowTitle the title to verify in the new window + */ + async verifyOpenInNewWindow(isWeb: boolean, windowTitle: string) { + if (!isWeb) { + await test.step(`Verify "open new window" contains: ${windowTitle}`, async () => { + const [newPage] = await Promise.all([ + this.page.context().waitForEvent('page'), + this.page.getByLabel('Move into new window').first().click(), + ]); + await newPage.waitForLoadState(); + await expect(newPage.getByText(windowTitle)).toBeVisible(); + }); + } + } + + /** + * Verify: Check that the preview renders the specified heading + * @param heading the heading to verify in the preview + */ + async verifyPreviewRendersHtml(heading: string) { + await test.step('Verify "preview" renders html', async () => { + await this.page.getByLabel('Preview', { exact: true }).click(); + const viewerFrame = this.viewer.getViewerFrame().frameLocator('iframe'); + await expect(viewerFrame.getByRole('heading', { name: heading })).toBeVisible({ timeout: 30000 }); + }); + } + + /** + * Verify: Check that the "open in viewer" renders the specified title + * @param isWeb whether the test is running in the web or desktop app + * @param title the title to verify in the viewer + */ + async verifyOpenViewerRendersHtml(isWeb: boolean, title: string) { + await test.step('verify "open in viewer" renders html', async () => { + const viewerFrame = this.page.locator('iframe.webview').contentFrame().locator('#active-frame').contentFrame(); + const cellLocator = isWeb + ? viewerFrame.frameLocator('iframe').getByRole('cell', { name: title }) + : viewerFrame.getByRole('cell', { name: title }); + + await expect(cellLocator).toBeVisible({ timeout: 30000 }); + }); + } + + /** + * Verify: Check that the summary is positioned on the specified side + * @param position the side to verify the summary is positioned + */ + async verifySummaryPosition(position: 'Left' | 'Right') { + await test.step(`Verify summary position: ${position}`, async () => { + // Get the summary and table locators. + const summaryLocator = this.page.locator('div.column-summary').first(); + const tableLocator = this.page.locator('div.data-grid-column-headers'); + + // Ensure both the summary and table elements are visible + await expect(summaryLocator).toBeVisible(); + await expect(tableLocator).toBeVisible(); + + // Get the bounding boxes for both elements + const summaryBox = await summaryLocator.boundingBox(); + const tableBox = await tableLocator.boundingBox(); + + // Validate bounding boxes are available + if (!summaryBox || !tableBox) { + throw new Error('Bounding boxes could not be retrieved for summary or table.'); + } + + // Validate positions based on the expected position + position === 'Left' + ? expect(summaryBox.x).toBeLessThan(tableBox.x) + : expect(summaryBox.x).toBeGreaterThan(tableBox.x); + }); + } +} diff --git a/test/e2e/tests/_test.setup.ts b/test/e2e/tests/_test.setup.ts index 2502b3a7328..725132493bf 100644 --- a/test/e2e/tests/_test.setup.ts +++ b/test/e2e/tests/_test.setup.ts @@ -137,6 +137,8 @@ export const test = base.extend({ }, { scope: 'test' }], + // ex: await packages.manage('ipykernel', 'install'); + // ex: await packages.manage('renv', 'uninstall'); packages: [async ({ app }, use) => { const packageManager = new PackageManager(app); await use(packageManager); @@ -147,20 +149,32 @@ export const test = base.extend({ await use(); }, { scope: 'test' }], - // example usage: await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); + // ex: await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); openFile: async ({ app }, use) => { await use(async (filePath: string) => { - await app.workbench.quickaccess.openFile(path.join(app.workspacePathOrFolder, filePath)); + await test.step(`Open file: ${path.basename(filePath)}`, async () => { + await app.workbench.quickaccess.openFile(path.join(app.workspacePathOrFolder, filePath)); + }); }); }, - // example usage: await openDataFile(app, 'workspaces/large_r_notebook/spotify.ipynb'); + // ex: await openDataFile(app, 'workspaces/large_r_notebook/spotify.ipynb'); openDataFile: async ({ app }, use) => { await use(async (filePath: string) => { - await app.workbench.quickaccess.openDataFile(path.join(app.workspacePathOrFolder, filePath)); + await test.step(`Open data file: ${path.basename(filePath)}`, async () => { + await app.workbench.quickaccess.openDataFile(path.join(app.workspacePathOrFolder, filePath)); + }); }); }, + // ex: await runCommand('workbench.action.files.save'); + runCommand: async ({ app }, use) => { + await use(async (command: string) => { + await app.workbench.quickaccess.runCommand(command); + }); + }, + + // ex: await userSettings.set([['editor.actionBar.enabled', 'true']], false); userSettings: [async ({ app }, use) => { const userSettings = new UserSettingsFixtures(app); @@ -354,6 +368,7 @@ interface TestFixtures { devTools: void; openFile: (filePath: string) => Promise; openDataFile: (filePath: string) => Promise; + runCommand: (command: string) => Promise; } interface WorkerFixtures { diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts index dd1b95038a7..70439481c2a 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts @@ -3,8 +3,11 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import { Application } from '../../infra'; +import { EditorActionBar } from '../../pages/editorActionBar'; import { test, expect, tags } from '../_test.setup'; -import { verifyOpenInNewWindow, verifySplitEditor, verifySummaryPosition } from './helpers'; + +let editorActionBar: EditorActionBar; const testCases = [ { @@ -33,17 +36,18 @@ test.describe('Editor Action Bar: Data Explorer', { tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.DATA_EXPLORER] }, () => { - test.beforeAll(async function ({ userSettings }) { + test.beforeAll(async function ({ app, userSettings }) { + editorActionBar = app.workbench.editorActionBar; await userSettings.set([['editor.actionBar.enabled', 'true']], false); }); - test.afterEach(async function ({ app }) { - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); - await app.workbench.quickaccess.runCommand('Console: Clear Console'); + test.afterEach(async function ({ runCommand }) { + await runCommand('workbench.action.closeAllEditors'); + await runCommand('Console: Clear Console'); }); for (const testCase of testCases) { - test(testCase.title, async function ({ app, page, interpreter, openDataFile, openFile }) { + test(testCase.title, async function ({ app, interpreter, openDataFile, openFile }) { // Set interpreter const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; await interpreter.set(language); @@ -55,20 +59,34 @@ test.describe('Editor Action Bar: Data Explorer', { // Open data explorer via variable pane if (testCase.variable) { - await test.step('Open data explorer via variable pane', async () => { - await app.workbench.editor.playButton.click(); - await app.workbench.variables.doubleClickVariableRow(testCase.variable); - await app.code.driver.page.getByRole('tablist').locator('.tab').first().click(); - await app.code.driver.page.getByLabel('Close').first().click(); - await expect(app.code.driver.page.getByText(testCase.tabName, { exact: true })).toBeVisible(); - }); + await openDataExplorerViaVariablePane(app, testCase.variable, testCase.tabName); } // Verify action bar behavior - await verifySummaryPosition(app, 'Left'); - await verifySummaryPosition(app, 'Right'); - await verifySplitEditor(page, testCase.tabName); - await verifyOpenInNewWindow(app, `${testCase.tabName} — qa-example-content`); + await editorActionBar.selectSummaryOn(app.web, 'Left'); + await editorActionBar.verifySummaryPosition('Left'); + + await editorActionBar.selectSummaryOn(app.web, 'Right'); + await editorActionBar.verifySummaryPosition('Right'); + + await editorActionBar.clickSplitEditorButton('right'); + await editorActionBar.verifySplitEditor('right', testCase.tabName); + + await editorActionBar.clickSplitEditorButton('down'); + await editorActionBar.verifySplitEditor('down', testCase.tabName); + + await editorActionBar.verifyOpenInNewWindow(app.web, `${testCase.tabName} — qa-example-content`); }); } }); + + +async function openDataExplorerViaVariablePane(app: Application, variable: string, tabName: string) { + await test.step('Open data explorer via variable pane', async () => { + await app.workbench.editor.playButton.click(); + await app.workbench.variables.doubleClickVariableRow(variable); + await app.code.driver.page.getByRole('tablist').locator('.tab').first().click(); + await app.code.driver.page.getByLabel('Close').first().click(); + await expect(app.code.driver.page.getByText(tabName, { exact: true })).toBeVisible(); + }); +} diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts index e45c220e279..b01fc70c5e3 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts +++ b/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts @@ -3,8 +3,12 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import { expect, Page } from '@playwright/test'; import { test, tags } from '../_test.setup'; -import { verifyOpenChanges, verifyOpenInNewWindow, verifyOpenViewerRendersHtml, verifyPreviewRendersHtml, verifySplitEditor, verifyToggleBreadcrumb, verifyToggleLineNumbers } from './helpers'; +import { EditorActionBar } from '../../pages/editorActionBar'; +import { Application } from '../../infra'; + +let editorActionBar: EditorActionBar; test.use({ suiteId: __filename @@ -14,20 +18,21 @@ test.describe('Editor Action Bar: Documents', { tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.EDITOR] }, () => { - test.beforeAll(async function ({ userSettings }) { + test.beforeAll(async function ({ userSettings, app }) { + editorActionBar = app.workbench.editorActionBar; await userSettings.set([['editor.actionBar.enabled', 'true']], false); }); - test.afterEach(async function ({ app }) { - await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors'); + test.afterEach(async function ({ runCommand }) { + await runCommand('workbench.action.closeAllEditors'); }); test('R Markdown Document [C1080703]', { tag: [tags.R_MARKDOWN] - }, async function ({ app, page, openFile }) { + }, async function ({ app, openFile }) { await openFile('workspaces/basic-rmd-file/basicRmd.rmd'); - await verifyPreviewRendersHtml(app, 'Getting startedAnchor'); - await verifySplitEditor(page, 'basicRmd.rmd'); + await verifyPreviewRendersHtml('Getting startedAnchor'); + await verifySplitEditor('basicRmd.rmd'); await verifyOpenInNewWindow(app, 'This post examines the features'); }); @@ -35,16 +40,16 @@ test.describe('Editor Action Bar: Documents', { tag: [tags.QUARTO] }, async function ({ app, page, openFile }) { await openFile('workspaces/quarto_basic/quarto_basic.qmd'); - await verifyPreviewRendersHtml(app, 'Diamond sizes'); + await verifyPreviewRendersHtml('Diamond sizes'); await verifyOpenChanges(page); - await verifySplitEditor(page, 'quarto_basic.qmd'); + await verifySplitEditor('quarto_basic.qmd'); await verifyOpenInNewWindow(app, 'Diamond sizes'); }); test('HTML Document [C1080701]', { tag: [tags.HTML] }, async function ({ app, page, openFile }) { await openFile('workspaces/dash-py-example/data/OilandGasMetadata.html'); - await verifyOpenViewerRendersHtml(app); - await verifySplitEditor(page, 'OilandGasMetadata.html'); + await verifyOpenViewerRendersHtml(app, 'Oil, Gas, and Other Regulated'); + await verifySplitEditor('OilandGasMetadata.html'); await verifyOpenInNewWindow(app, ' Oil & Gas Wells - Metadata'); }); @@ -58,7 +63,79 @@ test.describe('Editor Action Bar: Documents', { await verifyToggleBreadcrumb(page); } - await verifySplitEditor(page, 'spotify.ipynb'); + await verifySplitEditor('spotify.ipynb'); }); }); + +// Helper functions + +async function verifyPreviewRendersHtml(heading: string) { + await editorActionBar.previewButton.click(); + await editorActionBar.verifyPreviewRendersHtml(heading); +} + +async function verifySplitEditor(tabName: string) { + await editorActionBar.clickSplitEditorButton('right'); + await editorActionBar.verifySplitEditor('right', tabName); + + await editorActionBar.clickSplitEditorButton('down'); + await editorActionBar.verifySplitEditor('down', tabName); +} + +async function verifyOpenInNewWindow(app: Application, title: string) { + await editorActionBar.verifyOpenInNewWindow(app.web, title); +} + +async function verifyOpenViewerRendersHtml(app: Application, title: string) { + await editorActionBar.openInViewerButton.click(); + await editorActionBar.verifyOpenViewerRendersHtml(app.web, title); +} + +async function verifyOpenChanges(page: Page) { + await test.step('verify "open changes" shows diff', async () => { + async function bindPlatformHotkey(page: Page, key: string) { + await page.keyboard.press(process.platform === 'darwin' ? `Meta+${key}` : `Control+${key}`); + } + + // make change & save + await page.getByText('date', { exact: true }).click(); + await page.keyboard.press('X'); + await bindPlatformHotkey(page, 'S'); + + // click open changes & verify + await page.getByLabel('Open Changes').click(); + await expect(page.getByLabel('Revert Block')).toBeVisible(); + await expect(page.getByLabel('Stage Block')).toBeVisible(); + await page.getByRole('tab', { name: 'quarto_basic.qmd (Working' }).getByLabel('Close').click(); + + // undo changes & save + await bindPlatformHotkey(page, 'Z'); + await bindPlatformHotkey(page, 'S'); + }); +} + +async function verifyToggleLineNumbers(page: Page) { + async function verifyLineNumbersVisibility(page: Page, isVisible: boolean) { + for (const lineNum of [1, 2, 3, 4, 5]) { + const lineNumbers = expect(page.locator('.line-numbers').getByText(lineNum.toString(), { exact: true })); + isVisible ? await lineNumbers.toBeVisible() : await lineNumbers.not.toBeVisible(); + } + } + + await test.step('verify "customize notebook > toggle line numbers" (web only)', async () => { + await verifyLineNumbersVisibility(page, false); + await editorActionBar.clickCustomizeNotebookMenuItem('Toggle Notebook Line Numbers'); + await verifyLineNumbersVisibility(page, true); + }); +} + +async function verifyToggleBreadcrumb(page: Page) { + await test.step('verify "customize notebook > toggle breadcrumbs" (web only)', async () => { + const breadcrumbs = page.locator('.monaco-breadcrumbs'); + + await expect(breadcrumbs).toBeVisible(); + await editorActionBar.clickCustomizeNotebookMenuItem('Toggle Breadcrumbs'); + await expect(breadcrumbs).not.toBeVisible(); + }); +} diff --git a/test/e2e/tests/editor-action-bar/helpers.ts b/test/e2e/tests/editor-action-bar/helpers.ts deleted file mode 100644 index 8a303812b32..00000000000 --- a/test/e2e/tests/editor-action-bar/helpers.ts +++ /dev/null @@ -1,185 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import test, { expect, Page } from '@playwright/test'; -import { Application } from '../../infra'; - -// --- SHARED HELPERS --- - -export async function verifySplitEditor(page, tabName: string) { - await test.step(`Verify "split editor" behavior`, async () => { - // Split editor right - await page.getByLabel('Split Editor Right', { exact: true }).click(); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - // Verify tabs are on the same X plane - const rightSplitTabs = page.getByRole('tab', { name: tabName }); - const firstTabBox = await rightSplitTabs.nth(0).boundingBox(); - const secondTabBox = await rightSplitTabs.nth(1).boundingBox(); - - expect(firstTabBox).not.toBeNull(); - expect(secondTabBox).not.toBeNull(); - expect(firstTabBox!.y).toBeCloseTo(secondTabBox!.y, 1); - expect(firstTabBox!.x).not.toBeCloseTo(secondTabBox!.x, 1); - - // Close one tab - await rightSplitTabs.first().getByLabel('Close').click(); - - // Split editor down - await page.keyboard.down('Alt'); - await page.getByLabel('Split Editor Down').click(); - await page.keyboard.up('Alt'); - await expect(page.getByRole('tab', { name: tabName })).toHaveCount(2); - - // Verify tabs are on the same Y plane - const downSplitTabs = page.getByRole('tab', { name: tabName }); - const firstDownTabBox = await downSplitTabs.nth(0).boundingBox(); - const secondDownTabBox = await downSplitTabs.nth(1).boundingBox(); - - expect(firstDownTabBox).not.toBeNull(); - expect(secondDownTabBox).not.toBeNull(); - expect(firstDownTabBox!.x).toBeCloseTo(secondDownTabBox!.x, 1); - expect(firstDownTabBox!.y).not.toBeCloseTo(secondDownTabBox!.y, 1); - }); -} - -export async function verifyOpenInNewWindow(app: Application, expectedText: string) { - if (!app.web) { - await test.step(`Verify "open new window" contains: ${expectedText}`, async () => { - const [newPage] = await Promise.all([ - app.code.driver.page.context().waitForEvent('page'), - app.code.driver.page.getByLabel('Move into new window').first().click(), - ]); - await newPage.waitForLoadState(); - await expect(newPage.getByText(expectedText)).toBeVisible(); - }); - } -} - - -// --- editor-action-bar-data-explorer.test.ts HELPERS --- - -export async function verifySummaryPosition(app: Application, position: 'Left' | 'Right') { - const page = app.code.driver.page; - - await test.step(`Verify summary position: ${position}`, async () => { - // Toggle the summary position - if (app.web) { - await page.getByLabel('More actions', { exact: true }).click(); - await page.getByRole('menuitemcheckbox', { name: `Summary on ${position}` }).hover(); - await page.keyboard.press('Enter'); - } - else { - await app.workbench.quickaccess.runCommand(`workbench.action.positronDataExplorer.summaryOn${position}`); - } - - // Get the summary and table locators. - const summaryLocator = page.locator('div.column-summary').first(); - const tableLocator = page.locator('div.data-grid-column-headers'); - - // Ensure both the summary and table elements are visible - await expect(summaryLocator).toBeVisible(); - await expect(tableLocator).toBeVisible(); - - // Get the bounding boxes for both elements - const summaryBox = await summaryLocator.boundingBox(); - const tableBox = await tableLocator.boundingBox(); - - // Validate bounding boxes are available - if (!summaryBox || !tableBox) { - throw new Error('Bounding boxes could not be retrieved for summary or table.'); - } - - // Validate positions based on the expected position - position === 'Left' - ? expect(summaryBox.x).toBeLessThan(tableBox.x) - : expect(summaryBox.x).toBeGreaterThan(tableBox.x); - }); -} - -// --- editor-action-bar-documents.test.ts HELPERS --- - - -export async function clickCustomizeNotebookMenuItem(page, menuItem: string) { - const role = menuItem.includes('Line Numbers') ? 'menuitemcheckbox' : 'menuitem'; - const dropdownButton = page.getByLabel('Customize Notebook...'); - await dropdownButton.evaluate((button) => { - (button as HTMLElement).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); - }); - - const toggleMenuItem = page.getByRole(role, { name: menuItem }); - await toggleMenuItem.hover(); - await page.waitForTimeout(500); - await toggleMenuItem.click(); -} - -export async function verifyLineNumbersVisibility(page, isVisible: boolean) { - for (const lineNum of [1, 2, 3, 4, 5]) { - const lineNumbers = expect(page.locator('.line-numbers').getByText(lineNum.toString(), { exact: true })); - isVisible ? await lineNumbers.toBeVisible() : await lineNumbers.not.toBeVisible(); - } -} - -export async function verifyOpenChanges(page: Page) { - await test.step('verify "open changes" shows diff', async () => { - - // make change & save - await page.getByText('date', { exact: true }).click(); - await page.keyboard.press('X'); - await bindPlatformHotkey(page, 'S'); - - // click open changes & verify - await page.getByLabel('Open Changes').click(); - await expect(page.getByLabel('Revert Block')).toBeVisible(); - await expect(page.getByLabel('Stage Block')).toBeVisible(); - await page.getByRole('tab', { name: 'quarto_basic.qmd (Working' }).getByLabel('Close').click(); - - // undo changes & save - await bindPlatformHotkey(page, 'Z'); - await bindPlatformHotkey(page, 'S'); - }); -} - -export async function bindPlatformHotkey(page: Page, key: string) { - await page.keyboard.press(process.platform === 'darwin' ? `Meta+${key}` : `Control+${key}`); -} - -export async function verifyOpenViewerRendersHtml(app: Application) { - await test.step('verify "open in viewer" renders html', async () => { - await app.code.driver.page.getByLabel('Open in Viewer').click(); - const viewerFrame = app.code.driver.page.locator('iframe.webview').contentFrame().locator('#active-frame').contentFrame(); - const cellLocator = app.web - ? viewerFrame.frameLocator('iframe').getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }) - : viewerFrame.getByRole('cell', { name: 'Oil, Gas, and Other Regulated' }); - - await expect(cellLocator).toBeVisible({ timeout: 30000 }); - }); -} - -export async function verifyPreviewRendersHtml(app: Application, heading: string) { - await test.step('verify "preview" renders html', async () => { - await app.code.driver.page.getByLabel('Preview', { exact: true }).click(); - const viewerFrame = app.workbench.viewer.getViewerFrame().frameLocator('iframe'); - await expect(viewerFrame.getByRole('heading', { name: heading })).toBeVisible({ timeout: 30000 }); - }); -} - -export async function verifyToggleLineNumbers(page: Page) { - await test.step('verify "customize notebook > toggle line numbers" (web only)', async () => { - await verifyLineNumbersVisibility(page, false); - await clickCustomizeNotebookMenuItem(page, 'Toggle Notebook Line Numbers'); - await verifyLineNumbersVisibility(page, true); - }); -} - -export async function verifyToggleBreadcrumb(page: Page) { - await test.step('verify "customize notebook > toggle breadcrumbs" (web only)', async () => { - const breadcrumbs = page.locator('.monaco-breadcrumbs'); - - await expect(breadcrumbs).toBeVisible(); - await clickCustomizeNotebookMenuItem(page, 'Toggle Breadcrumbs'); - await expect(breadcrumbs).not.toBeVisible(); - }); -} From 46b7fb2a90d2a4bdcbb0ec45c7cce69dde70122f Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 11:19:29 -0600 Subject: [PATCH 18/21] nit --- test/e2e/pages/variables.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/e2e/pages/variables.ts b/test/e2e/pages/variables.ts index 50be07a24c7..99606805c42 100644 --- a/test/e2e/pages/variables.ts +++ b/test/e2e/pages/variables.ts @@ -64,10 +64,8 @@ export class Variables { } async doubleClickVariableRow(variableName: string) { - await test.step(`Double click variable row: ${variableName}`, async () => { - const desiredRow = await this.waitForVariableRow(variableName); - await desiredRow.dblclick(); - }); + const desiredRow = await this.waitForVariableRow(variableName); + await desiredRow.dblclick(); } async toggleVariablesView() { From d9da85a99f62cfaf2dd2e2a39d57a426120967ec Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 13:00:23 -0600 Subject: [PATCH 19/21] rename tests & add csv --- ...tion-bar-data-explorer.test.ts => data-files.test.ts} | 9 +++++++-- ...tion-bar-documents.test.ts => document-files.test.ts} | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) rename test/e2e/tests/editor-action-bar/{editor-action-bar-data-explorer.test.ts => data-files.test.ts} (93%) rename test/e2e/tests/editor-action-bar/{editor-action-bar-documents.test.ts => document-files.test.ts} (98%) diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts b/test/e2e/tests/editor-action-bar/data-files.test.ts similarity index 93% rename from test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts rename to test/e2e/tests/editor-action-bar/data-files.test.ts index 70439481c2a..d62a0e61957 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-data-explorer.test.ts +++ b/test/e2e/tests/editor-action-bar/data-files.test.ts @@ -23,9 +23,14 @@ const testCases = [ tabName: 'Data: df', }, { - title: 'Python - Open parquet file via DuckDB', + title: 'Open parquet file via DuckDB', openDataFile: 'data-files/100x100/100x100.parquet', tabName: 'Data: 100x100.parquet', + }, + { + title: 'Open CSV file via DuckDB', + openDataFile: 'data-files/flights/flights.csv', + tabName: 'Data: flights.csv' }]; test.use({ @@ -49,7 +54,7 @@ test.describe('Editor Action Bar: Data Explorer', { for (const testCase of testCases) { test(testCase.title, async function ({ app, interpreter, openDataFile, openFile }) { // Set interpreter - const language = testCase.title.startsWith('Python') ? 'Python' : 'R'; + const language = testCase.title.startsWith('R') ? 'R' : 'Python'; await interpreter.set(language); // Open file diff --git a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts b/test/e2e/tests/editor-action-bar/document-files.test.ts similarity index 98% rename from test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts rename to test/e2e/tests/editor-action-bar/document-files.test.ts index b01fc70c5e3..2e459a170c7 100644 --- a/test/e2e/tests/editor-action-bar/editor-action-bar-documents.test.ts +++ b/test/e2e/tests/editor-action-bar/document-files.test.ts @@ -14,7 +14,7 @@ test.use({ suiteId: __filename }); -test.describe('Editor Action Bar: Documents', { +test.describe('Editor Action Bar: Document Files', { tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.EDITOR] }, () => { From e19498939436100b29e62a8ed1fff158ff659c14 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 13:18:54 -0600 Subject: [PATCH 20/21] update test name --- test/e2e/tests/editor-action-bar/data-files.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/tests/editor-action-bar/data-files.test.ts b/test/e2e/tests/editor-action-bar/data-files.test.ts index d62a0e61957..4f58dae5b77 100644 --- a/test/e2e/tests/editor-action-bar/data-files.test.ts +++ b/test/e2e/tests/editor-action-bar/data-files.test.ts @@ -37,7 +37,7 @@ test.use({ suiteId: __filename }); -test.describe('Editor Action Bar: Data Explorer', { +test.describe('Editor Action Bar: Data Files', { tag: [tags.WEB, tags.WIN, tags.EDITOR_ACTION_BAR, tags.DATA_EXPLORER] }, () => { From 3300cb77aa05e80c638ba042b0b11122b05971a0 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Fri, 17 Jan 2025 14:41:29 -0600 Subject: [PATCH 21/21] minor things --- test/e2e/pages/variables.ts | 2 +- .../e2e/tests/data-explorer/data-explorer-python-pandas.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/pages/variables.ts b/test/e2e/pages/variables.ts index 99606805c42..2f230684c48 100644 --- a/test/e2e/pages/variables.ts +++ b/test/e2e/pages/variables.ts @@ -6,7 +6,7 @@ import { Code } from '../infra/code'; import * as os from 'os'; -import test, { expect, Locator } from '@playwright/test'; +import { expect, Locator } from '@playwright/test'; interface FlatVariables { value: string; diff --git a/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts b/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts index 2a2f21dd205..be7dd53f1df 100644 --- a/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts +++ b/test/e2e/tests/data-explorer/data-explorer-python-pandas.test.ts @@ -192,7 +192,7 @@ Data_Frame = pd.DataFrame({ "gear": [4, 4, 4, 3, 3], "carb": [4, 4, 1, 1, 2] })`; - await app.workbench.console.executeCode('Python', script, '>>>'); + await app.workbench.console.executeCode('Python', script); await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus'); await expect(async () => {