@opensumi/playwright 2.21.4 → 2.21.5-next-1668665440.0

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 (89) hide show
  1. package/lib/debug-view.js +1 -1
  2. package/lib/diff-editor.d.ts +4 -0
  3. package/lib/diff-editor.d.ts.map +1 -0
  4. package/lib/diff-editor.js +8 -0
  5. package/lib/diff-editor.js.map +1 -0
  6. package/lib/editor.d.ts +2 -2
  7. package/lib/editor.d.ts.map +1 -1
  8. package/lib/editor.js.map +1 -1
  9. package/lib/explorer-view.d.ts +1 -0
  10. package/lib/explorer-view.d.ts.map +1 -1
  11. package/lib/explorer-view.js +30 -9
  12. package/lib/explorer-view.js.map +1 -1
  13. package/lib/filetree-view.js +2 -2
  14. package/lib/filetree-view.js.map +1 -1
  15. package/lib/opened-editor-view.js +2 -2
  16. package/lib/opened-editor-view.js.map +1 -1
  17. package/lib/panel.js +2 -2
  18. package/lib/panel.js.map +1 -1
  19. package/lib/scm-view.d.ts +21 -0
  20. package/lib/scm-view.d.ts.map +1 -0
  21. package/lib/scm-view.js +65 -0
  22. package/lib/scm-view.js.map +1 -0
  23. package/lib/search-view.js +1 -1
  24. package/lib/source-control-view.d.ts +8 -0
  25. package/lib/source-control-view.d.ts.map +1 -0
  26. package/lib/source-control-view.js +45 -0
  27. package/lib/source-control-view.js.map +1 -0
  28. package/lib/tests/scm.test.js +41 -3
  29. package/lib/tests/scm.test.js.map +1 -1
  30. package/lib/tests/settings.test.d.ts +2 -0
  31. package/lib/tests/settings.test.d.ts.map +1 -0
  32. package/lib/tests/settings.test.js +83 -0
  33. package/lib/tests/settings.test.js.map +1 -0
  34. package/lib/text-editor.d.ts +2 -2
  35. package/lib/text-editor.d.ts.map +1 -1
  36. package/lib/text-editor.js.map +1 -1
  37. package/lib/tree-node.d.ts +3 -1
  38. package/lib/tree-node.d.ts.map +1 -1
  39. package/lib/tree-node.js +3 -3
  40. package/lib/tree-node.js.map +1 -1
  41. package/package.json +4 -3
  42. package/src/app.ts +135 -0
  43. package/src/component-editor.ts +64 -0
  44. package/src/constans/index.ts +18 -0
  45. package/src/context-menu.ts +27 -0
  46. package/src/debug-view.ts +24 -0
  47. package/src/diff-editor.ts +3 -0
  48. package/src/editor.ts +130 -0
  49. package/src/explorer-view.ts +142 -0
  50. package/src/filetree-view.ts +28 -0
  51. package/src/index.ts +19 -0
  52. package/src/menu-item.ts +40 -0
  53. package/src/menu.ts +69 -0
  54. package/src/menubar.ts +53 -0
  55. package/src/opened-editor-view.ts +28 -0
  56. package/src/panel.ts +50 -0
  57. package/src/quick-command-palette.ts +62 -0
  58. package/src/quick-open-palette.ts +62 -0
  59. package/src/scm-view.ts +73 -0
  60. package/src/search-view.ts +44 -0
  61. package/src/source-control-view.ts +44 -0
  62. package/src/terminal.ts +22 -0
  63. package/src/tests/app.test.ts +16 -0
  64. package/src/tests/debug.test.ts +68 -0
  65. package/src/tests/editor.test.ts +143 -0
  66. package/src/tests/explorer-view.test.ts +202 -0
  67. package/src/tests/hooks/index.ts +13 -0
  68. package/src/tests/keymaps.test.ts +118 -0
  69. package/src/tests/language.test.ts +55 -0
  70. package/src/tests/scm.test.ts +84 -0
  71. package/src/tests/search-view.test.ts +32 -0
  72. package/src/tests/settings.test.ts +103 -0
  73. package/src/tests/workspaces/debug/.sumi/launch.json +15 -0
  74. package/src/tests/workspaces/debug/index.js +18 -0
  75. package/src/tests/workspaces/default/editor.js +0 -0
  76. package/src/tests/workspaces/default/editor2.js +87 -0
  77. package/src/tests/workspaces/default/editor3.js +0 -0
  78. package/src/tests/workspaces/default/test/test.js +1 -0
  79. package/src/tests/workspaces/git-workspace/a.js +0 -0
  80. package/src/tests/workspaces/language/definition.ts +12 -0
  81. package/src/tests/workspaces/language/reference.ts +9 -0
  82. package/src/text-editor.ts +318 -0
  83. package/src/tree-node.ts +98 -0
  84. package/src/utils/element.ts +35 -0
  85. package/src/utils/index.ts +2 -0
  86. package/src/utils/key.ts +6 -0
  87. package/src/view-base.ts +11 -0
  88. package/src/view.ts +90 -0
  89. package/src/workspace.ts +36 -0
