@theia/playwright 1.45.0 → 1.46.0-next.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +54 -54
  2. package/lib/index.d.ts +30 -30
  3. package/lib/index.js +57 -57
  4. package/lib/tests/fixtures/theia-fixture.d.ts +4 -0
  5. package/lib/tests/fixtures/theia-fixture.d.ts.map +1 -0
  6. package/lib/tests/fixtures/theia-fixture.js +24 -0
  7. package/lib/tests/fixtures/theia-fixture.js.map +1 -0
  8. package/lib/tests/theia-app.test.d.ts +1 -1
  9. package/lib/tests/theia-app.test.js +29 -29
  10. package/lib/tests/theia-application-shell.test.d.ts +2 -0
  11. package/lib/tests/theia-application-shell.test.d.ts.map +1 -0
  12. package/lib/tests/theia-application-shell.test.js +58 -0
  13. package/lib/tests/theia-application-shell.test.js.map +1 -0
  14. package/lib/tests/theia-explorer-view.test.d.ts +1 -1
  15. package/lib/tests/theia-explorer-view.test.js +183 -183
  16. package/lib/tests/theia-main-menu.test.d.ts +1 -1
  17. package/lib/tests/theia-main-menu.test.js +92 -92
  18. package/lib/tests/theia-output-view.test.d.ts +1 -1
  19. package/lib/tests/theia-output-view.test.js +78 -78
  20. package/lib/tests/theia-preference-view.test.d.ts +1 -1
  21. package/lib/tests/theia-preference-view.test.js +101 -101
  22. package/lib/tests/theia-problems-view.test.d.ts +1 -1
  23. package/lib/tests/theia-problems-view.test.js +54 -54
  24. package/lib/tests/theia-quick-command.test.d.ts +1 -1
  25. package/lib/tests/theia-quick-command.test.js +69 -69
  26. package/lib/tests/theia-sample-app.test.d.ts +1 -1
  27. package/lib/tests/theia-sample-app.test.js +57 -57
  28. package/lib/tests/theia-status-bar.test.d.ts +1 -1
  29. package/lib/tests/theia-status-bar.test.js +44 -44
  30. package/lib/tests/theia-terminal-view.test.d.ts +1 -1
  31. package/lib/tests/theia-terminal-view.test.js +78 -78
  32. package/lib/tests/theia-text-editor.test.d.ts +1 -1
  33. package/lib/tests/theia-text-editor.test.js +155 -155
  34. package/lib/tests/theia-toolbar.test.d.ts +1 -1
  35. package/lib/tests/theia-toolbar.test.js +61 -61
  36. package/lib/tests/theia-workspace.test.d.ts +1 -1
  37. package/lib/tests/theia-workspace.test.js +72 -72
  38. package/lib/theia-about-dialog.d.ts +4 -4
  39. package/lib/theia-about-dialog.js +26 -26
  40. package/lib/theia-app-loader.d.ts +19 -19
  41. package/lib/theia-app-loader.js +129 -129
  42. package/lib/theia-app.d.ts +50 -50
  43. package/lib/theia-app.js +153 -153
  44. package/lib/theia-context-menu.d.ts +8 -8
  45. package/lib/theia-context-menu.js +37 -37
  46. package/lib/theia-dialog.d.ts +28 -28
  47. package/lib/theia-dialog.js +99 -99
  48. package/lib/theia-editor.d.ts +9 -9
  49. package/lib/theia-editor.js +68 -68
  50. package/lib/theia-explorer-view.d.ts +47 -47
  51. package/lib/theia-explorer-view.js +273 -273
  52. package/lib/theia-main-menu.d.ts +12 -12
  53. package/lib/theia-main-menu.js +53 -53
  54. package/lib/theia-menu-item.d.ts +14 -14
  55. package/lib/theia-menu-item.js +66 -66
  56. package/lib/theia-menu.d.ts +16 -16
  57. package/lib/theia-menu.js +86 -86
  58. package/lib/theia-monaco-editor.d.ts +15 -15
  59. package/lib/theia-monaco-editor.js +75 -75
  60. package/lib/theia-notification-indicator.d.ts +7 -7
  61. package/lib/theia-notification-indicator.js +44 -44
  62. package/lib/theia-notification-overlay.d.ts +22 -22
  63. package/lib/theia-notification-overlay.js +79 -79
  64. package/lib/theia-output-channel.d.ts +24 -24
  65. package/lib/theia-output-channel.js +71 -71
  66. package/lib/theia-output-view.d.ts +9 -9
  67. package/lib/theia-output-view.js +81 -81
  68. package/lib/theia-page-object.d.ts +7 -7
  69. package/lib/theia-page-object.js +27 -27
  70. package/lib/theia-preference-view.d.ts +84 -84
  71. package/lib/theia-preference-view.js +209 -209
  72. package/lib/theia-problem-indicator.d.ts +8 -8
  73. package/lib/theia-problem-indicator.js +38 -38
  74. package/lib/theia-problem-view.d.ts +5 -5
  75. package/lib/theia-problem-view.js +30 -30
  76. package/lib/theia-quick-command-palette.d.ts +12 -12
  77. package/lib/theia-quick-command-palette.js +80 -80
  78. package/lib/theia-rename-dialog.d.ts +5 -5
  79. package/lib/theia-rename-dialog.js +35 -35
  80. package/lib/theia-status-bar.d.ts +13 -13
  81. package/lib/theia-status-bar.js +39 -39
  82. package/lib/theia-status-indicator.d.ts +10 -10
  83. package/lib/theia-status-indicator.js +48 -48
  84. package/lib/theia-terminal.d.ts +13 -13
  85. package/lib/theia-terminal.js +59 -59
  86. package/lib/theia-text-editor.d.ts +26 -26
  87. package/lib/theia-text-editor.js +120 -120
  88. package/lib/theia-toggle-bottom-indicator.d.ts +4 -4
  89. package/lib/theia-toggle-bottom-indicator.js +26 -26
  90. package/lib/theia-toolbar-item.d.ts +10 -10
  91. package/lib/theia-toolbar-item.js +39 -39
  92. package/lib/theia-toolbar.d.ts +19 -19
  93. package/lib/theia-toolbar.js +90 -90
  94. package/lib/theia-tree-node.d.ts +19 -19
  95. package/lib/theia-tree-node.js +72 -72
  96. package/lib/theia-view.d.ts +32 -32
  97. package/lib/theia-view.js +149 -149
  98. package/lib/theia-welcome-view.d.ts +6 -0
  99. package/lib/theia-welcome-view.d.ts.map +1 -0
  100. package/lib/theia-welcome-view.js +31 -0
  101. package/lib/theia-welcome-view.js.map +1 -0
  102. package/lib/theia-workspace.d.ts +18 -18
  103. package/lib/theia-workspace.js +69 -69
  104. package/lib/util.d.ts +19 -19
  105. package/lib/util.js +93 -93
  106. package/package.json +2 -2
  107. package/src/index.ts +46 -46
  108. package/src/tests/resources/sample-files1/sample.txt +4 -4
  109. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-1.txt +1 -1
  110. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-1/sampleFile1-1-2.txt +1 -1
  111. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-1.txt +1 -1
  112. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder1/sampleFolder1-2/sampleFile1-2-2.txt +1 -1
  113. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-1.txt +1 -1
  114. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-1/sampleFile2-1-2.txt +1 -1
  115. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-1.txt +1 -1
  116. package/src/tests/resources/sample-files1/sampleFolder/sampleFolder2/sampleFolder2-2/sampleFile2-2-2.txt +1 -1
  117. package/src/tests/resources/sample-files2/another-sample.txt +1 -1
  118. package/src/tests/theia-app.test.ts +33 -33
  119. package/src/tests/theia-application-shell.test.ts +67 -0
  120. package/src/tests/theia-explorer-view.test.ts +211 -211
  121. package/src/tests/theia-main-menu.test.ts +112 -112
  122. package/src/tests/theia-output-view.test.ts +85 -85
  123. package/src/tests/theia-preference-view.test.ts +122 -122
  124. package/src/tests/theia-problems-view.test.ts +64 -64
  125. package/src/tests/theia-quick-command.test.ts +80 -80
  126. package/src/tests/theia-sample-app.test.ts +66 -66
  127. package/src/tests/theia-status-bar.test.ts +52 -52
  128. package/src/tests/theia-terminal-view.test.ts +90 -90
  129. package/src/tests/theia-text-editor.test.ts +187 -187
  130. package/src/tests/theia-toolbar.test.ts +69 -69
  131. package/src/tests/theia-workspace.test.ts +80 -80
  132. package/src/theia-about-dialog.ts +26 -26
  133. package/src/theia-app-loader.ts +167 -167
  134. package/src/theia-app.ts +188 -188
  135. package/src/theia-context-menu.ts +42 -42
  136. package/src/theia-dialog.ts +114 -114
  137. package/src/theia-editor.ts +73 -73
  138. package/src/theia-explorer-view.ts +311 -311
  139. package/src/theia-main-menu.ts +54 -54
  140. package/src/theia-menu-item.ts +75 -75
  141. package/src/theia-menu.ts +96 -96
  142. package/src/theia-monaco-editor.ts +83 -83
  143. package/src/theia-notification-indicator.ts +44 -44
  144. package/src/theia-notification-overlay.ts +94 -94
  145. package/src/theia-output-channel.ts +88 -88
  146. package/src/theia-output-view.ts +87 -87
  147. package/src/theia-page-object.ts +29 -29
  148. package/src/theia-preference-view.ts +240 -240
  149. package/src/theia-problem-indicator.ts +37 -37
  150. package/src/theia-problem-view.ts +30 -30
  151. package/src/theia-quick-command-palette.ts +83 -83
  152. package/src/theia-rename-dialog.ts +36 -36
  153. package/src/theia-status-bar.ts +44 -44
  154. package/src/theia-status-indicator.ts +50 -50
  155. package/src/theia-terminal.ts +69 -69
  156. package/src/theia-text-editor.ts +141 -141
  157. package/src/theia-toggle-bottom-indicator.ts +21 -21
  158. package/src/theia-toolbar-item.ts +41 -41
  159. package/src/theia-toolbar.ts +99 -99
  160. package/src/theia-tree-node.ts +81 -81
  161. package/src/theia-view.ts +177 -177
  162. package/src/theia-welcome-view.ts +31 -0
  163. package/src/theia-workspace.ts +76 -76
  164. package/src/util.ts +91 -91
