diff --git a/test/automation/src/index.ts b/test/automation/src/index.ts index 325778d8272..bd0005a39d4 100644 --- a/test/automation/src/index.ts +++ b/test/automation/src/index.ts @@ -55,5 +55,6 @@ export * from './positron/positronQuickaccess'; export * from './positron/positronOutline'; export * from './positron/positronClipboard'; export * from './positron/positronExtensions'; +export * from './positron/positronEditors'; // --- End Positron --- export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron'; diff --git a/test/automation/src/positron/positronEditor.ts b/test/automation/src/positron/positronEditor.ts index 91a719b84c3..37ed1498883 100644 --- a/test/automation/src/positron/positronEditor.ts +++ b/test/automation/src/positron/positronEditor.ts @@ -78,4 +78,36 @@ export class PositronEditor { return topValue; } + + async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { + if (text.includes('\n')) { + throw new Error('waitForTypeInEditor does not support new lines, use either a long single line or dispatchKeybinding(\'Enter\')'); + } + const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); + + await expect(this.code.driver.page.locator(editor)).toBeVisible(); + + const textarea = `${editor} textarea`; + await expect(this.code.driver.page.locator(textarea)).toBeFocused(); + + await this.code.driver.page.locator(textarea).fill(text); + + await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); + } + + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { + const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); + const locator = this.code.driver.page.locator(selector); + + let content = ''; + await expect(async () => { + content = (await locator.textContent())?.replace(/\u00a0/g, ' ') || ''; + if (!accept(content)) { + throw new Error(`Content did not match condition: ${content}`); + } + }).toPass(); + + return content; + } + } diff --git a/test/automation/src/positron/positronEditors.ts b/test/automation/src/positron/positronEditors.ts new file mode 100644 index 00000000000..0bd95e860b2 --- /dev/null +++ b/test/automation/src/positron/positronEditors.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Code } from '../code'; + + +export class PositronEditors { + + activeEditor = this.code.driver.page.locator('div.tab.tab-actions-right.active.selected'); + editorIcon = this.code.driver.page.locator('.monaco-icon-label.file-icon'); + editorPart = this.code.driver.page.locator('.split-view-view .part.editor'); + + constructor(private code: Code) { } + + async waitForActiveTab(fileName: string, isDirty: boolean = false): Promise { + await expect(this.code.driver.page.locator(`.tabs-container div.tab.active${isDirty ? '.dirty' : ''}[aria-selected="true"][data-resource-name$="${fileName}"]`)).toBeVisible(); + } + + async newUntitledFile(): Promise { + if (process.platform === 'darwin') { + await this.code.driver.page.keyboard.press('Meta+N'); + } else { + await this.code.driver.page.keyboard.press('Control+N'); + } + + await this.waitForEditorFocus('Untitled-1'); + } + + async waitForEditorFocus(fileName: string): Promise { + await this.waitForActiveTab(fileName, undefined); + await this.waitForActiveEditor(fileName); + } + + async waitForActiveEditor(fileName: string): Promise { + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + await expect(this.code.driver.page.locator(selector)).toBeFocused(); + } + + async selectTab(fileName: string): Promise { + + // Selecting a tab and making an editor have keyboard focus + // is critical to almost every test. As such, we try our + // best to retry this task in case some other component steals + // focus away from the editor while we attempt to get focus + + await expect(async () => { + await this.code.driver.page.locator(`.tabs-container div.tab[data-resource-name$="${fileName}"]`).click(); + await this.code.driver.page.keyboard.press(process.platform === 'darwin' ? 'Meta+1' : 'Control+1'); // make editor really active if click failed somehow + await this.waitForEditorFocus(fileName); + }).toPass(); + } + + async waitForTab(fileName: string, isDirty: boolean = false): Promise { + await expect(this.code.driver.page.locator(`.tabs-container div.tab${isDirty ? '.dirty' : ''}[data-resource-name$="${fileName}"]`)).toBeVisible(); + } +} diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index ceff57b6bfe..037cf22f626 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -43,6 +43,7 @@ import { PositronWelcome } from './positron/positronWelcome'; import { PositronTerminal } from './positron/positronTerminal'; import { PositronViewer } from './positron/positronViewer'; import { PositronEditor } from './positron/positronEditor'; +import { PositronEditors } from './positron/positronEditors'; import { PositronTestExplorer } from './positron/positronTestExplorer'; import { PositronQuickAccess } from './positron/positronQuickaccess'; import { PositronOutline } from './positron/positronOutline'; @@ -102,6 +103,7 @@ export class Workbench { readonly positronClipboard: PositronClipboard; readonly positronQuickInput: PositronQuickInput; readonly positronExtensions: PositronExtensions; + readonly positronEditors: PositronEditors; // --- End Positron --- constructor(code: Code) { @@ -150,6 +152,7 @@ export class Workbench { this.positronOutline = new PositronOutline(code, this.positronQuickaccess); this.positronClipboard = new PositronClipboard(code); this.positronExtensions = new PositronExtensions(code, this.positronQuickaccess); + this.positronEditors = new PositronEditors(code); // --- End Positron --- } } diff --git a/test/e2e/areas/console/console-ansi.test.ts b/test/e2e/areas/console/console-ansi.test.ts index dbdf8fd440b..5822b0e4550 100644 --- a/test/e2e/areas/console/console-ansi.test.ts +++ b/test/e2e/areas/console/console-ansi.test.ts @@ -31,7 +31,7 @@ test.describe('Console ANSI styling', { tag: [tags.CRITICAL, tags.CONSOLE, tags. await expect(link).toContainText(fileName, { useInnerText: true }); await link.click(); - await app.workbench.editors.waitForActiveTab(fileName); + await app.workbench.positronEditors.waitForActiveTab(fileName); }).toPass({ timeout: 60000 }); }); diff --git a/test/e2e/areas/data-explorer/data-explorer-python-pandas.test.ts b/test/e2e/areas/data-explorer/data-explorer-python-pandas.test.ts index 56854643bd9..c9ac12e0ad4 100644 --- a/test/e2e/areas/data-explorer/data-explorer-python-pandas.test.ts +++ b/test/e2e/areas/data-explorer/data-explorer-python-pandas.test.ts @@ -190,7 +190,7 @@ Data_Frame = data('mtcars')`; }).toPass(); // Now move focus out of the the data explorer pane - await app.workbench.editors.newUntitledFile(); + await app.workbench.positronEditors.newUntitledFile(); await app.workbench.positronQuickaccess.runCommand('workbench.panel.positronVariables.focus'); await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame'); diff --git a/test/e2e/areas/data-explorer/data-explorer-r.test.ts b/test/e2e/areas/data-explorer/data-explorer-r.test.ts index afcdfa53383..ee838ce8903 100644 --- a/test/e2e/areas/data-explorer/data-explorer-r.test.ts +++ b/test/e2e/areas/data-explorer/data-explorer-r.test.ts @@ -91,7 +91,7 @@ test.describe('Data Explorer - R ', { }).toPass(); // Now move focus out of the the data explorer pane - await app.workbench.editors.newUntitledFile(); + await app.workbench.positronEditors.newUntitledFile(); await app.workbench.positronQuickaccess.runCommand('workbench.panel.positronVariables.focus'); await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame'); diff --git a/test/e2e/areas/top-action-bar/top-action-bar-save.test.ts b/test/e2e/areas/top-action-bar/top-action-bar-save.test.ts index 41c01e732ec..ec8afa7b865 100644 --- a/test/e2e/areas/top-action-bar/top-action-bar-save.test.ts +++ b/test/e2e/areas/top-action-bar/top-action-bar-save.test.ts @@ -33,17 +33,17 @@ test.describe('Top Action Bar - Save Actions', { await app.workbench.positronQuickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); await app.workbench.positronQuickaccess.openFile(join(app.workspacePathOrFolder, fileName)); await app.workbench.positronQuickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false }); - await app.workbench.editors.selectTab(fileName); + await app.workbench.positronEditors.selectTab(fileName); await app.workbench.editor.waitForTypeInEditor(fileName, 'Puppies frolicking in a meadow of wildflowers'); // The file is now "dirty" and the save buttons should be enabled - await app.workbench.editors.waitForTab(fileName, true); + await app.workbench.positronEditors.waitForTab(fileName, true); await expect(async () => { expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy(); }).toPass({ timeout: 10000 }); await app.workbench.positronTopActionBar.saveButton.click(); // The file is now saved, so the file should no longer be "dirty" - await app.workbench.editors.waitForTab(fileName, false); + await app.workbench.positronEditors.waitForTab(fileName, false); await expect(async () => { // The Save button stays enabled even when the active file is not "dirty" expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); @@ -62,21 +62,21 @@ test.describe('Top Action Bar - Save Actions', { await app.workbench.positronQuickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false }); await app.workbench.positronQuickaccess.openFile(join(app.workspacePathOrFolder, fileName2)); await app.workbench.positronQuickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false }); - await app.workbench.editors.selectTab(fileName1); - await app.workbench.editor.waitForTypeInEditor(fileName1, text); - await app.workbench.editors.selectTab(fileName2); - await app.workbench.editor.waitForTypeInEditor(fileName2, text); + await app.workbench.positronEditors.selectTab(fileName1); + await app.workbench.positronEditor.waitForTypeInEditor(fileName1, text); + await app.workbench.positronEditors.selectTab(fileName2); + await app.workbench.positronEditor.waitForTypeInEditor(fileName2, text); // The files are now "dirty" and the save buttons should be enabled - await app.workbench.editors.waitForTab(fileName1, true); - await app.workbench.editors.waitForTab(fileName2, true); + await app.workbench.positronEditors.waitForTab(fileName1, true); + await app.workbench.positronEditors.waitForTab(fileName2, true); await expect(async () => { expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy(); }).toPass({ timeout: 10000 }); await app.workbench.positronTopActionBar.saveAllButton.click(); // The files are now saved, so the files should no longer be "dirty" - await app.workbench.editors.waitForTab(fileName1, false); - await app.workbench.editors.waitForTab(fileName2, false); + await app.workbench.positronEditors.waitForTab(fileName1, false); + await app.workbench.positronEditors.waitForTab(fileName2, false); await expect(async () => { // The Save button stays enabled even when the active file is not "dirty" expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); @@ -91,10 +91,10 @@ test.describe('Top Action Bar - Save Actions', { // Open a new file and type in some text await app.workbench.positronQuickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); await app.workbench.positronQuickaccess.runCommand('workbench.action.files.newUntitledFile', { keepOpen: false }); - await app.workbench.editors.selectTab(fileName); - await app.workbench.editor.waitForTypeInEditor(fileName, text); + await app.workbench.positronEditors.selectTab(fileName); + await app.workbench.positronEditor.waitForTypeInEditor(fileName, text); // The file is now "dirty" and the save buttons should be enabled - await app.workbench.editors.waitForTab(fileName, true); + await app.workbench.positronEditors.waitForTab(fileName, true); await expect(async () => { expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy(); diff --git a/test/e2e/areas/welcome/welcome.test.ts b/test/e2e/areas/welcome/welcome.test.ts index f5c81228309..64796d5a39b 100644 --- a/test/e2e/areas/welcome/welcome.test.ts +++ b/test/e2e/areas/welcome/welcome.test.ts @@ -74,7 +74,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronQuickInput.selectQuickInputElementContaining('Python File'); - await expect(app.workbench.editors.activeEditor.locator(app.workbench.editors.editorIcon)).toHaveClass(/python-lang-file-icon/); + await expect(app.workbench.positronEditors.activeEditor.locator(app.workbench.positronEditors.editorIcon)).toHaveClass(/python-lang-file-icon/); await app.workbench.positronQuickaccess.runCommand('View: Close Editor'); }); @@ -85,7 +85,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronPopups.clickOnModalDialogPopupOption('Python Notebook'); - await expect(app.workbench.editors.activeEditor.locator(app.workbench.editors.editorIcon)).toHaveClass(/ipynb-ext-file-icon/); + await expect(app.workbench.positronEditors.activeEditor.locator(app.workbench.positronEditors.editorIcon)).toHaveClass(/ipynb-ext-file-icon/); const expectedInterpreterVersion = new RegExp(`Python ${process.env.POSITRON_PY_VER_SEL}`, 'i'); await expect(app.workbench.positronNotebooks.kernelLabel).toHaveText(expectedInterpreterVersion); @@ -100,7 +100,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronPopups.clickOnModalDialogPopupOption(expectedInterpreterVersion); // editor is hidden because bottom panel is maximized - await expect(app.workbench.editors.editorPart).not.toBeVisible(); + await expect(app.workbench.positronEditors.editorPart).not.toBeVisible(); // console is the active view in the bottom panel await expect(app.workbench.positronLayouts.panelViewsTab.and(app.code.driver.page.locator('.checked'))).toHaveText('Console'); @@ -113,7 +113,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronQuickInput.selectQuickInputElementContaining('R File'); - await expect(app.workbench.editors.activeEditor.locator(app.workbench.editors.editorIcon)).toHaveClass(/r-lang-file-icon/); + await expect(app.workbench.positronEditors.activeEditor.locator(app.workbench.positronEditors.editorIcon)).toHaveClass(/r-lang-file-icon/); }); test('Click on R console from the Welcome page [C684756]', async function ({ app, r }) { @@ -124,7 +124,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronPopups.clickOnModalDialogPopupOption(expectedInterpreterVersion); // editor is hidden because bottom panel is maximized - await expect(app.workbench.editors.editorPart).not.toBeVisible(); + await expect(app.workbench.positronEditors.editorPart).not.toBeVisible(); // console is the active view in the bottom panel await expect(app.workbench.positronLayouts.panelViewsTab.and(app.code.driver.page.locator('.checked'))).toHaveText('Console'); @@ -135,7 +135,7 @@ test.describe('Welcome Page', { tag: [tags.WELCOME] }, () => { await app.workbench.positronPopups.clickOnModalDialogPopupOption('R Notebook'); - await expect(app.workbench.editors.activeEditor.locator(app.workbench.editors.editorIcon)).toHaveClass(/ipynb-ext-file-icon/); + await expect(app.workbench.positronEditors.activeEditor.locator(app.workbench.positronEditors.editorIcon)).toHaveClass(/ipynb-ext-file-icon/); const expectedInterpreterVersion = new RegExp(`R ${process.env.POSITRON_R_VER_SEL}`, 'i'); await expect(app.workbench.positronNotebooks.kernelLabel).toHaveText(expectedInterpreterVersion);