Skip to content

Commit

Permalink
Register top action bar Save and Save All button commands (#3889)
Browse files Browse the repository at this point in the history
## Description

- Addresses #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 <[email protected]>
  • Loading branch information
sharon-wang and jonvanausdeln authored Jul 8, 2024
1 parent d545aeb commit e246589
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -167,8 +168,8 @@ export const PositronTopActionBar = (props: PositronTopActionBarProps) => {
/>
<ActionBarCommandButton
iconId='positron-save-all'
commandId={SAVE_FILES}
ariaLabel={CommandCenter.title(SAVE_FILES)}
commandId={SAVE_ALL}
ariaLabel={CommandCenter.title(SAVE_ALL)}
/>
</ActionBarRegion>

Expand Down
22 changes: 22 additions & 0 deletions src/vs/workbench/contrib/files/browser/fileActions.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down
8 changes: 8 additions & 0 deletions test/automation/src/positron/positronBaseElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export class PositronBaseElement {
async hover(): Promise<void> {
await this.code.driver.getLocator(this.myselector).hover();
}

async isDisabled(): Promise<boolean> {
return await this.code.driver.getLocator(this.myselector).isDisabled();
}

async isEnabled(): Promise<boolean> {
return await this.code.driver.getLocator(this.myselector).isEnabled();
}
}

export class PositronTextElement extends PositronBaseElement {
Expand Down
27 changes: 27 additions & 0 deletions test/automation/src/positron/positronTopActionBar.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions test/automation/src/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -76,6 +77,7 @@ export class Workbench {
readonly positronExplorer: PositronExplorer;
readonly positronConnections: PositronConnections;
readonly positronHelp: PositronHelp;
readonly positronTopActionBar: PositronTopActionBar;
// --- End Positron ---

constructor(code: Code) {
Expand Down Expand Up @@ -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 ---
}
}
113 changes: 113 additions & 0 deletions test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});

});

}

2 changes: 2 additions & 0 deletions test/smoke/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '..', '..', '..');
Expand Down Expand Up @@ -440,5 +441,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => {
setupXLSXDataFrameTest(logger);
setupHelpTest(logger);
setupClipboardTest(logger);
setupTopActionBarTest(logger);
// --- End Positron ---
});

0 comments on commit e246589

Please sign in to comment.