@@ -0,0 +1,84 @@
1
+ import path from 'path';
2
+
3
+ import { expect } from '@playwright/test';
4
+
5
+ import { OpenSumiApp } from '../app';
6
+ import { OpenSumiDiffEditor } from '../diff-editor';
7
+ import { OpenSumiExplorerView } from '../explorer-view';
8
+ import { OpenSumiFileTreeView } from '../filetree-view';
9
+ import { OpenSumiSCMView } from '../scm-view';
10
+ import { OpenSumiTerminal } from '../terminal';
11
+ import { OpenSumiWorkspace } from '../workspace';
12
+
13
+ import test, { page } from './hooks';
14
+
15
+ let app: OpenSumiApp;
16
+ let explorer: OpenSumiExplorerView;
17
+ let scm: OpenSumiSCMView;
18
+ let fileTreeView: OpenSumiFileTreeView;
19
+ let workspace: OpenSumiWorkspace;
20
+
21
+ test.describe('OpenSumi SCM Panel', () => {
22
+ test.beforeAll(async () => {
23
+ workspace = new OpenSumiWorkspace([path.resolve('./src/tests/workspaces/git-workspace')]);
24
+ app = await OpenSumiApp.load(page, workspace);
25
+ explorer = await app.open(OpenSumiExplorerView);
26
+ explorer.initFileTreeView(workspace.workspace.displayName);
27
+ fileTreeView = explorer.fileTreeView;
28
+ const terminal = await app.open(OpenSumiTerminal);
29
+ // There should have GIT on the PATH
30
+ await terminal.sendText('git init');
31
+ });
32
+
33
+ test.afterAll(() => {
34
+ app.dispose();
35
+ });
36
+
37
+ test('The "U" charset should on the files tail after git initialized', async () => {
38
+ await explorer.fileTreeView.open();
39
+ const action = await fileTreeView.getTitleActionByName('Refresh');
40
+ await action?.click();
41
+ await app.page.waitForTimeout(2000);
42
+ const node = await explorer.getFileStatTreeNodeByPath('a.js');
43
+ const badge = await node?.badge();
44
+ expect(badge).toBe('U');
45
+ });
46
+
47
+ test('The "U" charset should on the files tail on SCM view', async () => {
48
+ scm = await app.open(OpenSumiSCMView);
49
+ await scm.open();
50
+ await app.page.waitForTimeout(2000);
51
+ const node = await scm.getFileStatTreeNodeByPath('a.js');
52
+ const badge = await node?.getBadge();
53
+ expect(badge).toBe('U');
54
+ });
55
+
56
+ test('Open file from context menu', async () => {
57
+ scm = await app.open(OpenSumiSCMView);
58
+ await scm.open();
59
+ const node = await scm.getFileStatTreeNodeByPath('a.js');
60
+ const item = await node?.getMenuItemByName('Open File');
61
+ await item?.click();
62
+ await app.page.waitForTimeout(1000);
63
+ if (node) {
64
+ const editor = new OpenSumiDiffEditor(app, node);
65
+ const currentTab = await editor.getCurrentTab();
66
+ const dataUri = await currentTab?.getAttribute('data-uri');
67
+ expect(dataUri?.startsWith('file')).toBeTruthy();
68
+ }
69
+ });
70
+
71
+ test('Open file with diff editor', async () => {
72
+ scm = await app.open(OpenSumiSCMView);
73
+ await scm.open();
74
+ const node = await scm.getFileStatTreeNodeByPath('a.js');
75
+ await node?.open();
76
+ await app.page.waitForTimeout(1000);
77
+ if (node) {
78
+ const editor = new OpenSumiDiffEditor(app, node);
79
+ const currentTab = await editor.getCurrentTab();
80
+ const dataUri = await currentTab?.getAttribute('data-uri');
81
+ expect(dataUri?.startsWith('diff')).toBeTruthy();
82
+ }
83
+ });
84
+ });
@@ -0,0 +1,32 @@
1
+ import path from 'path';
2
+
3
+ import { expect } from '@playwright/test';
4
+
5
+ import { OpenSumiApp } from '..';
6
+ import { OpenSumiSearchView } from '../search-view';
7
+ import { OpenSumiWorkspace } from '../workspace';
8
+
9
+ import test, { page } from './hooks';
10
+
11
+ let app: OpenSumiApp;
12
+ let search: OpenSumiSearchView;
13
+
14
+ test.describe('OpenSumi Search Panel', () => {
15
+ test.beforeAll(async () => {
16
+ const workspace = new OpenSumiWorkspace([path.resolve('./src/tests/workspaces/default')]);
17
+ app = await OpenSumiApp.load(page, workspace);
18
+ search = await app.open(OpenSumiSearchView);
19
+ });
20
+
21
+ test('Can search files by simple text', async () => {
22
+ const searchText = 'hello';
23
+ expect(await search.isVisible()).toBeTruthy();
24
+ await search.focusOnSearch();
25
+ await page.keyboard.type(searchText);
26
+ // search panel should searched after typing
27
+ await app.page.keyboard.press('Enter');
28
+ const { results, files } = await search.getSearchResult();
29
+ expect(results).toBe(1);
30
+ expect(files).toBe(1);
31
+ });
32
+ });
@@ -0,0 +1,103 @@
1
+ import path from 'path';
2
+
3
+ import { expect } from '@playwright/test';
4
+
5
+ import { OpenSumiApp } from '../app';
6
+ import { OpenSumiComponentEditor } from '../component-editor';
7
+ import { OPENSUMI_VIEW_CONTAINERS } from '../constans';
8
+ import { OpenSumiContextMenu } from '../context-menu';
9
+ import { OpenSumiExplorerView } from '../explorer-view';
10
+ import { keypressWithCmdCtrl } from '../utils';
11
+ import { OpenSumiWorkspace } from '../workspace';
12
+
13
+ import test, { page } from './hooks';
14
+
15
+ let app: OpenSumiApp;
16
+ let explorer: OpenSumiExplorerView;
17
+ let workspace: OpenSumiWorkspace;
18
+
19
+ test.describe('OpenSumi Shortcuts', () => {
20
+ test.beforeAll(async () => {
21
+ workspace = new OpenSumiWorkspace([path.resolve('./src/tests/workspaces/default')]);
22
+ app = await OpenSumiApp.load(page, workspace);
23
+ explorer = await app.open(OpenSumiExplorerView);
24
+ explorer.initFileTreeView(workspace.workspace.displayName);
25
+ });
26
+
27
+ test.afterAll(() => {
28
+ app.dispose();
29
+ });
30
+
31
+ const openSettingsView = async () => {
32
+ const leftTabbar = await app.page.waitForSelector(`#${OPENSUMI_VIEW_CONTAINERS.LEFT_TABBAR}`);
33
+ const settingsButton = await leftTabbar.$('[class*="titleActions___"] span');
34
+ await settingsButton?.click();
35
+ const menu = new OpenSumiContextMenu(app);
36
+ await menu.clickMenuItem('Settings');
37
+ };
38
+
39
+ test('open Settings by keybinding', async () => {
40
+ await explorer.fileTreeView.focus();
41
+ await app.page.keyboard.press(keypressWithCmdCtrl(','), { delay: 200 });
42
+ const editor = await app.openComponentEditor(
43
+ OpenSumiComponentEditor,
44
+ 'pref:/',
45
+ 'Settings',
46
+ "[class*='preferences___']",
47
+ );
48
+ expect(await editor.isVisible()).toBeTruthy();
49
+ await editor.close();
50
+ await app.page.waitForTimeout(1000);
51
+ expect(await editor.isVisible()).toBeFalsy();
52
+ });
53
+
54
+ test('open Settings by settings button', async () => {
55
+ await openSettingsView();
56
+ const editor = await app.openComponentEditor(
57
+ OpenSumiComponentEditor,
58
+ 'pref:/',
59
+ 'Settings',
60
+ "[class*='preferences___']",
61
+ );
62
+ expect(await editor.isVisible()).toBeTruthy();
63
+ await editor.close();
64
+ });
65
+
66
+ test('edit settings in settings.json', async () => {
67
+ await openSettingsView();
68
+ const editor = await app.openComponentEditor(
69
+ OpenSumiComponentEditor,
70
+ 'pref:/',
71
+ 'Settings',
72
+ "[class*='preferences___']",
73
+ );
74
+ expect(await editor.isVisible()).toBeTruthy();
75
+ // Settings => Feature => Search > Include
76
+ const tabs = (await (await editor.getContainer())?.$$('[class*="index_item_wrapper__"]')) || [];
77
+ let featrueTab;
78
+ for (const tab of tabs) {
79
+ const title = await tab.textContent();
80
+ if (title === 'Feature') {
81
+ featrueTab = tab;
82
+ break;
83
+ }
84
+ }
85
+ await featrueTab.click();
86
+ await app.page.waitForTimeout(1000);
87
+ const items = (await (await editor.getContainer())?.$$('[class*="preference_item___"]')) || [];
88
+ let searchIncludeItem;
89
+ for (const item of items) {
90
+ const key = await (await item.$('[class*="key___"]'))?.textContent();
91
+ if (key === 'Search > Include ') {
92
+ searchIncludeItem = item;
93
+ break;
94
+ }
95
+ }
96
+ const editButton = await searchIncludeItem.$('[class*="control_wrap___"] a');
97
+ expect(editButton).toBeDefined();
98
+ await editButton.click();
99
+ await app.page.waitForTimeout(2000);
100
+ const currentTab = await editor.getCurrentTab();
101
+ expect(await currentTab?.textContent()).toBe(' settings.json');
102
+ });
103
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ // 使用 IntelliSense 了解相关属性。
3
+ // 悬停以查看现有属性的描述。
4
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "launch",
10
+ "name": "Launch Program",
11
+ "program": "${workspaceFolder}/index.js",
12
+ "skipFiles": ["<node_internals>/**"]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,18 @@
1
+ const str = 'hello';
2
+ const str2 = 'opensumi';
3
+ const buffStr = Buffer.from(str + str2);
4
+
5
+ const fn = () => {
6
+ let a = 0;
7
+ setTimeout(() => {
8
+ a += 1;
9
+ console.log(a);
10
+ }, 10);
11
+ console.log(a);
12
+ };
13
+
14
+ fn();
15
+
16
+ console.log(str);
17
+ console.log(str2);
18
+ console.log(buffStr);
File without changes
@@ -0,0 +1,87 @@
1
+ // ------------------------------
2
+ // ------------------------------
3
+ function Person(age) {
4
+ if (age) {
5
+ this.age = age;
6
+ }
7
+ }
8
+ Person.prototype.getAge = function () {
9
+ return this.age;
10
+ };
11
+
12
+ // ------------------------------
13
+ // ------------------------------
14
+ function Person(age) {
15
+ if (age) {
16
+ this.age = age;
17
+ }
18
+ }
19
+ Person.prototype.getAge = function () {
20
+ return this.age;
21
+ };
22
+
23
+ // ------------------------------
24
+ // ------------------------------
25
+ function Person(age) {
26
+ if (age) {
27
+ this.age = age;
28
+ }
29
+ }
30
+ Person.prototype.getAge = function () {
31
+ return this.age;
32
+ };
33
+
34
+ // ------------------------------
35
+ // ------------------------------
36
+ function Person(age) {
37
+ if (age) {
38
+ this.age = age;
39
+ }
40
+ }
41
+ Person.prototype.getAge = function () {
42
+ return this.age;
43
+ };
44
+
45
+ // ------------------------------
46
+ // ------------------------------And this is some long line. And this is some long line. And this is some long line. And this is some long line. And this is some long line.
47
+ function Person(age) {
48
+ if (age) {
49
+ this.age = age;
50
+ }
51
+ }
52
+ Person.prototype.getAge = function () {
53
+ return this.age;
54
+ };
55
+
56
+ // ------------------------------
57
+ // ------------------------------
58
+ function Person(age) {
59
+ if (age) {
60
+ this.age = age;
61
+ }
62
+ }
63
+ Person.prototype.getAge = function () {
64
+ return this.age;
65
+ };
66
+
67
+ // ------------------------------
68
+ // ------------------------------
69
+ function Person(age) {
70
+ if (age) {
71
+ this.age = age;
72
+ }
73
+ }
74
+ Person.prototype.getAge = function () {
75
+ return this.age;
76
+ };
77
+
78
+ // ------------------------------
79
+ // ------------------------------
80
+ function Person(age) {
81
+ if (age) {
82
+ this.age = age;
83
+ }
84
+ }
85
+ Person.prototype.getAge = function () {
86
+ return this.age;
87
+ };
File without changes
@@ -0,0 +1 @@
1
+ console.log('hello ./test/test.js');
File without changes
@@ -0,0 +1,12 @@
1
+ export class Definition {
2
+ private variable1: string = '';
3
+ constructor() {}
4
+
5
+ public setVariable1(s: string): void {
6
+ this.variable1 = s;
7
+ }
8
+
9
+ public getVariable1(): string {
10
+ return this.variable1;
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { Definition } from './definition';
2
+
3
+ export class Reference {
4
+ private def: Definition = new Definition();
5
+
6
+ constructor() {
7
+ this.def.setVariable1('6');
8
+ }
9
+ }
@@ -0,0 +1,318 @@
1
+ import { ElementHandle, Page } from '@playwright/test';
2
+
3
+ import { OpenSumiApp } from './app';
4
+ import { OpenSumiContextMenu } from './context-menu';
5
+ import { OpenSumiEditor } from './editor';
6
+ import { OpenSumiTreeNode } from './tree-node';
7
+ import { keypressWithCmdCtrl } from './utils';
8
+
9
+ abstract class ViewsModel {
10
+ constructor(readonly page: Page) {}
11
+ protected viewElement: ElementHandle<SVGElement | HTMLElement> | null;
12
+ async mount(v: ElementHandle<SVGElement | HTMLElement> | null): Promise<void> {
13
+ this.viewElement = v;
14
+ }
15
+ }
16
+
17
+ class GlyphMarginModel extends ViewsModel {
18
+ async getElement() {
19
+ const glyphMargin = await this.viewElement?.$('.glyph-margin');
20
+ const parent = await glyphMargin?.getProperty('parentNode');
21
+ return parent?.asElement();
22
+ }
23
+
24
+ async getOverlay(lineNumber: number) {
25
+ const margin = await this.getElement();
26
+ const overlays = await margin?.$$('.margin-view-overlays > div');
27
+ if (!overlays) {
28
+ return;
29
+ }
30
+
31
+ for (const node of overlays) {
32
+ const lineNode = await node.$('.line-numbers');
33
+ const content = await lineNode?.textContent();
34
+ if (content === lineNumber.toString()) {
35
+ return node;
36
+ }
37
+ }
38
+ }
39
+
40
+ async hasBreakpoint(node: ElementHandle<SVGElement | HTMLElement>): Promise<boolean> {
41
+ return !!(await node.$('.sumi-debug-breakpoint'));
42
+ }
43
+
44
+ async hasTopStackFrame(node: ElementHandle<SVGElement | HTMLElement>): Promise<boolean> {
45
+ return !!(await node.$('.sumi-debug-top-stack-frame'));
46
+ }
47
+
48
+ async hasTopStackFrameLine(node: ElementHandle<SVGElement | HTMLElement>): Promise<boolean> {
49
+ return !!(await node.$('.sumi-debug-top-stack-frame-line'));
50
+ }
51
+ }
52
+
53
+ class OverlaysModel extends ViewsModel {
54
+ async getElement() {
55
+ return await this.viewElement?.$('.view-overlays');
56
+ }
57
+
58
+ async getOverlay(lineNumber: number) {
59
+ const element = await this.getElement();
60
+ const overlay = await element?.$(`div:nth-child(${lineNumber})`);
61
+ return overlay;
62
+ }
63
+ }
64
+
65
+ export class OpenSumiTextEditor extends OpenSumiEditor {
66
+ private glyphMarginModel: GlyphMarginModel;
67
+ private overlaysModel: OverlaysModel;
68
+
69
+ constructor(app: OpenSumiApp, filestatElement: OpenSumiTreeNode) {
70
+ super(app, filestatElement);
71
+ this.glyphMarginModel = new GlyphMarginModel(this.page);
72
+ this.overlaysModel = new OverlaysModel(this.page);
73
+ }
74
+
75
+ async getGlyphMarginModel() {
76
+ const viewElement = await this.getViewElement();
77
+ this.glyphMarginModel.mount(viewElement);
78
+ return this.glyphMarginModel;
79
+ }
80
+
81
+ async getOverlaysModel() {
82
+ const viewElement = await this.getViewElement();
83
+ this.overlaysModel.mount(viewElement);
84
+ return this.overlaysModel;
85
+ }
86
+
87
+ async openLineContextMenuByLineNumber(lineNumber: number) {
88
+ const existingLine = await this.lineByLineNumber(lineNumber);
89
+ if (!existingLine) {
90
+ return;
91
+ }
92
+ return OpenSumiContextMenu.open(this.app, async () => existingLine);
93
+ }
94
+
95
+ async openGlyphMarginContextMenu() {
96
+ const glyphMargin = await this.getGlyphMarginModel();
97
+ const view = await glyphMargin.getElement();
98
+ if (!view) {
99
+ return;
100
+ }
101
+ return OpenSumiContextMenu.open(this.app, async () => view);
102
+ }
103
+
104
+ async openTabContextMenu() {
105
+ const view = await this.getTab();
106
+ if (!view) {
107
+ return;
108
+ }
109
+ return OpenSumiContextMenu.open(this.app, async () => view);
110
+ }
111
+
112
+ async numberOfLines(): Promise<number | undefined> {
113
+ await this.activate();
114
+ const viewElement = await this.getViewElement();
115
+ const lineElements = await viewElement?.$$('.view-lines .view-line');
116
+ return lineElements?.length;
117
+ }
118
+
119
+ async textContentOfLineByLineNumber(lineNumber: number): Promise<string | undefined> {
120
+ const lineElement = await this.lineByLineNumber(lineNumber);
121
+ const content = await lineElement?.textContent();
122
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
123
+ }
124
+
125
+ async replaceLineWithLineNumber(text: string, lineNumber: number): Promise<void> {
126
+ await this.selectLineWithLineNumber(lineNumber);
127
+ await this.typeTextAndHitEnter(text);
128
+ }
129
+
130
+ protected async typeTextAndHitEnter(text: string): Promise<void> {
131
+ await this.page.keyboard.type(text);
132
+ await this.page.keyboard.press('Enter');
133
+ }
134
+
135
+ async selectLineWithLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
136
+ await this.activate();
137
+ const lineElement = await this.lineByLineNumber(lineNumber);
138
+ await this.selectLine(lineElement);
139
+ return lineElement;
140
+ }
141
+
142
+ async placeCursorInLineWithLineNumber(
143
+ lineNumber: number,
144
+ ): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
145
+ await this.activate();
146
+ const lineElement = await this.lineByLineNumber(lineNumber);
147
+ await this.placeCursorInLine(lineElement);
148
+ return lineElement;
149
+ }
150
+
151
+ async placeCursorInLineWithPosition(
152
+ lineNumber: number,
153
+ columnNumber: number,
154
+ ): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
155
+ await this.activate();
156
+ const lineElement = await this.lineByLineNumber(lineNumber);
157
+ await this.placeCursorInLine(lineElement, 'start');
158
+ for (let i = 0; i < columnNumber; i++) {
159
+ await this.page.keyboard.press('ArrowRight', { delay: 200 });
160
+ }
161
+ return lineElement;
162
+ }
163
+
164
+ async deleteLineByLineNumber(lineNumber: number): Promise<void> {
165
+ await this.selectLineWithLineNumber(lineNumber);
166
+ await this.page.keyboard.press('Backspace');
167
+ }
168
+
169
+ async getGlyphMarginElement() {
170
+ await this.activate();
171
+ const viewElement = await this.getViewElement();
172
+ return await viewElement?.$('.glyph-margin');
173
+ }
174
+
175
+ async getCursorElement(): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
176
+ const viewElement = await this.getViewElement();
177
+
178
+ const cursorNode = await viewElement?.$('.cursor.monaco-mouse-cursor-text');
179
+ if (cursorNode) {
180
+ return cursorNode;
181
+ }
182
+ }
183
+
184
+ async getCursorLineNumber(node: ElementHandle<SVGElement | HTMLElement> | undefined) {
185
+ const style = await node!.getAttribute('style');
186
+ const tops = style?.match(/top: [0-9]*px;/g) || ['0'];
187
+ const topNums = tops[0].match(/\d+/g);
188
+ if (topNums && topNums.length > 0) {
189
+ let topNum: number | string = topNums[0];
190
+ topNum = Number(topNum);
191
+
192
+ // 每个 view-lines 默认高度都是 18
193
+ const line = topNum / 18 + 1;
194
+ return line;
195
+ }
196
+ return undefined;
197
+ }
198
+ async lineByLineNumber(lineNumber: number): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
199
+ await this.activate();
200
+ const viewElement = await this.getViewElement();
201
+
202
+ const lineNode = await viewElement!.$(`.view-lines > div:nth-child(${lineNumber})`);
203
+
204
+ if (!lineNode) {
205
+ throw new Error(`Couldn't retrieve lines of text editor ${this.tabSelector}`);
206
+ }
207
+
208
+ return lineNode.asElement();
209
+ }
210
+
211
+ async textContentOfLineContainingText(text: string): Promise<string | undefined> {
212
+ await this.activate();
213
+ const lineElement = await this.lineContainingText(text);
214
+ const content = await lineElement?.textContent();
215
+ return content ? this.replaceEditorSymbolsWithSpace(content) : undefined;
216
+ }
217
+
218
+ async replaceLineContainingText(newText: string, oldText: string): Promise<void> {
219
+ await this.selectLineContainingText(oldText);
220
+ await this.typeTextAndHitEnter(newText);
221
+ }
222
+
223
+ async selectLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
224
+ await this.activate();
225
+ const lineElement = await this.lineContainingText(text);
226
+ await this.selectLine(lineElement);
227
+ return lineElement;
228
+ }
229
+
230
+ async placeCursorInLineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
231
+ await this.activate();
232
+ const lineElement = await this.lineContainingText(text);
233
+ await this.placeCursorInLine(lineElement);
234
+ return lineElement;
235
+ }
236
+
237
+ async deleteLineContainingText(text: string): Promise<void> {
238
+ await this.selectLineContainingText(text);
239
+ await this.page.keyboard.press('Backspace');
240
+ }
241
+
242
+ async addTextToNewLineAfterLineContainingText(textContainedByExistingLine: string, newText: string): Promise<void> {
243
+ const existingLine = await this.lineContainingText(textContainedByExistingLine);
244
+ await this.placeCursorInLine(existingLine);
245
+ await this.page.keyboard.press('End');
246
+ await this.page.keyboard.press('Enter');
247
+ await this.page.keyboard.type(newText);
248
+ }
249
+
250
+ async addTextToNewLineAfterLineByLineNumber(lineNumber: number, newText: string): Promise<void> {
251
+ const existingLine = await this.lineByLineNumber(lineNumber);
252
+ await this.placeCursorInLine(existingLine);
253
+ await this.page.keyboard.press('End');
254
+ await this.page.keyboard.press('Enter');
255
+ await this.page.keyboard.type(newText);
256
+ }
257
+
258
+ async pasteContentAfterLineByLineNumber(lineNumber: number): Promise<void> {
259
+ const existingLine = await this.lineByLineNumber(lineNumber);
260
+ await this.placeCursorInLine(existingLine);
261
+ await this.page.keyboard.press('End');
262
+ await this.page.keyboard.press(keypressWithCmdCtrl('KeyV'));
263
+ }
264
+
265
+ protected async lineContainingText(text: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
266
+ const viewElement = await this.getViewElement();
267
+ return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
268
+ }
269
+
270
+ protected async selectLine(lineElement: ElementHandle<SVGElement | HTMLElement> | undefined): Promise<void> {
271
+ await lineElement?.click({ clickCount: 3 });
272
+ }
273
+
274
+ protected async placeCursorInLine(
275
+ lineElement: ElementHandle<SVGElement | HTMLElement> | undefined,
276
+ point: 'start' | 'end' = 'end',
277
+ ): Promise<void> {
278
+ if (!lineElement) {
279
+ return;
280
+ }
281
+
282
+ if (point === 'start') {
283
+ await lineElement.click({
284
+ position: { x: 0, y: 0 },
285
+ });
286
+ return;
287
+ }
288
+
289
+ await lineElement.click();
290
+ }
291
+
292
+ protected replaceEditorSymbolsWithSpace(content: string): string | Promise<string | undefined> {
293
+ // [ ] &nbsp; => \u00a0
294
+ // [·] &middot; => \u00b7
295
+ return content.replace(/[\u00a0\u00b7]/g, ' ');
296
+ }
297
+
298
+ protected async selectedSuggestion(): Promise<ElementHandle<SVGElement | HTMLElement>> {
299
+ return this.page.waitForSelector(this.viewSelector + ' .monaco-list-row.show-file-icons.focused');
300
+ }
301
+
302
+ async getSelectedSuggestionText(): Promise<string> {
303
+ const suggestion = await this.selectedSuggestion();
304
+ const text = await suggestion.textContent();
305
+ if (text === null) {
306
+ throw new Error('Text content could not be found');
307
+ }
308
+ return text;
309
+ }
310
+
311
+ async clearContent() {
312
+ const line = await this.lineByLineNumber(1);
313
+ await line?.click();
314
+ await this.placeCursorInLine(line);
315
+ await this.page.keyboard.press(keypressWithCmdCtrl('KeyA'));
316
+ await this.page.keyboard.press('Delete');
317
+ }
318
+ }