diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index e782029440e..4ecd0454eb0 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -271,6 +271,13 @@ export class ViewsService extends Disposable implements IViewsService { } else if (location === ViewContainerLocation.Panel) { this.paneCompositeService.hideActivePaneComposite(location); } + + // The blur event doesn't fire on WebKit when the focused element is hidden, + // so the context key needs to be forced here too otherwise a view may still + // think it's showing, breaking toggle commands. + if (this.focusedViewContextKey.get() === id) { + this.focusedViewContextKey.reset(); + } } else { view.setExpanded(false); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 01c66be16e9..f2b5da0aeb2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -46,6 +46,9 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isAbsolute } from 'vs/base/common/path'; +import { ITerminalQuickPickItem } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { getIconId, getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; // allow-any-unicode-next-line export const switchTerminalActionViewItemSeparator = '─────────'; @@ -1582,6 +1585,52 @@ export function registerTerminalActions() { } } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.Join, + title: { value: localize('workbench.action.terminal.join', "Join Terminals"), original: 'Join Terminals' }, + category, + f1: true, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated)) + }); + } + async run(accessor: ServicesAccessor) { + const themeService = accessor.get(IThemeService); + const groupService = accessor.get(ITerminalGroupService); + const picks: ITerminalQuickPickItem[] = []; + if (!groupService.activeInstance || groupService.instances.length === 1) { + return; + } + const otherInstances = groupService.instances.filter(i => i.instanceId !== groupService.activeInstance?.instanceId); + for (const terminal of otherInstances) { + const group = groupService.getGroupForInstance(terminal); + if (group?.terminalInstances.length === 1) { + const iconId = getIconId(terminal); + const label = `$(${iconId}): ${terminal.title}`; + const iconClasses: string[] = []; + const colorClass = getColorClass(terminal); + if (colorClass) { + iconClasses.push(colorClass); + } + const uriClasses = getUriClasses(terminal, themeService.getColorTheme().type); + if (uriClasses) { + iconClasses.push(...uriClasses); + } + picks.push({ + terminal, + label, + iconClasses + }); + } + } + const result = await accessor.get(IQuickInputService).pick(picks, {}); + if (result) { + groupService.joinInstances([result.terminal, groupService.activeInstance!]); + } + } + } + ); registerAction2(class extends Action2 { constructor() { super({ @@ -1736,6 +1785,24 @@ export function registerTerminalActions() { } } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.KillAll, + title: { value: localize('workbench.action.terminal.killAll', "Kill All Terminals"), original: 'Kill All Terminals' }, + f1: true, + category, + precondition: ContextKeyExpr.or(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen), + icon: Codicon.trash + }); + } + async run(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + for (const instance of terminalService.instances) { + await terminalService.safeDisposeTerminal(instance); + } + } + }); registerAction2(class extends Action2 { constructor() { super({ @@ -1818,7 +1885,7 @@ export function registerTerminalActions() { constructor() { super({ id: TerminalCommandId.SelectDefaultProfile, - title: { value: localize('workbench.action.terminal.selectDefaultProfile', "Select Default Profile"), original: 'Select Default Profile' }, + title: { value: localize('workbench.action.terminal.selectDefaultShell', "Select Default Profile"), original: 'Select Default Profile' }, f1: true, category, precondition: TerminalContextKeys.processSupported diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index 4f56bbebdee..30da8845d02 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -20,7 +20,6 @@ import { getInstanceFromResource } from 'vs/workbench/contrib/terminal/browser/t import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; export class TerminalGroupService extends Disposable implements ITerminalGroupService, ITerminalFindHost { declare _serviceBrand: undefined; @@ -60,7 +59,6 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe @IContextKeyService private _contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IViewsService private readonly _viewsService: IViewsService, - @IWorkbenchLayoutService private _layoutService: IWorkbenchLayoutService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { @@ -81,7 +79,7 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe if (location === ViewContainerLocation.Panel) { const panel = this._viewDescriptorService.getViewContainerByViewId(TERMINAL_VIEW_ID); if (panel && this._viewDescriptorService.getViewContainerModel(panel).activeViewDescriptors.length === 1) { - this._layoutService.setPartHidden(true, Parts.PANEL_PART); + this._viewsService.closeView(TERMINAL_VIEW_ID); TerminalContextKeys.tabsMouse.bindTo(this._contextKeyService).set(false); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index e7dac04a1c1..1f2efc11842 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -12,7 +12,8 @@ import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/brow import * as nls from 'vs/nls'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IQuickPickTerminalObject } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IQuickPickTerminalObject, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; type DefaultProfileName = string; @@ -238,3 +239,7 @@ export interface IProfileQuickPickItem extends IQuickPickItem { profileName: string; keyMods?: IKeyMods | undefined; } + +export interface ITerminalQuickPickItem extends IPickerQuickAccessItem { + terminal: ITerminalInstance +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 4344e2f0a19..873f4e0ea91 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -76,9 +76,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider this._commandService.executeCommand(TerminalCommandId.NewWithProfile) }); - return terminalPicks; - } private _createPick(terminal: ITerminalInstance, terminalIndex: number, filter: string, groupIndex?: number): IPickerQuickAccessItem | undefined { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 0895d2d7eed..b95ad260ca9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -889,7 +889,7 @@ export class TerminalService implements ITerminalService { // Launch the contributed profile if (contributedProfile) { const resolvedLocation = this.resolveLocation(options?.location); - const splitActiveTerminal = typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? options.location.splitActiveTerminal : false; + const splitActiveTerminal = typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? options.location.splitActiveTerminal : typeof options?.location === 'object' ? 'parentTerminal' in options.location : false; let location: TerminalLocation | { viewColumn: number, preserveState?: boolean } | { splitActiveTerminal: boolean } | undefined; if (splitActiveTerminal) { location = resolvedLocation === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 35bdcce9826..c155c2edd45 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -430,6 +430,7 @@ export const enum TerminalCommandId { Kill = 'workbench.action.terminal.kill', KillEditor = 'workbench.action.terminal.killEditor', KillInstance = 'workbench.action.terminal.killInstance', + KillAll = 'workbench.action.terminal.killAll', QuickKill = 'workbench.action.terminal.quickKill', ConfigureTerminalSettings = 'workbench.action.terminal.openSettings', CopySelection = 'workbench.action.terminal.copySelection', @@ -450,6 +451,7 @@ export const enum TerminalCommandId { Unsplit = 'workbench.action.terminal.unsplit', UnsplitInstance = 'workbench.action.terminal.unsplitInstance', JoinInstance = 'workbench.action.terminal.joinInstance', + Join = 'workbench.action.terminal.join', Relaunch = 'workbench.action.terminal.relaunch', FocusPreviousPane = 'workbench.action.terminal.focusPreviousPane', ShowTabs = 'workbench.action.terminal.showTabs', diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 6f7a8a7e9ee..4357b5aef99 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -225,14 +225,14 @@ async function poll( if (trial > retryCount) { console.error('** Timeout!'); console.error(lastError); - + console.error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`); } let result; try { result = await fn(); - + console.log('DEBUG: poll result', result); if (acceptFn(result)) { return result; } else { @@ -250,7 +250,7 @@ async function poll( export class Code { private _activeWindowId: number | undefined = undefined; - private driver: IDriver; + driver: IDriver; constructor( private client: IDisposable, diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 4ea07cb4cc4..b469974df25 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -41,12 +41,13 @@ function buildDriver(browser: playwright.Browser, context: playwright.BrowserCon class PlaywrightDriver implements IDriver { _serviceBrand: undefined; - + page: playwright.Page; constructor( private readonly _browser: playwright.Browser, private readonly _context: playwright.BrowserContext, private readonly _page: playwright.Page ) { + this.page = _page; } async getWindowIds() { return [1]; } @@ -70,6 +71,12 @@ class PlaywrightDriver implements IDriver { if (i > 0) { await timeout(100); } + + if (keybinding.startsWith('Alt') || keybinding.startsWith('Control')) { + await this._page.keyboard.press(keybinding); + return; + } + const keys = chord.split('+'); const keysDown: string[] = []; for (let i = 0; i < keys.length; i++) { diff --git a/test/automation/src/quickaccess.ts b/test/automation/src/quickaccess.ts index ff8131f5f81..86438768f37 100644 --- a/test/automation/src/quickaccess.ts +++ b/test/automation/src/quickaccess.ts @@ -79,14 +79,14 @@ export class QuickAccess { await this.editors.waitForEditorFocus(fileName); } - async runCommand(commandId: string): Promise { + async runCommand(commandId: string, keepOpen?: boolean): Promise { await this.openQuickAccess(`>${commandId}`); // wait for best choice to be focused await this.code.waitForTextContent(QuickInput.QUICK_INPUT_FOCUSED_ELEMENT); // wait and click on best choice - await this.quickInput.selectQuickInputElement(0); + await this.quickInput.selectQuickInputElement(0, keepOpen); } async openQuickOutline(): Promise { diff --git a/test/automation/src/quickinput.ts b/test/automation/src/quickinput.ts index 70b37ea9e2d..78170370fd9 100644 --- a/test/automation/src/quickinput.ts +++ b/test/automation/src/quickinput.ts @@ -39,12 +39,14 @@ export class QuickInput { await this.code.waitForElement(QuickInput.QUICK_INPUT, r => !!r && r.attributes.style.indexOf('display: none;') !== -1); } - async selectQuickInputElement(index: number): Promise { + async selectQuickInputElement(index: number, keepOpen?: boolean): Promise { await this.waitForQuickInputOpened(); for (let from = 0; from < index; from++) { await this.code.dispatchKeybinding('down'); } await this.code.dispatchKeybinding('enter'); - await this.waitForQuickInputClosed(); + if (!keepOpen) { + await this.waitForQuickInputClosed(); + } } } diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 16a6dae6197..42afbd78bf7 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -3,30 +3,84 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IElement, QuickInput } from '.'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; const TERMINAL_VIEW_SELECTOR = `#terminal`; const XTERM_SELECTOR = `${TERMINAL_VIEW_SELECTOR} .terminal-wrapper`; -const XTERM_TEXTAREA = `${XTERM_SELECTOR} textarea.xterm-helper-textarea`; +const CONTRIBUTED_PROFILE_NAME = `JavaScript Debug Terminal`; +const TABS = '.tabs-list .terminal-tabs-entry'; +const XTERM_FOCUSED_SELECTOR = '.terminal.xterm.focus'; + +const enum TerminalCommandId { + Rename = 'workbench.action.terminal.rename', + ChangeColor = 'workbench.action.terminal.changeColor', + ChangeIcon = 'workbench.action.terminal.changeIcon', + Split = 'workbench.action.terminal.split', + KillAll = 'workbench.action.terminal.killAll', + Unsplit = 'workbench.action.terminal.unsplit', + Join = 'workbench.action.terminal.join', + Show = 'workbench.action.terminal.toggleTerminal', + CreateNew = 'workbench.action.terminal.new', + NewWithProfile = 'workbench.action.terminal.newWithProfile', + SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell' +} export class Terminal { - constructor(private code: Code, private quickaccess: QuickAccess) { } + constructor(private code: Code, private quickaccess: QuickAccess, private quickinput: QuickInput) { } - async showTerminal(): Promise { - await this.quickaccess.runCommand('workbench.action.terminal.toggleTerminal'); - await this.code.waitForActiveElement(XTERM_TEXTAREA); - await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0)); + // TODO: Strongly type using non-const enum TerminalCommandId + async runCommand(commandId: string, value?: string): Promise { + await this.quickaccess.runCommand(commandId, !!value || commandId === TerminalCommandId.Join); + if (commandId === TerminalCommandId.Show || commandId === TerminalCommandId.CreateNew) { + return await this._waitForTerminal(); + } + if (value) { + await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, value); + } + await this.code.dispatchKeybinding('enter'); + await this.quickinput.waitForQuickInputClosed(); } - async runCommand(commandText: string): Promise { + async runCommandInTerminal(commandText: string): Promise { await this.code.writeInTerminal(XTERM_SELECTOR, commandText); // hold your horses await new Promise(c => setTimeout(c, 500)); await this.code.dispatchKeybinding('enter'); } + // TODO: Return something more robust: + // export interface ITerminalInstance { + // name: string; + // icon: string; + // } + // export type TerminalGroup = ITerminalInstance[]; + // export type TerminalLayout = TerminalGroup[]; + async getTabLabels(expectedCount: number, splits?: boolean, accept?: (result: IElement[]) => boolean): Promise { + const result: string[] = []; + const tabs = await this.code.waitForElements(TABS, true, e => accept ? accept(e) : e.length === expectedCount && (!splits || e.some(e => e.textContent.startsWith('┌'))) && e.every(element => element.textContent.trim().length > 1)); + for (const t of tabs) { + result.push(t.textContent); + } + if (!result[0].startsWith('┌')) { + const first = result[1]; + const second = result[0]; + return [first, second]; + } + return result; + } + + async runProfileCommand(command: string, contributed?: boolean, altKey?: boolean): Promise { + await this.quickaccess.runCommand(command, true); + if (contributed) { + await this.code.waitForSetValue(QuickInput.QUICK_INPUT_INPUT, CONTRIBUTED_PROFILE_NAME); + } + await this.code.dispatchKeybinding(altKey ? 'Alt+Enter' : 'enter'); + await this.quickinput.waitForQuickInputClosed(); + } + async waitForTerminalText(accept: (buffer: string[]) => boolean, message?: string): Promise { try { await this.code.waitForTerminalBuffer(XTERM_SELECTOR, accept); @@ -37,4 +91,13 @@ export class Terminal { throw err; } } + + async getPage(): Promise { + return (this.code.driver as any).page; + } + + private async _waitForTerminal(): Promise { + await this.code.waitForElement(XTERM_FOCUSED_SELECTOR); + await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0)); + } } diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index c2f5abe8096..b5c675fac7f 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -61,7 +61,7 @@ export class Workbench { this.problems = new Problems(code, this.quickaccess); this.settingsEditor = new SettingsEditor(code, userDataPath, this.editors, this.editor, this.quickaccess); this.keybindingsEditor = new KeybindingsEditor(code); - this.terminal = new Terminal(code, this.quickaccess); + this.terminal = new Terminal(code, this.quickaccess, this.quickinput); this.notebook = new Notebook(this.quickaccess, code); this.localization = new Localization(code); } diff --git a/test/smoke/src/areas/terminal/terminal-profiles.test.ts b/test/smoke/src/areas/terminal/terminal-profiles.test.ts index c4fe5e9d789..0f202d6e40c 100644 --- a/test/smoke/src/areas/terminal/terminal-profiles.test.ts +++ b/test/smoke/src/areas/terminal/terminal-profiles.test.ts @@ -5,31 +5,105 @@ import { ok } from 'assert'; import { ParsedArgs } from 'minimist'; -import { Application } from '../../../../automation'; +import { Code, Terminal } from '../../../../automation'; import { afterSuite, beforeSuite } from '../../utils'; -export function setup(opts: ParsedArgs) { - describe('Terminal Profiles', () => { - let app: Application; +const ContributedProfileName = `JavaScript Debug Terminal`; +export function setup(opts: ParsedArgs) { + + describe('Terminal Profiles', () => { + let code: Code; + let terminal: Terminal; + const enum TerminalCommandId { + Split = 'workbench.action.terminal.split', + KillAll = 'workbench.action.terminal.killAll', + Show = 'workbench.action.terminal.toggleTerminal', + CreateNew = 'workbench.action.terminal.new', + NewWithProfile = 'workbench.action.terminal.newWithProfile', + SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell' + } beforeSuite(opts); afterSuite(opts); before(function () { - app = this.app; + code = this.app.code; + terminal = this.app.workbench.terminal; }); - it('should launch the default profile', async function () { - await app.workbench.terminal.showTerminal(); + afterEach(async () => { + await terminal.runCommand(TerminalCommandId.KillAll); + }); - // Verify the terminal buffer has some content - await app.workbench.terminal.waitForTerminalText(buffer => { - return buffer.some(e => e.length > 0); - }, 'The terminal buffer should have some content'); + it('should launch the default profile', async () => { + await terminal.runCommand(TerminalCommandId.Show); + // TODO: Use getSingleTabLabel? Share logic with getTabLabel? + await code.waitForElement('.single-terminal-tab', e => e ? !e.textContent.endsWith(ContributedProfileName) : false); + }); - // Verify the terminal single tab shows up and has a title - const terminalTab = await app.code.waitForElement('.single-terminal-tab'); - ok(terminalTab.textContent.trim().length > 0); + it.skip('should set the default profile to a contributed one', async () => { + await terminal.runProfileCommand(TerminalCommandId.SelectDefaultProfile, true); + await terminal.runCommand(TerminalCommandId.CreateNew); + await code.waitForElement('.single-terminal-tab', e => e ? e.textContent.endsWith(ContributedProfileName) : false); + }); + + it.skip('should use the default contributed profile on panel open and for splitting', async () => { + await terminal.runProfileCommand(TerminalCommandId.SelectDefaultProfile, true); + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Split); + const tabs = await terminal.getTabLabels(2); + console.log('DEBUG: tabs', tabs); + ok(tabs[0].startsWith('┌') && tabs[0].endsWith(ContributedProfileName)); + ok(tabs[1].startsWith('└') && tabs[1].endsWith(ContributedProfileName)); + }); + + it('should set the default profile', async () => { + await terminal.runProfileCommand(TerminalCommandId.SelectDefaultProfile, undefined); + await terminal.runCommand(TerminalCommandId.CreateNew); + await code.waitForElement('.single-terminal-tab', e => e ? !e.textContent.endsWith(ContributedProfileName) : false); + }); + + it('should use the default profile on panel open and for splitting', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await code.waitForElement('.single-terminal-tab', e => e ? !e.textContent.endsWith(ContributedProfileName) : false); + await terminal.runCommand(TerminalCommandId.Split); + const tabs = await terminal.getTabLabels(2, true); + ok(tabs[0].startsWith('┌') && !tabs[0].endsWith(ContributedProfileName)); + ok(tabs[1].startsWith('└') && !tabs[1].endsWith(ContributedProfileName)); + }); + + it('clicking the plus button should create a terminal and display the tabs view showing no split decorations', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await code.waitAndClick('li.action-item.monaco-dropdown-with-primary > div.action-container.menu-entry > a'); + const tabLabels = await terminal.getTabLabels(2); + ok(!tabLabels[0].startsWith('┌') && !tabLabels[1].startsWith('└')); + }); + + it('createWithProfile command should create a terminal with a profile', async () => { + await terminal.runProfileCommand(TerminalCommandId.NewWithProfile); + await code.waitForElement('.single-terminal-tab', e => e ? !e.textContent.endsWith(ContributedProfileName) : false); + }); + + it.skip('createWithProfile command should create a terminal with a contributed profile', async () => { + await terminal.runProfileCommand(TerminalCommandId.NewWithProfile, true); + await code.waitForElement('.single-terminal-tab', e => e ? e.textContent.endsWith(ContributedProfileName) : false); + }); + + it('createWithProfile command should create a split terminal with a profile', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runProfileCommand(TerminalCommandId.NewWithProfile, undefined, true); + const tabs = await terminal.getTabLabels(2, true); + ok(tabs[0].startsWith('┌') && !tabs[0].endsWith(ContributedProfileName)); + ok(tabs[1].startsWith('└') && !tabs[1].endsWith(ContributedProfileName)); + }); + + it.skip('createWithProfile command should create a split terminal with a contributed profile', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await code.waitForElement('.single-terminal-tab', e => e ? !e.textContent.endsWith(ContributedProfileName) : false); + await terminal.runProfileCommand(TerminalCommandId.NewWithProfile, true, true); + const tabs = await terminal.getTabLabels(2, true); + ok(tabs[0].startsWith('┌') && !tabs[0].endsWith(ContributedProfileName)); + ok(tabs[1].startsWith('└') && tabs[1].endsWith(ContributedProfileName)); }); }); } diff --git a/test/smoke/src/areas/terminal/terminal-tabs.test.ts b/test/smoke/src/areas/terminal/terminal-tabs.test.ts new file mode 100644 index 00000000000..1a5897eb0fd --- /dev/null +++ b/test/smoke/src/areas/terminal/terminal-tabs.test.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok } from 'assert'; +import { ParsedArgs } from 'minimist'; +import { Code, Terminal } from '../../../../automation/out'; +import { afterSuite, beforeSuite } from '../../utils'; + +export function setup(opts: ParsedArgs) { + // TODO: Re-enable when stable + describe.skip('Terminal Tabs', () => { + let code: Code; + let terminal: Terminal; + + // TODO: Move into automation/terminal + const enum TerminalCommandId { + Rename = 'workbench.action.terminal.rename', + ChangeColor = 'workbench.action.terminal.changeColor', + ChangeIcon = 'workbench.action.terminal.changeIcon', + Split = 'workbench.action.terminal.split', + KillAll = 'workbench.action.terminal.killAll', + Unsplit = 'workbench.action.terminal.unsplit', + Join = 'workbench.action.terminal.join', + Show = 'workbench.action.terminal.toggleTerminal', + CreateNew = 'workbench.action.terminal.new' + } + + beforeSuite(opts); + afterSuite(opts); + + before(function () { + code = this.app.code; + terminal = this.app.workbench.terminal; + }); + + afterEach(async () => { + await terminal.runCommand(TerminalCommandId.KillAll); + }); + + it('clicking the plus button should create a terminal and display the tabs view showing no split decorations', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await code.waitAndClick('li.action-item.monaco-dropdown-with-primary > div.action-container.menu-entry > a'); + const tabLabels = await terminal.getTabLabels(2); + ok(!tabLabels[0].startsWith('┌') && !tabLabels[1].startsWith('└')); + }); + + it('should update color of the single tab', async () => { + await terminal.runCommand(TerminalCommandId.Show); + const color = 'Cyan'; + await terminal.runCommand(TerminalCommandId.ChangeColor, color); + const singleTab = await code.waitForElement('.single-terminal-tab'); + ok(singleTab.className.includes(`terminal-icon-terminal_ansi${color}`)); + }); + + it('should update color of the tab in the tabs list', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Split); + const tabs = await terminal.getTabLabels(2); + ok(tabs[0].startsWith('┌')); + ok(tabs[1].startsWith('└')); + const color = 'Cyan'; + await terminal.runCommand(TerminalCommandId.ChangeColor, color); + await code.waitForElement(`.terminal-tabs-entry .terminal-icon-terminal_ansi${color}`); + }); + + it('should update icon of the single tab', async () => { + await terminal.runCommand(TerminalCommandId.Show); + const icon = 'symbol-method'; + await terminal.runCommand(TerminalCommandId.ChangeIcon, icon); + await code.waitForElement(`.single-terminal-tab .codicon-${icon}`); + }); + + it('should update icon of the tab in the tabs list', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Split); + const tabs = await terminal.getTabLabels(2); + ok(tabs[0].startsWith('┌')); + ok(tabs[1].startsWith('└')); + const icon = 'symbol-method'; + await terminal.runCommand(TerminalCommandId.ChangeIcon, icon); + await code.waitForElement(`.terminal-tabs-entry .codicon-${icon}`); + }); + + it('should rename the single tab', async () => { + await terminal.runCommand(TerminalCommandId.Show); + const name = 'my terminal name'; + await terminal.runCommand(TerminalCommandId.Rename, name); + await code.waitForElement('.single-terminal-tab', e => e ? e?.textContent.includes(name) : false); + }); + + it('should rename the tab in the tabs list', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Split); + const name = 'my terminal name'; + await terminal.runCommand(TerminalCommandId.Rename, name); + await terminal.getTabLabels(2, true, t => t.some(element => element.textContent.includes(name))); + }); + + it('should create a split terminal when single tab is alt clicked', async () => { + await terminal.runCommand(TerminalCommandId.Show); + const page = await terminal.getPage(); + page.keyboard.down('Alt'); + await code.waitAndClick('.single-terminal-tab'); + page.keyboard.up('Alt'); + await terminal.getTabLabels(2, true); + }); + + it('should do nothing when join tabs is run with only one terminal', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Join); + await code.waitForElement('.single-terminal-tab'); + }); + + it('should join tabs when more than one terminal', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.CreateNew); + await terminal.runCommand(TerminalCommandId.Join); + await terminal.getTabLabels(2, true); + }); + + it('should do nothing when unsplit tabs called with no splits', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.CreateNew); + await terminal.getTabLabels(2, false); + await terminal.runCommand(TerminalCommandId.Unsplit); + await terminal.getTabLabels(2, false); + }); + + it('should unsplit tabs', async () => { + await terminal.runCommand(TerminalCommandId.Show); + await terminal.runCommand(TerminalCommandId.Split); + await terminal.getTabLabels(2, true); + await terminal.runCommand(TerminalCommandId.Unsplit); + await terminal.getTabLabels(2, false, t => t.every(label => !label.textContent.startsWith('┌') && !label.textContent.startsWith('└'))); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 1cf54e68e77..7f95e942bec 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -28,6 +28,7 @@ import { setup as setupMultirootTests } from './areas/multiroot/multiroot.test'; import { setup as setupLocalizationTests } from './areas/workbench/localization.test'; import { setup as setupLaunchTests } from './areas/workbench/launch.test'; import { setup as setupTerminalProfileTests } from './areas/terminal/terminal-profiles.test'; +import { setup as setupTerminalTabsTests } from './areas/terminal/terminal-tabs.test'; const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); if (fs.existsSync(testDataPath)) { @@ -360,4 +361,5 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { // TODO: Enable terminal tests for non-web if (opts.web) { setupTerminalProfileTests(opts); } + if (opts.web) { setupTerminalTabsTests(opts); } });