Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

e2e-test: add data explorer editor action bar test #6019

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions test/e2e/infra/fixtures/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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);
});
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/infra/test-runner/test-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

export enum TestTags {
// feature tags
EDITOR_ACTION_BAR = '@:editor-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',
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/infra/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
Expand Down Expand Up @@ -65,6 +66,7 @@ export class Workbench {
readonly extensions: Extensions;
readonly editors: Editors;
readonly settings: Settings;
readonly editorActionBar: EditorActionBar;

constructor(code: Code) {

Expand Down Expand Up @@ -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);
}
}

60 changes: 35 additions & 25 deletions test/e2e/pages/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,37 +71,39 @@ 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, prompt: string): Promise<void> {
async executeCode(languageName: 'Python' | 'R', code: string): Promise<void> {
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(languageName === 'Python' ? '>>>' : '>');
await this.maximizeConsole();
});
}

async logConsoleContents() {
Expand Down Expand Up @@ -129,9 +131,17 @@ export class Console {

async waitForReady(prompt: string, timeout = 30000): Promise<void> {
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<void> {
this.waitForReady(prompt, timeout);
await this.waitForConsoleContents('started');
}

async waitForReadyAndRestarted(prompt: string, timeout = 30000): Promise<void> {
this.waitForReady(prompt, timeout);
await this.waitForConsoleContents('restarted');
}

/**
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/pages/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
182 changes: 182 additions & 0 deletions test/e2e/pages/editorActionBar.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
1 change: 1 addition & 0 deletions test/e2e/pages/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class Settings {
async addUserSettings(settings: [key: string, value: string][]): Promise<void> {
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();
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/pages/utils/packageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/pages/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading