From e246589c08fd5d1342aa728060d1c331b5e9a989 Mon Sep 17 00:00:00 2001 From: sharon Date: Mon, 8 Jul 2024 20:32:07 +0200 Subject: [PATCH] Register top action bar Save and Save All button commands (#3889) ## Description - Addresses https://github.com/posit-dev/positron/issues/2276 ### Changes - registers the Save and Save All commands to the `CommandCenter` so that they are available in the top action bar - add smoke test for the Save/Save All top action bar buttons and some scaffolding for future top action bar smoke tests - adds `isDisabled()` and `isEnabled()` to `PositronBaseElement` ### Demo https://github.com/posit-dev/positron/assets/25834218/6b6fb56c-5829-4700-8fda-8739cb678d8c ### QA Notes Please add new TestRail items for these new tests. --------- Co-authored-by: Jon Vanausdeln --- .../positronTopActionBar.tsx | 9 +- .../files/browser/fileActions.contribution.ts | 22 ++++ .../src/positron/positronBaseElement.ts | 8 ++ .../src/positron/positronTopActionBar.ts | 27 +++++ test/automation/src/workbench.ts | 3 + .../top-action-bar/top-action-bar.test.ts | 113 ++++++++++++++++++ test/smoke/src/main.ts | 2 + 7 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 test/automation/src/positron/positronTopActionBar.ts create mode 100644 test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts diff --git a/src/vs/workbench/browser/parts/positronTopActionBar/positronTopActionBar.tsx b/src/vs/workbench/browser/parts/positronTopActionBar/positronTopActionBar.tsx index 99d92d90d09..99b6962291b 100644 --- a/src/vs/workbench/browser/parts/positronTopActionBar/positronTopActionBar.tsx +++ b/src/vs/workbench/browser/parts/positronTopActionBar/positronTopActionBar.tsx @@ -38,13 +38,14 @@ import { PositronTopActionBarContextProvider } from 'vs/workbench/browser/parts/ import { TopActionBarCustonFolderMenu } from 'vs/workbench/browser/parts/positronTopActionBar/components/topActionBarCustomFolderMenu'; import { ILanguageRuntimeMetadata, ILanguageRuntimeService } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; import { TopActionBarInterpretersManager } from 'vs/workbench/browser/parts/positronTopActionBar/components/topActionBarInterpretersManager'; +import { SAVE_ALL_COMMAND_ID, SAVE_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; // Constants. const kHorizontalPadding = 4; const kCenterUIBreak = 600; const kFulllCenterUIBreak = 765; -const SAVE = 'workbench.action.files.save'; -const SAVE_FILES = 'workbench.action.files.saveFiles'; +const SAVE = SAVE_FILE_COMMAND_ID; +const SAVE_ALL = SAVE_ALL_COMMAND_ID; const NAV_BACK = NavigateBackwardsAction.ID; const NAV_FORWARD = NavigateForwardAction.ID; @@ -167,8 +168,8 @@ export const PositronTopActionBar = (props: PositronTopActionBarProps) => { /> diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 7abd58ad0a6..3e60e79a125 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -28,6 +28,10 @@ import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { Codicon } from 'vs/base/common/codicons'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +// --- Start Positron -- +import { CommandCenter } from 'vs/platform/commandCenter/common/commandCenter'; +// --- End Positron -- + // Contribute Global Actions registerAction2(GlobalCompareResourcesAction); @@ -706,6 +710,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { order: 1 }); +// --- Start Positron -- +// Add the Save command to the Command Center, so it can be used in the top action bar. +CommandCenter.addCommandInfo({ + id: SAVE_FILE_COMMAND_ID, + title: nls.localize('Save', "Save"), + precondition: ContextKeyExpr.or(ActiveEditorContext, ContextKeyExpr.and(FoldersViewVisibleContext, SidebarFocusContext)) +}); +// --- End Positron -- + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { @@ -726,6 +739,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { order: 3 }); +// --- Start Positron -- +// Add the Save All command to the Command Center, so it can be used in the top action bar. +CommandCenter.addCommandInfo({ + id: SAVE_ALL_COMMAND_ID, + title: nls.localize('Save All', "Save All"), + precondition: DirtyWorkingCopiesContext +}); +// --- End Positron -- + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '5_autosave', command: { diff --git a/test/automation/src/positron/positronBaseElement.ts b/test/automation/src/positron/positronBaseElement.ts index 86097444678..3afbc5cfbe1 100644 --- a/test/automation/src/positron/positronBaseElement.ts +++ b/test/automation/src/positron/positronBaseElement.ts @@ -31,6 +31,14 @@ export class PositronBaseElement { async hover(): Promise { await this.code.driver.getLocator(this.myselector).hover(); } + + async isDisabled(): Promise { + return await this.code.driver.getLocator(this.myselector).isDisabled(); + } + + async isEnabled(): Promise { + return await this.code.driver.getLocator(this.myselector).isEnabled(); + } } export class PositronTextElement extends PositronBaseElement { diff --git a/test/automation/src/positron/positronTopActionBar.ts b/test/automation/src/positron/positronTopActionBar.ts new file mode 100644 index 00000000000..d97117f04cf --- /dev/null +++ b/test/automation/src/positron/positronTopActionBar.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Code } from '../code'; +import { PositronBaseElement } from './positronBaseElement'; + +const POSITRON_TOP_ACTION_BAR = 'div[id="workbench.parts.positron-top-action-bar"]'; +const POSITRON_TOP_ACTION_SAVE_BUTTON = 'div[id="workbench.parts.positron-top-action-bar"] .action-bar-region-left .action-bar-button[aria-label="Save"]'; +const POSITRON_TOP_ACTION_SAVE_ALL_BUTTON = 'div[id="workbench.parts.positron-top-action-bar"] .action-bar-region-left .action-bar-button[aria-label="Save All"]'; + +/* + * Reuseable Positron top action bar functionality for tests to leverage. + */ +export class PositronTopActionBar { + topActionBar: PositronBaseElement; + saveButton: PositronBaseElement; + saveAllButton: PositronBaseElement; + + constructor(private code: Code) { + this.topActionBar = new PositronBaseElement(POSITRON_TOP_ACTION_BAR, this.code); + this.saveButton = new PositronBaseElement(POSITRON_TOP_ACTION_SAVE_BUTTON, this.code); + this.saveAllButton = new PositronBaseElement(POSITRON_TOP_ACTION_SAVE_ALL_BUTTON, this.code); + } +} diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index 18522d5032f..0b32b5c5392 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -36,6 +36,7 @@ import { PositronNewProjectWizard } from './positron/positronNewProjectWizard'; import { PositronExplorer } from './positron/positronExplorer'; import { PositronConnections } from './positron/positronConnections'; import { PositronHelp } from './positron/positronHelp'; +import { PositronTopActionBar } from './positron/positronTopActionBar'; // --- End Positron --- export interface Commands { @@ -76,6 +77,7 @@ export class Workbench { readonly positronExplorer: PositronExplorer; readonly positronConnections: PositronConnections; readonly positronHelp: PositronHelp; + readonly positronTopActionBar: PositronTopActionBar; // --- End Positron --- constructor(code: Code) { @@ -111,6 +113,7 @@ export class Workbench { this.positronExplorer = new PositronExplorer(code); this.positronConnections = new PositronConnections(code, this.quickaccess); this.positronHelp = new PositronHelp(code); + this.positronTopActionBar = new PositronTopActionBar(code); // --- End Positron --- } } diff --git a/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts b/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts new file mode 100644 index 00000000000..c2f471ca209 --- /dev/null +++ b/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { join } from 'path'; +import { expect } from '@playwright/test'; +import { Application, Logger } from '../../../../../automation'; +import { installAllHandlers } from '../../../utils'; + +/* + * Top Action Bar test cases + */ +export function setup(logger: Logger) { + describe('Top Action Bar', () => { + // Shared before/after handling + installAllHandlers(logger); + + describe('Save Actions', () => { + before(async function () { + + }); + + it('Save and Save All both disabled when no unsaved editors are open [C656253]', async function () { + const app = this.app as Application; + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); + expect(await app.workbench.positronTopActionBar.saveButton.isDisabled()).toBeTruthy(); + expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy(); + }); + + it('Save enabled and Save All disabled when a single unsaved file is open [C656254]', async function () { + const app = this.app as Application; + const fileName = 'README.md'; + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName)); + await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false }); + await app.workbench.editors.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 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 expect(async () => { + // The Save button stays enabled even when the active file is not "dirty" + expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); + // The Save All button is disabled when less than 2 files are "dirty" + expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy(); + }).toPass({ timeout: 10000 }); + }); + + it('Save and Save All both enabled when multiple unsaved files are open [C656255]', async function () { + const app = this.app as Application; + const fileName1 = 'README.md'; + const fileName2 = 'DESCRIPTION'; + const text = 'Kittens playing with yarn'; + // Open two files and type in some text + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName1)); + await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false }); + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName2)); + await app.workbench.quickaccess.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); + // 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 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 expect(async () => { + // The Save button stays enabled even when the active file is not "dirty" + expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy(); + // The Save All button is disabled when less than 2 files are "dirty" + expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy(); + }).toPass({ timeout: 10000 }); + }); + + it('Save and Save All both enabled when an unsaved new file is open [C656253]', async function () { + const app = this.app as Application; + const fileName = 'Untitled-1'; + const text = 'Bunnies hopping through a field of clover'; + // Open a new file and type in some text + await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false }); + await app.workbench.quickaccess.runCommand('workbench.action.files.newUntitledFile', { keepOpen: false }); + await app.workbench.editors.selectTab(fileName); + await app.workbench.editor.waitForTypeInEditor(fileName, text); + // The file is now "dirty" and the save buttons should be enabled + await app.workbench.editors.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 }); + // We won't try to click the Save buttons because a system dialog will pop up and we + // can't automate interactions with the native file dialog + }); + }); + + }); + +} + diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 0b5635108bd..d2461f3f023 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -42,6 +42,7 @@ import { setup as setupNewProjectWizardTest } from './areas/positron/new-project import { setup as setupXLSXDataFrameTest } from './areas/positron/dataexplorer/xlsxDataFrame.test'; import { setup as setupHelpTest } from './areas/positron/help/help.test'; import { setup as setupClipboardTest} from './areas/positron/console/consoleClipboard.test' +import { setup as setupTopActionBarTest } from './areas/positron/top-action-bar/top-action-bar.test'; // --- End Positron --- const rootPath = path.join(__dirname, '..', '..', '..'); @@ -440,5 +441,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { setupXLSXDataFrameTest(logger); setupHelpTest(logger); setupClipboardTest(logger); + setupTopActionBarTest(logger); // --- End Positron --- });