diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index bfc28632174..55783b1355b 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -36,7 +36,9 @@ export class PlaywrightDriver { constructor( private readonly application: playwright.Browser | playwright.ElectronApplication, private readonly context: playwright.BrowserContext, - private readonly page: playwright.Page, + // --- Start Positron --- + readonly page: playwright.Page, + // --- End Positron --- private readonly serverProcess: ChildProcess | undefined, private readonly whenLoaded: Promise, private readonly options: LaunchOptions diff --git a/test/automation/src/positron/positronVariables.ts b/test/automation/src/positron/positronVariables.ts index 256499bfd21..acfd3eec560 100644 --- a/test/automation/src/positron/positronVariables.ts +++ b/test/automation/src/positron/positronVariables.ts @@ -7,19 +7,21 @@ import { Code } from '../code'; import * as os from 'os'; import { IElement } from '../driver'; -import { Locator } from '@playwright/test'; +import { expect, Locator } from '@playwright/test'; interface FlatVariables { value: string; type: string; } -const VARIABLE_ITEMS = '.variables-instance[style*="z-index: 1"] .list .variable-item'; +const VARIABLE_ITEMS = '.variable-item'; const VARIABLE_NAMES = 'name-column'; const VARIABLE_DETAILS = 'details-column'; const VARIABLES_NAME_COLUMN = '.variables-instance[style*="z-index: 1"] .variable-item .name-column'; const VARIABLES_SECTION = '[aria-label="Variables Section"]'; const VARIABLES_INTERPRETER = '.positron-variables-container .action-bar-button-text'; +const VARIABLE_CHEVRON_ICON = '.gutter .expand-collapse-icon'; +const VARIABLE_INDENTED = '.name-column-indenter[style*="margin-left: 40px"]'; /* * Reuseable Positron variables functionality for tests to leverage. @@ -49,7 +51,6 @@ export class PositronVariables { } async waitForVariableRow(variableName: string): Promise { - const desiredRow = this.code.driver.getLocator(`${VARIABLES_NAME_COLUMN} .name-value:text("${variableName}")`); await desiredRow.waitFor({ state: 'attached' }); return desiredRow; @@ -61,19 +62,79 @@ export class PositronVariables { await desiredRow.dblclick(); } - async openVariables() { - + async toggleVariablesView() { const isMac = os.platform() === 'darwin'; const modifier = isMac ? 'Meta' : 'Control'; await this.code.driver.getKeyboard().press(`${modifier}+Alt+B`); - await this.code.waitForElement(VARIABLES_SECTION); + } + + async toggleVariable({ variableName, action }: { variableName: string; action: 'expand' | 'collapse' }) { + await this.waitForVariableRow(variableName); + const variable = this.code.driver.page.locator('.name-value', { hasText: variableName }); + const chevronIcon = variable.locator('..').locator(VARIABLE_CHEVRON_ICON); + const isExpanded = await chevronIcon.evaluate((el) => el.classList.contains('codicon-chevron-down')); + + // perform action based on the 'action' parameter + if (action === 'expand' && !isExpanded) { + await chevronIcon.click(); + } else if (action === 'collapse' && isExpanded) { + await chevronIcon.click(); + } + + const expectedClass = action === 'expand' + ? /codicon-chevron-down/ + : /codicon-chevron-right/; + + await expect(chevronIcon).toHaveClass(expectedClass); + } + + async expandVariable(variableName: string) { + await this.toggleVariable({ variableName, action: 'expand' }); + } + + async collapseVariable(variableName: string) { + await this.toggleVariable({ variableName, action: 'collapse' }); } async getVariablesInterpreter(): Promise { const interpreter = await this.code.waitForElement(VARIABLES_INTERPRETER); return interpreter; } + + /** + * Gets the data (value and type) for the children of a parent variable. + * NOTE: it assumes that either ALL variables are collapsed or ONLY the parent variable is expanded. + * + * @param parentVariable the parent variable to get the children of + * @param collapseParent whether to collapse the parent variable after getting the children data + * @returns a map of the children's name, value, and type + */ + async getVariableChildren(parentVariable: string, collapseParent = true): Promise<{ [key: string]: { value: string; type: string } }> { + await this.expandVariable(parentVariable); + const variable = this.code.driver.page.locator(`.name-value:text-is("${parentVariable}")`); + + // get the children of the parent variable, which are indented + const children = await variable.locator('..').locator('..').locator('..').locator('..').locator(VARIABLE_ITEMS) + .filter({ has: this.code.driver.page.locator(VARIABLE_INDENTED) }).all(); + + // create a map of the children's name, value, and type + const result: { [key: string]: { value: string; type: string } } = {}; + for (const child of children) { + const childName = await child.locator('.name-value').textContent() || ''; + const childValue = await child.locator('.details-column .value').textContent() || ''; + const childType = await child.locator('.details-column .right-column').textContent() || ''; + + if (childName) { + result[childName] = { value: childValue, type: childType }; + } + } + + // collapse the parent variable if the flag is set + if (collapseParent) { await this.collapseVariable(parentVariable); } + + return result; + } } diff --git a/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts b/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts index 231996b510d..b660528c225 100644 --- a/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts +++ b/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts @@ -66,7 +66,7 @@ df = pd.DataFrame(data)`; }).toPass({ timeout: 60000 }); await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); @@ -145,7 +145,7 @@ df2 = pd.DataFrame(data)`; await app.workbench.positronSideBar.closeSecondarySideBar(); await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); // This test is not dependent on the previous test, so it refreshes the python environment @@ -215,7 +215,7 @@ df2 = pd.DataFrame(data)`; const app = this.app as Application; await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); }); diff --git a/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts b/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts index bd197076b16..a00c7b16eda 100644 --- a/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts +++ b/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts @@ -53,7 +53,7 @@ describe('Data Explorer - Very Large Data Frame #win', () => { const app = this.app as Application; await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); @@ -135,7 +135,7 @@ describe('Data Explorer - Very Large Data Frame #win', () => { const app = this.app as Application; await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); diff --git a/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts b/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts index e15e27d4d6e..7411612beb2 100644 --- a/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts +++ b/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts @@ -27,7 +27,7 @@ describe('Data Explorer - XLSX #web #win', () => { const app = this.app as Application; await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); @@ -68,7 +68,7 @@ describe('Data Explorer - XLSX #web #win', () => { const app = this.app as Application; await app.workbench.positronDataExplorer.closeDataExplorer(); - await app.workbench.positronVariables.openVariables(); + await app.workbench.positronVariables.toggleVariablesView(); }); diff --git a/test/smoke/src/areas/positron/variables/variables-expanded.test.ts b/test/smoke/src/areas/positron/variables/variables-expanded.test.ts new file mode 100644 index 00000000000..41c341cbbd5 --- /dev/null +++ b/test/smoke/src/areas/positron/variables/variables-expanded.test.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from '@playwright/test'; +import { Application, PositronPythonFixtures } from '../../../../../automation'; +import { setupAndStartApp } from '../../../test-runner/test-hooks'; + +describe('Variables - Expanded View #web #win', () => { + setupAndStartApp(); + + beforeEach(async function () { + const app = this.app as Application; + await PositronPythonFixtures.SetupFixtures(app); + await app.workbench.positronConsole.executeCode('Python', script, '>>>'); + await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar'); + + }); + + it('Python - should display children values and types when variable is expanded', async function () { + const app = this.app as Application; + const variables = app.workbench.positronVariables; + + await variables.expandVariable('df'); + for (const variable of Object.keys(expectedData)) { + const actualData = await variables.getVariableChildren(variable); + expect(actualData).toEqual(expectedData[variable]); + } + }); +}); + +const script = ` +import polars as pl + +from datetime import date +df = pl.DataFrame( + { + "foo": [1, 2, 3], + "bar": [6.0, 7.0, 8.0], + "ham": [date(2020, 1, 2), date(2021, 3, 4), date(2022, 5, 6)], + "green": [None, 2, 3], + "eggs": [0.5, None, 2.5], + "cheese": [True, None, False], + } +) +`; + +const expectedData = { + foo: { 0: { type: "int", value: "1" }, 1: { type: "int", value: "2" }, 2: { type: "int", value: "3" } }, + bar: { 0: { type: "float", value: "6.0" }, 1: { type: "float", value: "7.0" }, 2: { type: "float", value: "8.0" } }, + ham: { 0: { type: "date", value: "datetime.date(2020, 1, 2)" }, 1: { type: "date", value: "datetime.date(2021, 3, 4)" }, 2: { type: "date", value: "datetime.date(2022, 5, 6)" } }, + green: { 0: { type: "NoneType", value: "None" }, 1: { type: "int", value: "2" }, 2: { type: "int", value: "3" } }, + eggs: { 0: { type: "float", value: "0.5" }, 1: { type: "NoneType", value: "None" }, 2: { type: "float", value: "2.5" } }, + cheese: { 0: { type: "bool", value: "True" }, 1: { type: "NoneType", value: "None" }, 2: { type: "bool", value: "False" } }, +}; diff --git a/test/smoke/src/areas/positron/variables/notebookVariables.test.ts b/test/smoke/src/areas/positron/variables/variables-notebook.test.ts similarity index 100% rename from test/smoke/src/areas/positron/variables/notebookVariables.test.ts rename to test/smoke/src/areas/positron/variables/variables-notebook.test.ts diff --git a/test/smoke/src/areas/positron/variables/variablespane.test.ts b/test/smoke/src/areas/positron/variables/variables-pane.test.ts similarity index 100% rename from test/smoke/src/areas/positron/variables/variablespane.test.ts rename to test/smoke/src/areas/positron/variables/variables-pane.test.ts