Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/automation/src/terminal.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { QuickInput } from './quickinput';
7
import { Code } from './code';
8
import { QuickAccess } from './quickaccess';
9
import { IElement } from './driver';
10
import { wait } from './playwrightDriver';
11
12
export enum Selector {
13
TerminalView = `#terminal`,
14
CommandDecorationPlaceholder = `.terminal-command-decoration.codicon-terminal-decoration-incomplete`,
15
CommandDecorationSuccess = `.terminal-command-decoration.codicon-terminal-decoration-success`,
16
CommandDecorationError = `.terminal-command-decoration.codicon-terminal-decoration-error`,
17
Xterm = `#terminal .terminal-wrapper`,
18
XtermEditor = `.editor-instance .terminal-wrapper`,
19
TabsEntry = '.terminal-tabs-entry',
20
Name = '.label-name',
21
Description = '.label-description',
22
XtermFocused = '.terminal.xterm.focus',
23
PlusButton = '.codicon-plus',
24
EditorGroups = '.editor .split-view-view',
25
EditorTab = '.terminal-tab',
26
SingleTab = '.single-terminal-tab',
27
Tabs = '.tabs-list .monaco-list-row',
28
SplitButton = '.editor .codicon-split-horizontal',
29
XtermSplitIndex0 = '#terminal .terminal-groups-container .split-view-view:nth-child(1) .terminal-wrapper',
30
XtermSplitIndex1 = '#terminal .terminal-groups-container .split-view-view:nth-child(2) .terminal-wrapper',
31
Hide = '.hide'
32
}
33
34
/**
35
* Terminal commands that accept a value in a quick input.
36
*/
37
export enum TerminalCommandIdWithValue {
38
Rename = 'workbench.action.terminal.rename',
39
ChangeColor = 'workbench.action.terminal.changeColor',
40
ChangeIcon = 'workbench.action.terminal.changeIcon',
41
NewWithProfile = 'workbench.action.terminal.newWithProfile',
42
SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell',
43
AttachToSession = 'workbench.action.terminal.attachToSession',
44
WriteDataToTerminal = 'workbench.action.terminal.writeDataToTerminal'
45
}
46
47
/**
48
* Terminal commands that do not present a quick input.
49
*/
50
export enum TerminalCommandId {
51
Split = 'workbench.action.terminal.split',
52
KillAll = 'workbench.action.terminal.killAll',
53
Unsplit = 'workbench.action.terminal.unsplit',
54
Join = 'workbench.action.terminal.join',
55
Show = 'workbench.action.terminal.toggleTerminal',
56
CreateNewEditor = 'workbench.action.createTerminalEditor',
57
SplitEditor = 'workbench.action.createTerminalEditorSide',
58
MoveToPanel = 'workbench.action.terminal.moveToTerminalPanel',
59
MoveToEditor = 'workbench.action.terminal.moveToEditor',
60
NewWithProfile = 'workbench.action.terminal.newWithProfile',
61
SelectDefaultProfile = 'workbench.action.terminal.selectDefaultShell',
62
DetachSession = 'workbench.action.terminal.detachSession',
63
CreateNew = 'workbench.action.terminal.new'
64
}
65
interface TerminalLabel {
66
name?: string;
67
description?: string;
68
icon?: string;
69
color?: string;
70
}
71
type TerminalGroup = TerminalLabel[];
72
73
interface ICommandDecorationCounts {
74
placeholder: number;
75
success: number;
76
error: number;
77
}
78
79
export class Terminal {
80
81
constructor(private code: Code, private quickaccess: QuickAccess, private quickinput: QuickInput) { }
82
83
async runCommand(commandId: TerminalCommandId, expectedLocation?: 'editor' | 'panel'): Promise<void> {
84
const keepOpen = commandId === TerminalCommandId.Join;
85
await this.quickaccess.runCommand(commandId, { keepOpen });
86
if (keepOpen) {
87
await this.code.dispatchKeybinding('enter', async () => {
88
await this.quickinput.waitForQuickInputClosed();
89
});
90
}
91
switch (commandId) {
92
case TerminalCommandId.Show:
93
case TerminalCommandId.CreateNewEditor:
94
case TerminalCommandId.CreateNew:
95
case TerminalCommandId.NewWithProfile:
96
await this._waitForTerminal(expectedLocation === 'editor' || commandId === TerminalCommandId.CreateNewEditor ? 'editor' : 'panel');
97
break;
98
case TerminalCommandId.KillAll:
99
// HACK: Attempt to kill all terminals to clean things up, this is known to be flaky
100
// but the reason why isn't known. This is typically called in the after each hook,
101
// Since it's not actually required that all terminals are killed just continue on
102
// after 2 seconds.
103
await Promise.race([
104
this.code.waitForElements(Selector.Xterm, true, e => e.length === 0),
105
this.code.wait(2000)
106
]);
107
break;
108
}
109
}
110
111
async runCommandWithValue(commandId: TerminalCommandIdWithValue, value?: string, altKey?: boolean): Promise<void> {
112
const keepOpen = !!value || commandId === TerminalCommandIdWithValue.NewWithProfile || commandId === TerminalCommandIdWithValue.Rename || (commandId === TerminalCommandIdWithValue.SelectDefaultProfile && value !== 'PowerShell');
113
await this.quickaccess.runCommand(commandId, { keepOpen });
114
// Running the command should hide the quick input in the following frame, this next wait
115
// ensures that the quick input is opened again before proceeding to avoid a race condition
116
// where the enter keybinding below would close the quick input if it's triggered before the
117
// new quick input shows.
118
await this.quickinput.waitForQuickInputOpened();
119
if (value) {
120
await this.quickinput.type(value);
121
} else if (commandId === TerminalCommandIdWithValue.Rename) {
122
// Reset
123
await this.code.dispatchKeybinding('Backspace', async () => {
124
// TODO https://github.com/microsoft/vscode/issues/242535
125
await wait(100);
126
});
127
}
128
await this.code.wait(100);
129
await this.code.dispatchKeybinding(altKey ? 'Alt+Enter' : 'enter', async () => {
130
// TODO https://github.com/microsoft/vscode/issues/242535
131
await wait(100);
132
});
133
await this.quickinput.waitForQuickInputClosed();
134
if (commandId === TerminalCommandIdWithValue.NewWithProfile) {
135
await this._waitForTerminal();
136
}
137
}
138
139
async runCommandInTerminal(commandText: string, skipEnter?: boolean): Promise<void> {
140
await this.code.writeInTerminal(Selector.Xterm, commandText);
141
if (!skipEnter) {
142
await this.code.dispatchKeybinding('enter', async () => {
143
// TODO https://github.com/microsoft/vscode/issues/242535
144
await wait(100);
145
});
146
}
147
}
148
149
/**
150
* Creates a terminal using the new terminal command.
151
* @param expectedLocation The location to check the terminal for, defaults to panel.
152
*/
153
async createTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
154
await this.runCommand(TerminalCommandId.CreateNew, expectedLocation);
155
await this._waitForTerminal(expectedLocation);
156
}
157
158
/**
159
* Creates an empty terminal by opening a regular terminal and resetting its state such that it
160
* essentially acts like an Pseudoterminal extension API-based terminal. This can then be paired
161
* with `TerminalCommandIdWithValue.WriteDataToTerminal` to make more reliable tests.
162
*/
163
async createEmptyTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
164
await this.createTerminal(expectedLocation);
165
166
// Run a command to ensure the shell has started, this is used to ensure the shell's data
167
// does not leak into the "empty terminal"
168
await this.runCommandInTerminal('echo "initialized"');
169
await this.waitForTerminalText(buffer => buffer.some(line => line.startsWith('initialized')));
170
171
// Erase all content and reset cursor to top
172
await this.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('H')}`);
173
174
// Force windows pty mode off; assume all sequences are rendered in correct position
175
if (process.platform === 'win32') {
176
await this.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${vsc('P;IsWindows=False')}`);
177
}
178
}
179
180
async assertEditorGroupCount(count: number): Promise<void> {
181
await this.code.waitForElements(Selector.EditorGroups, true, editorGroups => editorGroups && editorGroups.length === count);
182
}
183
184
async assertSingleTab(label: TerminalLabel, editor?: boolean): Promise<void> {
185
let regex = undefined;
186
if (label.name && label.description) {
187
regex = new RegExp(label.name + ' - ' + label.description);
188
} else if (label.name) {
189
regex = new RegExp(label.name);
190
}
191
await this.assertTabExpected(editor ? Selector.EditorTab : Selector.SingleTab, undefined, regex, label.icon, label.color);
192
}
193
194
async assertTerminalGroups(expectedGroups: TerminalGroup[]): Promise<void> {
195
let expectedCount = 0;
196
expectedGroups.forEach(g => expectedCount += g.length);
197
let index = 0;
198
while (index < expectedCount) {
199
for (let groupIndex = 0; groupIndex < expectedGroups.length; groupIndex++) {
200
const terminalsInGroup = expectedGroups[groupIndex].length;
201
let indexInGroup = 0;
202
const isSplit = terminalsInGroup > 1;
203
while (indexInGroup < terminalsInGroup) {
204
const instance = expectedGroups[groupIndex][indexInGroup];
205
const nameRegex = instance.name && isSplit ? new RegExp('\\s*[├┌└]\\s*' + instance.name) : instance.name ? new RegExp(/^\s*/ + instance.name) : undefined;
206
await this.assertTabExpected(undefined, index, nameRegex, instance.icon, instance.color, instance.description);
207
indexInGroup++;
208
index++;
209
}
210
}
211
}
212
}
213
214
async getTerminalGroups(): Promise<TerminalGroup[]> {
215
const tabCount = (await this.code.waitForElements(Selector.Tabs, true)).length;
216
const groups: TerminalGroup[] = [];
217
for (let i = 0; i < tabCount; i++) {
218
const title = await this.code.waitForElement(`${Selector.Tabs}[data-index="${i}"] ${Selector.TabsEntry} ${Selector.Name}`, e => e?.textContent?.length ? e?.textContent?.length > 1 : false);
219
const description: IElement | undefined = await this.code.waitForElement(`${Selector.Tabs}[data-index="${i}"] ${Selector.TabsEntry} ${Selector.Description}`, () => true);
220
221
const label: TerminalLabel = {
222
name: title.textContent.replace(/^[├┌└]\s*/, ''),
223
description: description?.textContent
224
};
225
// It's a new group if the tab does not start with ├ or └
226
if (title.textContent.match(/^[├└]/)) {
227
groups[groups.length - 1].push(label);
228
} else {
229
groups.push([label]);
230
}
231
}
232
return groups;
233
}
234
235
async getSingleTabName(): Promise<string> {
236
const tab = await this.code.waitForElement(Selector.SingleTab, singleTab => !!singleTab && singleTab?.textContent.length > 1);
237
return tab.textContent;
238
}
239
240
private async assertTabExpected(selector?: string, listIndex?: number, nameRegex?: RegExp, icon?: string, color?: string, description?: string): Promise<void> {
241
if (listIndex) {
242
if (nameRegex) {
243
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} ${Selector.Name}`, entry => !!entry && !!entry?.textContent.match(nameRegex));
244
if (description) {
245
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} ${Selector.Description}`, e => !!e && e.textContent === description);
246
}
247
}
248
if (color) {
249
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} .monaco-icon-label.terminal-icon-terminal_ansi${color}`);
250
}
251
if (icon) {
252
await this.code.waitForElement(`${Selector.Tabs}[data-index="${listIndex}"] ${Selector.TabsEntry} .codicon-${icon}`);
253
}
254
} else if (selector) {
255
if (nameRegex) {
256
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab?.textContent.match(nameRegex));
257
}
258
if (color) {
259
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab.className.includes(`terminal-icon-terminal_ansi${color}`));
260
}
261
if (icon) {
262
selector = selector === Selector.EditorTab ? selector : `${selector} .codicon`;
263
await this.code.waitForElement(`${selector}`, singleTab => !!singleTab && !!singleTab.className.includes(icon));
264
}
265
}
266
}
267
268
async assertTerminalViewHidden(): Promise<void> {
269
await this.code.waitForElement(Selector.TerminalView, result => result === undefined);
270
}
271
272
async assertCommandDecorations(expectedCounts?: ICommandDecorationCounts, customIcon?: { updatedIcon: string; count: number }, showDecorations?: 'both' | 'gutter' | 'overviewRuler' | 'never'): Promise<void> {
273
if (expectedCounts) {
274
const placeholderSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationPlaceholder}${Selector.Hide}` : Selector.CommandDecorationPlaceholder;
275
await this.code.waitForElements(placeholderSelector, true, decorations => decorations && decorations.length === expectedCounts.placeholder);
276
const successSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationSuccess}${Selector.Hide}` : Selector.CommandDecorationSuccess;
277
await this.code.waitForElements(successSelector, true, decorations => decorations && decorations.length === expectedCounts.success);
278
const errorSelector = showDecorations === 'overviewRuler' ? `${Selector.CommandDecorationError}${Selector.Hide}` : Selector.CommandDecorationError;
279
await this.code.waitForElements(errorSelector, true, decorations => decorations && decorations.length === expectedCounts.error);
280
}
281
282
if (customIcon) {
283
await this.code.waitForElements(`.terminal-command-decoration.codicon-${customIcon.updatedIcon}`, true, decorations => decorations && decorations.length === customIcon.count);
284
}
285
}
286
287
async clickPlusButton(): Promise<void> {
288
await this.code.waitAndClick(Selector.PlusButton);
289
}
290
291
async clickSplitButton(): Promise<void> {
292
await this.code.waitAndClick(Selector.SplitButton);
293
}
294
295
async clickSingleTab(): Promise<void> {
296
await this.code.waitAndClick(Selector.SingleTab);
297
}
298
299
async waitForTerminalText(accept: (buffer: string[]) => boolean, message?: string, splitIndex?: 0 | 1): Promise<void> {
300
try {
301
let selector: string = Selector.Xterm;
302
if (splitIndex !== undefined) {
303
selector = splitIndex === 0 ? Selector.XtermSplitIndex0 : Selector.XtermSplitIndex1;
304
}
305
await this.code.waitForTerminalBuffer(selector, accept);
306
} catch (err: any) {
307
if (message) {
308
throw new Error(`${message} \n\nInner exception: \n${err.message} `);
309
}
310
throw err;
311
}
312
}
313
314
async getPage(): Promise<any> {
315
return (this.code.driver as any).page;
316
}
317
318
/**
319
* Waits for the terminal to be focused and to contain content.
320
* @param expectedLocation The location to check the terminal for, defaults to panel.
321
*/
322
private async _waitForTerminal(expectedLocation?: 'editor' | 'panel'): Promise<void> {
323
await this.code.waitForElement(Selector.XtermFocused);
324
await this.code.waitForTerminalBuffer(expectedLocation === 'editor' ? Selector.XtermEditor : Selector.Xterm, lines => lines.some(line => line.length > 0));
325
}
326
}
327
328
function vsc(data: string) {
329
return setTextParams(`633;${data}`);
330
}
331
332
function setTextParams(data: string) {
333
return osc(`${data}\\x07`);
334
}
335
336
function osc(data: string) {
337
return `\\x1b]${data}`;
338
}
339
340
function csi(data: string) {
341
return `\\x1b[${data}`;
342
}
343
344