@@ -1,311 +1,311 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import { ElementHandle } from '@playwright/test';
18
- import { TheiaApp } from './theia-app';
19
- import { TheiaDialog } from './theia-dialog';
20
- import { TheiaMenuItem } from './theia-menu-item';
21
- import { TheiaRenameDialog } from './theia-rename-dialog';
22
- import { TheiaTreeNode } from './theia-tree-node';
23
- import { TheiaView } from './theia-view';
24
- import { elementContainsClass, normalizeId, OSUtil, urlEncodePath } from './util';
25
-
26
- const TheiaExplorerViewData = {
27
- tabSelector: '#shell-tab-explorer-view-container',
28
- viewSelector: '#explorer-view-container--files',
29
- viewName: 'Explorer'
30
- };
31
-
32
- export class TheiaExplorerFileStatNode extends TheiaTreeNode {
33
-
34
- constructor(protected override elementHandle: ElementHandle<SVGElement | HTMLElement>, protected explorerView: TheiaExplorerView) {
35
- super(elementHandle, explorerView.app);
36
- }
37
-
38
- async absolutePath(): Promise<string | null> {
39
- return this.elementHandle.getAttribute('title');
40
- }
41
-
42
- async isFile(): Promise<boolean> {
43
- return ! await this.isFolder();
44
- }
45
-
46
- async isFolder(): Promise<boolean> {
47
- return elementContainsClass(this.elementHandle, 'theia-DirNode');
48
- }
49
-
50
- async getMenuItemByNamePath(names: string[], nodeSegmentLabel?: string): Promise<TheiaMenuItem> {
51
- const contextMenu = nodeSegmentLabel ? await this.openContextMenuOnSegment(nodeSegmentLabel) : await this.openContextMenu();
52
- const menuItem = await contextMenu.menuItemByNamePath(...names);
53
- if (!menuItem) { throw Error('MenuItem could not be retrieved by path'); }
54
- return menuItem;
55
- }
56
-
57
- }
58
-
59
- export type TheiaExplorerFileStatNodePredicate = (node: TheiaExplorerFileStatNode) => Promise<boolean>;
60
- export const DOT_FILES_FILTER: TheiaExplorerFileStatNodePredicate = async node => {
61
- const label = await node.label();
62
- return label ? !label.startsWith('.') : true;
63
- };
64
-
65
- export class TheiaExplorerView extends TheiaView {
66
-
67
- constructor(app: TheiaApp) {
68
- super(TheiaExplorerViewData, app);
69
- }
70
-
71
- override async activate(): Promise<void> {
72
- await super.activate();
73
- const viewElement = await this.viewElement();
74
- await viewElement?.waitForSelector('.theia-TreeContainer');
75
- }
76
-
77
- async refresh(): Promise<void> {
78
- await this.clickButton('navigator.refresh');
79
- }
80
-
81
- async collapseAll(): Promise<void> {
82
- await this.clickButton('navigator.collapse.all');
83
- }
84
-
85
- protected async clickButton(id: string): Promise<void> {
86
- await this.activate();
87
- const viewElement = await this.viewElement();
88
- await viewElement?.hover();
89
- const button = await viewElement?.waitForSelector(`#${normalizeId(id)}`);
90
- await button?.click();
91
- }
92
-
93
- async visibleFileStatNodes(filterPredicate: TheiaExplorerFileStatNodePredicate = (_ => Promise.resolve(true))): Promise<TheiaExplorerFileStatNode[]> {
94
- const viewElement = await this.viewElement();
95
- const handles = await viewElement?.$$('.theia-FileStatNode');
96
- if (handles) {
97
- const nodes = handles.map(handle => new TheiaExplorerFileStatNode(handle, this));
98
- const filteredNodes = [];
99
- for (const node of nodes) {
100
- if ((await filterPredicate(node)) === true) {
101
- filteredNodes.push(node);
102
- }
103
- }
104
- return filteredNodes;
105
- }
106
- return [];
107
- }
108
-
109
- async getFileStatNodeByLabel(label: string, compact = false): Promise<TheiaExplorerFileStatNode> {
110
- const file = await this.fileStatNode(label, compact);
111
- if (!file) { throw Error('File stat node could not be retrieved by path fragments'); }
112
- return file;
113
- }
114
-
115
- async fileStatNode(filePath: string, compact = false): Promise<TheiaExplorerFileStatNode | undefined> {
116
- return compact ? this.compactFileStatNode(filePath) : this.fileStatNodeBySegments(...filePath.split('/'));
117
- }
118
-
119
- protected async fileStatNodeBySegments(...pathFragments: string[]): Promise<TheiaExplorerFileStatNode | undefined> {
120
- await super.activate();
121
- const viewElement = await this.viewElement();
122
-
123
- let currentTreeNode = undefined;
124
- let fragmentsSoFar = '';
125
- for (let index = 0; index < pathFragments.length; index++) {
126
- const fragment = pathFragments[index];
127
- fragmentsSoFar += index !== 0 ? '/' : '';
128
- fragmentsSoFar += fragment;
129
-
130
- const selector = this.treeNodeSelector(fragmentsSoFar);
131
- const nextTreeNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
132
- if (!nextTreeNode) {
133
- throw new Error(`Tree node '${selector}' not found in explorer`);
134
- }
135
- currentTreeNode = new TheiaExplorerFileStatNode(nextTreeNode, this);
136
- if (index < pathFragments.length - 1 && await currentTreeNode.isCollapsed()) {
137
- await currentTreeNode.expand();
138
- }
139
- }
140
-
141
- return currentTreeNode;
142
- }
143
-
144
- protected async compactFileStatNode(path: string): Promise<TheiaExplorerFileStatNode | undefined> {
145
- // default setting `explorer.compactFolders=true` renders folders in a compact form - single child folders will be compressed in a combined tree element
146
- await super.activate();
147
- const viewElement = await this.viewElement();
148
-
149
- // check if first segment folder needs to be expanded first (if folder has never been expanded, it will not show the compact folder structure)
150
- await this.waitForVisibleFileNodes();
151
- const firstSegment = path.split('/')[0];
152
- const selector = this.treeNodeSelector(firstSegment);
153
- const folderElement = await viewElement?.$(selector);
154
- if (folderElement && await folderElement.isVisible()) {
155
- const folderNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
156
- if (!folderNode) {
157
- throw new Error(`Tree node '${selector}' not found in explorer`);
158
- }
159
- const folderFileStatNode = new TheiaExplorerFileStatNode(folderNode, this);
160
- if (await folderFileStatNode.isCollapsed()) {
161
- await folderFileStatNode.expand();
162
- }
163
- }
164
- // now get tree node via the full path
165
- const fullPathSelector = this.treeNodeSelector(path);
166
- const treeNode = await viewElement?.waitForSelector(fullPathSelector, { state: 'visible' });
167
- if (!treeNode) {
168
- throw new Error(`Tree node '${fullPathSelector}' not found in explorer`);
169
- }
170
- return new TheiaExplorerFileStatNode(treeNode, this);
171
- }
172
-
173
- async selectTreeNode(filePath: string): Promise<void> {
174
- await this.activate();
175
- const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
176
- if (await this.isTreeNodeSelected(filePath)) {
177
- await treeNode.focus();
178
- } else {
179
- await treeNode.click({ modifiers: ['Control'] });
180
- // make sure the click has been acted-upon before returning
181
- while (!await this.isTreeNodeSelected(filePath)) {
182
- console.debug('Waiting for clicked tree node to be selected: ' + filePath);
183
- }
184
- }
185
- await this.page.waitForSelector(this.treeNodeSelector(filePath) + '.theia-mod-selected');
186
- }
187
-
188
- async isTreeNodeSelected(filePath: string): Promise<boolean> {
189
- const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
190
- return elementContainsClass(treeNode, 'theia-mod-selected');
191
- }
192
-
193
- protected treeNodeSelector(filePath: string): string {
194
- return `.theia-FileStatNode:has(#${normalizeId(this.treeNodeId(filePath))})`;
195
- }
196
-
197
- protected treeNodeId(filePath: string): string {
198
- const workspacePath = this.app.workspace.path;
199
- const nodeId = `${workspacePath}:${workspacePath}${OSUtil.fileSeparator}${filePath}`;
200
- if (OSUtil.isWindows) {
201
- return urlEncodePath(nodeId);
202
- }
203
- return nodeId;
204
- }
205
-
206
- async clickContextMenuItem(file: string, path: string[], nodeSegmentLabel?: string): Promise<void> {
207
- await this.activate();
208
- const fileStatNode = await this.fileStatNode(file, !!nodeSegmentLabel);
209
- if (!fileStatNode) { throw Error('File stat node could not be retrieved by path fragments'); }
210
- const menuItem = await fileStatNode.getMenuItemByNamePath(path, nodeSegmentLabel);
211
- await menuItem.click();
212
- }
213
-
214
- protected async existsNode(path: string, isDirectory: boolean, compact = false): Promise<boolean> {
215
- const fileStatNode = await this.fileStatNode(path, compact);
216
- if (!fileStatNode) {
217
- return false;
218
- }
219
- if (isDirectory) {
220
- if (!await fileStatNode.isFolder()) {
221
- throw Error(`FileStatNode for '${path}' is not a directory!`);
222
- }
223
- } else {
224
- if (!await fileStatNode.isFile()) {
225
- throw Error(`FileStatNode for '${path}' is not a file!`);
226
- }
227
- }
228
- return true;
229
- }
230
-
231
- async existsFileNode(path: string): Promise<boolean> {
232
- return this.existsNode(path, false);
233
- }
234
-
235
- async existsDirectoryNode(path: string, compact = false): Promise<boolean> {
236
- return this.existsNode(path, true, compact);
237
- }
238
-
239
- async waitForTreeNodeVisible(path: string): Promise<void> {
240
- // wait for tree node to be visible, e.g. after triggering create
241
- const viewElement = await this.viewElement();
242
- await viewElement?.waitForSelector(this.treeNodeSelector(path), { state: 'visible' });
243
- }
244
-
245
- async getNumberOfVisibleNodes(): Promise<number> {
246
- await this.activate();
247
- await this.refresh();
248
- const fileStatElements = await this.visibleFileStatNodes(DOT_FILES_FILTER);
249
- return fileStatElements.length;
250
- }
251
-
252
- async deleteNode(path: string, confirm = true, nodeSegmentLabel?: string): Promise<void> {
253
- await this.activate();
254
- await this.clickContextMenuItem(path, ['Delete'], nodeSegmentLabel);
255
-
256
- const confirmDialog = new TheiaDialog(this.app);
257
- await confirmDialog.waitForVisible();
258
- confirm ? await confirmDialog.clickMainButton() : await confirmDialog.clickSecondaryButton();
259
- await confirmDialog.waitForClosed();
260
- }
261
-
262
- async renameNode(path: string, newName: string, confirm = true, nodeSegmentLabel?: string): Promise<void> {
263
- await this.activate();
264
- await this.clickContextMenuItem(path, ['Rename'], nodeSegmentLabel);
265
-
266
- const renameDialog = new TheiaRenameDialog(this.app);
267
- await renameDialog.waitForVisible();
268
- await renameDialog.enterNewName(newName);
269
- await renameDialog.waitUntilMainButtonIsEnabled();
270
- confirm ? await renameDialog.confirm() : await renameDialog.close();
271
- await renameDialog.waitForClosed();
272
- await this.refresh();
273
- }
274
-
275
- override async waitForVisible(): Promise<void> {
276
- await super.waitForVisible();
277
- await this.page.waitForSelector(this.tabSelector, { state: 'visible' });
278
- }
279
-
280
- /**
281
- * Waits until some non-dot file nodes are visible
282
- */
283
- async waitForVisibleFileNodes(): Promise<void> {
284
- while ((await this.visibleFileStatNodes(DOT_FILES_FILTER)).length === 0) {
285
- console.debug('Awaiting for tree nodes to appear');
286
- }
287
- }
288
-
289
- async waitForFileNodesToIncrease(numberBefore: number): Promise<void> {
290
- const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
291
- await this.page.waitForFunction(
292
- (predicate: { selector: string; numberBefore: number; }) => {
293
- const elements = document.querySelectorAll(predicate.selector);
294
- return !!elements && elements.length > predicate.numberBefore;
295
- },
296
- { selector: fileStatNodesSelector, numberBefore }
297
- );
298
- }
299
-
300
- async waitForFileNodesToDecrease(numberBefore: number): Promise<void> {
301
- const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
302
- await this.page.waitForFunction(
303
- (predicate: { selector: string; numberBefore: number; }) => {
304
- const elements = document.querySelectorAll(predicate.selector);
305
- return !!elements && elements.length < predicate.numberBefore;
306
- },
307
- { selector: fileStatNodesSelector, numberBefore }
308
- );
309
- }
310
-
311
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 logi.cals GmbH, EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { ElementHandle } from '@playwright/test';
18
+ import { TheiaApp } from './theia-app';
19
+ import { TheiaDialog } from './theia-dialog';
20
+ import { TheiaMenuItem } from './theia-menu-item';
21
+ import { TheiaRenameDialog } from './theia-rename-dialog';
22
+ import { TheiaTreeNode } from './theia-tree-node';
23
+ import { TheiaView } from './theia-view';
24
+ import { elementContainsClass, normalizeId, OSUtil, urlEncodePath } from './util';
25
+
26
+ const TheiaExplorerViewData = {
27
+ tabSelector: '#shell-tab-explorer-view-container',
28
+ viewSelector: '#explorer-view-container--files',
29
+ viewName: 'Explorer'
30
+ };
31
+
32
+ export class TheiaExplorerFileStatNode extends TheiaTreeNode {
33
+
34
+ constructor(protected override elementHandle: ElementHandle<SVGElement | HTMLElement>, protected explorerView: TheiaExplorerView) {
35
+ super(elementHandle, explorerView.app);
36
+ }
37
+
38
+ async absolutePath(): Promise<string | null> {
39
+ return this.elementHandle.getAttribute('title');
40
+ }
41
+
42
+ async isFile(): Promise<boolean> {
43
+ return ! await this.isFolder();
44
+ }
45
+
46
+ async isFolder(): Promise<boolean> {
47
+ return elementContainsClass(this.elementHandle, 'theia-DirNode');
48
+ }
49
+
50
+ async getMenuItemByNamePath(names: string[], nodeSegmentLabel?: string): Promise<TheiaMenuItem> {
51
+ const contextMenu = nodeSegmentLabel ? await this.openContextMenuOnSegment(nodeSegmentLabel) : await this.openContextMenu();
52
+ const menuItem = await contextMenu.menuItemByNamePath(...names);
53
+ if (!menuItem) { throw Error('MenuItem could not be retrieved by path'); }
54
+ return menuItem;
55
+ }
56
+
57
+ }
58
+
59
+ export type TheiaExplorerFileStatNodePredicate = (node: TheiaExplorerFileStatNode) => Promise<boolean>;
60
+ export const DOT_FILES_FILTER: TheiaExplorerFileStatNodePredicate = async node => {
61
+ const label = await node.label();
62
+ return label ? !label.startsWith('.') : true;
63
+ };
64
+
65
+ export class TheiaExplorerView extends TheiaView {
66
+
67
+ constructor(app: TheiaApp) {
68
+ super(TheiaExplorerViewData, app);
69
+ }
70
+
71
+ override async activate(): Promise<void> {
72
+ await super.activate();
73
+ const viewElement = await this.viewElement();
74
+ await viewElement?.waitForSelector('.theia-TreeContainer');
75
+ }
76
+
77
+ async refresh(): Promise<void> {
78
+ await this.clickButton('navigator.refresh');
79
+ }
80
+
81
+ async collapseAll(): Promise<void> {
82
+ await this.clickButton('navigator.collapse.all');
83
+ }
84
+
85
+ protected async clickButton(id: string): Promise<void> {
86
+ await this.activate();
87
+ const viewElement = await this.viewElement();
88
+ await viewElement?.hover();
89
+ const button = await viewElement?.waitForSelector(`#${normalizeId(id)}`);
90
+ await button?.click();
91
+ }
92
+
93
+ async visibleFileStatNodes(filterPredicate: TheiaExplorerFileStatNodePredicate = (_ => Promise.resolve(true))): Promise<TheiaExplorerFileStatNode[]> {
94
+ const viewElement = await this.viewElement();
95
+ const handles = await viewElement?.$$('.theia-FileStatNode');
96
+ if (handles) {
97
+ const nodes = handles.map(handle => new TheiaExplorerFileStatNode(handle, this));
98
+ const filteredNodes = [];
99
+ for (const node of nodes) {
100
+ if ((await filterPredicate(node)) === true) {
101
+ filteredNodes.push(node);
102
+ }
103
+ }
104
+ return filteredNodes;
105
+ }
106
+ return [];
107
+ }
108
+
109
+ async getFileStatNodeByLabel(label: string, compact = false): Promise<TheiaExplorerFileStatNode> {
110
+ const file = await this.fileStatNode(label, compact);
111
+ if (!file) { throw Error('File stat node could not be retrieved by path fragments'); }
112
+ return file;
113
+ }
114
+
115
+ async fileStatNode(filePath: string, compact = false): Promise<TheiaExplorerFileStatNode | undefined> {
116
+ return compact ? this.compactFileStatNode(filePath) : this.fileStatNodeBySegments(...filePath.split('/'));
117
+ }
118
+
119
+ protected async fileStatNodeBySegments(...pathFragments: string[]): Promise<TheiaExplorerFileStatNode | undefined> {
120
+ await super.activate();
121
+ const viewElement = await this.viewElement();
122
+
123
+ let currentTreeNode = undefined;
124
+ let fragmentsSoFar = '';
125
+ for (let index = 0; index < pathFragments.length; index++) {
126
+ const fragment = pathFragments[index];
127
+ fragmentsSoFar += index !== 0 ? '/' : '';
128
+ fragmentsSoFar += fragment;
129
+
130
+ const selector = this.treeNodeSelector(fragmentsSoFar);
131
+ const nextTreeNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
132
+ if (!nextTreeNode) {
133
+ throw new Error(`Tree node '${selector}' not found in explorer`);
134
+ }
135
+ currentTreeNode = new TheiaExplorerFileStatNode(nextTreeNode, this);
136
+ if (index < pathFragments.length - 1 && await currentTreeNode.isCollapsed()) {
137
+ await currentTreeNode.expand();
138
+ }
139
+ }
140
+
141
+ return currentTreeNode;
142
+ }
143
+
144
+ protected async compactFileStatNode(path: string): Promise<TheiaExplorerFileStatNode | undefined> {
145
+ // default setting `explorer.compactFolders=true` renders folders in a compact form - single child folders will be compressed in a combined tree element
146
+ await super.activate();
147
+ const viewElement = await this.viewElement();
148
+
149
+ // check if first segment folder needs to be expanded first (if folder has never been expanded, it will not show the compact folder structure)
150
+ await this.waitForVisibleFileNodes();
151
+ const firstSegment = path.split('/')[0];
152
+ const selector = this.treeNodeSelector(firstSegment);
153
+ const folderElement = await viewElement?.$(selector);
154
+ if (folderElement && await folderElement.isVisible()) {
155
+ const folderNode = await viewElement?.waitForSelector(selector, { state: 'visible' });
156
+ if (!folderNode) {
157
+ throw new Error(`Tree node '${selector}' not found in explorer`);
158
+ }
159
+ const folderFileStatNode = new TheiaExplorerFileStatNode(folderNode, this);
160
+ if (await folderFileStatNode.isCollapsed()) {
161
+ await folderFileStatNode.expand();
162
+ }
163
+ }
164
+ // now get tree node via the full path
165
+ const fullPathSelector = this.treeNodeSelector(path);
166
+ const treeNode = await viewElement?.waitForSelector(fullPathSelector, { state: 'visible' });
167
+ if (!treeNode) {
168
+ throw new Error(`Tree node '${fullPathSelector}' not found in explorer`);
169
+ }
170
+ return new TheiaExplorerFileStatNode(treeNode, this);
171
+ }
172
+
173
+ async selectTreeNode(filePath: string): Promise<void> {
174
+ await this.activate();
175
+ const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
176
+ if (await this.isTreeNodeSelected(filePath)) {
177
+ await treeNode.focus();
178
+ } else {
179
+ await treeNode.click({ modifiers: ['Control'] });
180
+ // make sure the click has been acted-upon before returning
181
+ while (!await this.isTreeNodeSelected(filePath)) {
182
+ console.debug('Waiting for clicked tree node to be selected: ' + filePath);
183
+ }
184
+ }
185
+ await this.page.waitForSelector(this.treeNodeSelector(filePath) + '.theia-mod-selected');
186
+ }
187
+
188
+ async isTreeNodeSelected(filePath: string): Promise<boolean> {
189
+ const treeNode = await this.page.waitForSelector(this.treeNodeSelector(filePath));
190
+ return elementContainsClass(treeNode, 'theia-mod-selected');
191
+ }
192
+
193
+ protected treeNodeSelector(filePath: string): string {
194
+ return `.theia-FileStatNode:has(#${normalizeId(this.treeNodeId(filePath))})`;
195
+ }
196
+
197
+ protected treeNodeId(filePath: string): string {
198
+ const workspacePath = this.app.workspace.path;
199
+ const nodeId = `${workspacePath}:${workspacePath}${OSUtil.fileSeparator}${filePath}`;
200
+ if (OSUtil.isWindows) {
201
+ return urlEncodePath(nodeId);
202
+ }
203
+ return nodeId;
204
+ }
205
+
206
+ async clickContextMenuItem(file: string, path: string[], nodeSegmentLabel?: string): Promise<void> {
207
+ await this.activate();
208
+ const fileStatNode = await this.fileStatNode(file, !!nodeSegmentLabel);
209
+ if (!fileStatNode) { throw Error('File stat node could not be retrieved by path fragments'); }
210
+ const menuItem = await fileStatNode.getMenuItemByNamePath(path, nodeSegmentLabel);
211
+ await menuItem.click();
212
+ }
213
+
214
+ protected async existsNode(path: string, isDirectory: boolean, compact = false): Promise<boolean> {
215
+ const fileStatNode = await this.fileStatNode(path, compact);
216
+ if (!fileStatNode) {
217
+ return false;
218
+ }
219
+ if (isDirectory) {
220
+ if (!await fileStatNode.isFolder()) {
221
+ throw Error(`FileStatNode for '${path}' is not a directory!`);
222
+ }
223
+ } else {
224
+ if (!await fileStatNode.isFile()) {
225
+ throw Error(`FileStatNode for '${path}' is not a file!`);
226
+ }
227
+ }
228
+ return true;
229
+ }
230
+
231
+ async existsFileNode(path: string): Promise<boolean> {
232
+ return this.existsNode(path, false);
233
+ }
234
+
235
+ async existsDirectoryNode(path: string, compact = false): Promise<boolean> {
236
+ return this.existsNode(path, true, compact);
237
+ }
238
+
239
+ async waitForTreeNodeVisible(path: string): Promise<void> {
240
+ // wait for tree node to be visible, e.g. after triggering create
241
+ const viewElement = await this.viewElement();
242
+ await viewElement?.waitForSelector(this.treeNodeSelector(path), { state: 'visible' });
243
+ }
244
+
245
+ async getNumberOfVisibleNodes(): Promise<number> {
246
+ await this.activate();
247
+ await this.refresh();
248
+ const fileStatElements = await this.visibleFileStatNodes(DOT_FILES_FILTER);
249
+ return fileStatElements.length;
250
+ }
251
+
252
+ async deleteNode(path: string, confirm = true, nodeSegmentLabel?: string): Promise<void> {
253
+ await this.activate();
254
+ await this.clickContextMenuItem(path, ['Delete'], nodeSegmentLabel);
255
+
256
+ const confirmDialog = new TheiaDialog(this.app);
257
+ await confirmDialog.waitForVisible();
258
+ confirm ? await confirmDialog.clickMainButton() : await confirmDialog.clickSecondaryButton();
259
+ await confirmDialog.waitForClosed();
260
+ }
261
+
262
+ async renameNode(path: string, newName: string, confirm = true, nodeSegmentLabel?: string): Promise<void> {
263
+ await this.activate();
264
+ await this.clickContextMenuItem(path, ['Rename'], nodeSegmentLabel);
265
+
266
+ const renameDialog = new TheiaRenameDialog(this.app);
267
+ await renameDialog.waitForVisible();
268
+ await renameDialog.enterNewName(newName);
269
+ await renameDialog.waitUntilMainButtonIsEnabled();
270
+ confirm ? await renameDialog.confirm() : await renameDialog.close();
271
+ await renameDialog.waitForClosed();
272
+ await this.refresh();
273
+ }
274
+
275
+ override async waitForVisible(): Promise<void> {
276
+ await super.waitForVisible();
277
+ await this.page.waitForSelector(this.tabSelector, { state: 'visible' });
278
+ }
279
+
280
+ /**
281
+ * Waits until some non-dot file nodes are visible
282
+ */
283
+ async waitForVisibleFileNodes(): Promise<void> {
284
+ while ((await this.visibleFileStatNodes(DOT_FILES_FILTER)).length === 0) {
285
+ console.debug('Awaiting for tree nodes to appear');
286
+ }
287
+ }
288
+
289
+ async waitForFileNodesToIncrease(numberBefore: number): Promise<void> {
290
+ const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
291
+ await this.page.waitForFunction(
292
+ (predicate: { selector: string; numberBefore: number; }) => {
293
+ const elements = document.querySelectorAll(predicate.selector);
294
+ return !!elements && elements.length > predicate.numberBefore;
295
+ },
296
+ { selector: fileStatNodesSelector, numberBefore }
297
+ );
298
+ }
299
+
300
+ async waitForFileNodesToDecrease(numberBefore: number): Promise<void> {
301
+ const fileStatNodesSelector = `${this.viewSelector} .theia-FileStatNode`;
302
+ await this.page.waitForFunction(
303
+ (predicate: { selector: string; numberBefore: number; }) => {
304
+ const elements = document.querySelectorAll(predicate.selector);
305
+ return !!elements && elements.length < predicate.numberBefore;
306
+ },
307
+ { selector: fileStatNodesSelector, numberBefore }
308
+ );
309
+ }
310
+
311
+ }