@theia/playwright 1.54.0 → 1.55.0-next.25

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 (49) hide show
  1. package/lib/index.d.ts +3 -0
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +3 -0
  4. package/lib/index.js.map +1 -1
  5. package/lib/tests/theia-explorer-view.test.js +3 -2
  6. package/lib/tests/theia-explorer-view.test.js.map +1 -1
  7. package/lib/tests/theia-notebook-editor.test.d.ts +2 -0
  8. package/lib/tests/theia-notebook-editor.test.d.ts.map +1 -0
  9. package/lib/tests/theia-notebook-editor.test.js +170 -0
  10. package/lib/tests/theia-notebook-editor.test.js.map +1 -0
  11. package/lib/tests/theia-quick-command.test.js +5 -0
  12. package/lib/tests/theia-quick-command.test.js.map +1 -1
  13. package/lib/theia-monaco-editor.d.ts +10 -0
  14. package/lib/theia-monaco-editor.d.ts.map +1 -1
  15. package/lib/theia-monaco-editor.js +29 -1
  16. package/lib/theia-monaco-editor.js.map +1 -1
  17. package/lib/theia-notebook-cell.d.ts +88 -0
  18. package/lib/theia-notebook-cell.d.ts.map +1 -0
  19. package/lib/theia-notebook-cell.js +195 -0
  20. package/lib/theia-notebook-cell.js.map +1 -0
  21. package/lib/theia-notebook-editor.d.ts +50 -0
  22. package/lib/theia-notebook-editor.d.ts.map +1 -0
  23. package/lib/theia-notebook-editor.js +153 -0
  24. package/lib/theia-notebook-editor.js.map +1 -0
  25. package/lib/theia-notebook-toolbar.d.ts +13 -0
  26. package/lib/theia-notebook-toolbar.d.ts.map +1 -0
  27. package/lib/theia-notebook-toolbar.js +47 -0
  28. package/lib/theia-notebook-toolbar.js.map +1 -0
  29. package/lib/theia-preference-view.d.ts +7 -1
  30. package/lib/theia-preference-view.d.ts.map +1 -1
  31. package/lib/theia-preference-view.js +16 -2
  32. package/lib/theia-preference-view.js.map +1 -1
  33. package/lib/theia-quick-command-palette.d.ts +1 -0
  34. package/lib/theia-quick-command-palette.d.ts.map +1 -1
  35. package/lib/theia-quick-command-palette.js +8 -0
  36. package/lib/theia-quick-command-palette.js.map +1 -1
  37. package/package.json +3 -3
  38. package/src/index.ts +3 -0
  39. package/src/tests/resources/notebook-files/.theia/settings.json +3 -0
  40. package/src/tests/resources/notebook-files/sample.ipynb +18 -0
  41. package/src/tests/theia-explorer-view.test.ts +3 -2
  42. package/src/tests/theia-notebook-editor.test.ts +209 -0
  43. package/src/tests/theia-quick-command.test.ts +6 -0
  44. package/src/theia-monaco-editor.ts +31 -1
  45. package/src/theia-notebook-cell.ts +232 -0
  46. package/src/theia-notebook-editor.ts +171 -0
  47. package/src/theia-notebook-toolbar.ts +53 -0
  48. package/src/theia-preference-view.ts +14 -2
  49. package/src/theia-quick-command-palette.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theia/playwright",
3
- "version": "1.54.0",
3
+ "version": "1.55.0-next.25+2be612f8a",
4
4
  "description": "System tests for Theia",
5
5
  "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
6
6
  "repository": {
@@ -39,12 +39,12 @@
39
39
  "allure-commandline": "^2.23.1",
40
40
  "allure-playwright": "^2.5.0",
41
41
  "cross-env": "^7.0.3",
42
- "rimraf": "^2.6.1",
42
+ "rimraf": "^5.0.0",
43
43
  "typescript": "~5.4.5"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
48
  "main": "lib/index",
49
- "gitHead": "8fb36a237db744cff6e78eaff1481e1f36bb7a69"
49
+ "gitHead": "2be612f8a7efb90c1183ba47f0897040925b304a"
50
50
  }
package/src/index.ts CHANGED
@@ -26,6 +26,9 @@ export * from './theia-menu-item';
26
26
  export * from './theia-menu';
27
27
  export * from './theia-notification-indicator';
28
28
  export * from './theia-notification-overlay';
29
+ export * from './theia-notebook-cell';
30
+ export * from './theia-notebook-editor';
31
+ export * from './theia-notebook-toolbar';
29
32
  export * from './theia-output-channel';
30
33
  export * from './theia-output-view';
31
34
  export * from './theia-page-object';
@@ -0,0 +1,3 @@
1
+ {
2
+ "files.autoSave": "off"
3
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": []
9
+ }
10
+ ],
11
+ "metadata": {
12
+ "language_info": {
13
+ "name": "python"
14
+ }
15
+ },
16
+ "nbformat": 4,
17
+ "nbformat_minor": 2
18
+ }
@@ -183,7 +183,8 @@ test.describe('Theia Explorer View', () => {
183
183
  expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
184
184
  });
185
185
 
186
- test('should delete nested folder "sampleDirectoryCompact/nestedFolder1/nestedFolder2"', async () => {
186
+ // TODO These tests only seems to fail on Ubuntu - it's not clear why
187
+ test.skip('should delete nested folder "sampleDirectoryCompact/nestedFolder1/nestedFolder2"', async () => {
187
188
  const fileStatElements = await explorer.visibleFileStatNodes();
188
189
  expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
189
190
  await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* confirm */, 'nestedFolder2' /* nodeSegmentLabel */);
@@ -192,7 +193,7 @@ test.describe('Theia Explorer View', () => {
192
193
  expect(updatedFileStatElements.length).toBe(fileStatElements.length - 1);
193
194
  });
194
195
 
195
- test('should delete compact folder "sampleDirectoryCompact/nestedFolder1"', async () => {
196
+ test.skip('should delete compact folder "sampleDirectoryCompact/nestedFolder1"', async () => {
196
197
  const fileStatElements = await explorer.visibleFileStatNodes();
197
198
  expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1', true /* compact */)).toBe(true);
198
199
  await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1', true /* confirm */, 'sampleDirectoryCompact' /* nodeSegmentLabel */);
@@ -0,0 +1,209 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 TypeFox GmbH 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 { PlaywrightWorkerArgs, expect, test } from '@playwright/test';
18
+ import { TheiaApp } from '../theia-app';
19
+ import { TheiaAppLoader, TheiaPlaywrightTestConfig } from '../theia-app-loader';
20
+ import { TheiaNotebookCell } from '../theia-notebook-cell';
21
+ import { TheiaNotebookEditor } from '../theia-notebook-editor';
22
+ import { TheiaWorkspace } from '../theia-workspace';
23
+ import path = require('path');
24
+
25
+ // See .github/workflows/playwright.yml for preferred python version
26
+ const preferredKernel = process.env.CI ? 'Python 3.11' : 'Python 3';
27
+
28
+ test.describe('Theia Notebook Editor interaction', () => {
29
+
30
+ let app: TheiaApp;
31
+ let editor: TheiaNotebookEditor;
32
+
33
+ test.beforeAll(async ({ playwright, browser }) => {
34
+ app = await loadApp({ playwright, browser });
35
+ });
36
+
37
+ test.beforeEach(async ({ playwright, browser }) => {
38
+ editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
39
+ });
40
+
41
+ test.afterAll(async () => {
42
+ await app.page.close();
43
+ });
44
+
45
+ test.afterEach(async () => {
46
+ if (editor) {
47
+ await editor.closeWithoutSave();
48
+ }
49
+ });
50
+
51
+ test('kernels are installed', async () => {
52
+ const kernels = await editor.availableKernels();
53
+ const msg = `Available kernels:\n ${kernels.join('\n')}`;
54
+ console.log(msg); // Print available kernels, useful when running in CI.
55
+ expect(kernels.length, msg).toBeGreaterThan(0);
56
+
57
+ const py3kernel = kernels.filter(kernel => kernel.match(new RegExp(`^${preferredKernel}`)));
58
+ expect(py3kernel.length, msg).toBeGreaterThan(0);
59
+ });
60
+
61
+ test('should select a kernel', async () => {
62
+ await editor.selectKernel(preferredKernel);
63
+ const selectedKernel = await editor.selectedKernel();
64
+ expect(selectedKernel).toMatch(new RegExp(`^${preferredKernel}`));
65
+ });
66
+
67
+ test('should add a new code cell', async () => {
68
+ await editor.addCodeCell();
69
+ const cells = await editor.cells();
70
+ expect(cells.length).toBe(2);
71
+ expect(await cells[1].mode()).toBe('python');
72
+ });
73
+
74
+ test('should add a new markdown cell', async () => {
75
+ await editor.addMarkdownCell();
76
+ await (await editor.cells())[1].addEditorText('print("markdown")');
77
+
78
+ const cells = await editor.cells();
79
+ expect(cells.length).toBe(2);
80
+ expect(await cells[1].mode()).toBe('markdown');
81
+ expect(await cells[1].editorText()).toBe('print("markdown")');
82
+ });
83
+
84
+ test('should execute all cells', async () => {
85
+ const cell = await firstCell(editor);
86
+ await cell.addEditorText('print("Hallo Notebook!")');
87
+
88
+ await editor.addCodeCell();
89
+ const secondCell = (await editor.cells())[1];
90
+ await secondCell.addEditorText('print("Bye Notebook!")');
91
+
92
+ await editor.executeAllCells();
93
+
94
+ expect(await cell.outputText()).toBe('Hallo Notebook!');
95
+ expect(await secondCell.outputText()).toBe('Bye Notebook!');
96
+ });
97
+
98
+ test('should split cell', async () => {
99
+ const cell = await firstCell(editor);
100
+ /*
101
+ Add cell text:
102
+ print("Line-1")
103
+ print("Line-2")
104
+ */
105
+ await cell.addEditorText('print("Line-1")\nprint("Line-2")');
106
+
107
+ /*
108
+ Set cursor:
109
+ print("Line-1")
110
+ <|>print("Line-2")
111
+ */
112
+ const line = await cell.editor.lineByLineNumber(1);
113
+ await line?.waitForElementState('visible');
114
+ await line?.click();
115
+ await line?.press('ArrowRight');
116
+
117
+ // split cell
118
+ await cell.splitCell();
119
+
120
+ // expect two cells with text "print("Line-1")" and "print("Line-2")"
121
+ expect(await editor.cells()).toHaveLength(2);
122
+ expect(await (await editor.cells())[0].editorText()).toBe('print("Line-1")');
123
+ expect(await (await editor.cells())[1].editorText()).toBe('print("Line-2")');
124
+ });
125
+ });
126
+
127
+ test.describe('Theia Notebook Cell interaction', () => {
128
+
129
+ let app: TheiaApp;
130
+ let editor: TheiaNotebookEditor;
131
+
132
+ test.beforeAll(async ({ playwright, browser }) => {
133
+ app = await loadApp({ playwright, browser });
134
+ });
135
+
136
+ test.afterAll(async () => {
137
+ await app.page.close();
138
+ });
139
+
140
+ test.beforeEach(async () => {
141
+ editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
142
+ const selectedKernel = await editor.selectedKernel();
143
+ if (selectedKernel?.match(new RegExp(`^${preferredKernel}`)) === null) {
144
+ await editor.selectKernel(preferredKernel);
145
+ }
146
+ });
147
+
148
+ test.afterEach(async () => {
149
+ if (editor) {
150
+ await editor.closeWithoutSave();
151
+ }
152
+ });
153
+
154
+ test('should write text in a code cell', async () => {
155
+ const cell = await firstCell(editor);
156
+ // assume the first cell is a code cell
157
+ expect(await cell.isCodeCell()).toBe(true);
158
+
159
+ await cell.addEditorText('print("Hallo")');
160
+ const cellText = await cell.editorText();
161
+ expect(cellText).toBe('print("Hallo")');
162
+ });
163
+
164
+ test('should write multi-line text in a code cell', async () => {
165
+ const cell = await firstCell(editor);
166
+ await cell.addEditorText('print("Hallo")\nprint("Notebook")');
167
+
168
+ const cellText = await cell.editorText();
169
+ expect(cellText).toBe('print("Hallo")\nprint("Notebook")');
170
+ });
171
+
172
+ test('Execute code cell and read output', async () => {
173
+ const cell = await firstCell(editor);
174
+ await cell.addEditorText('print("Hallo Notebook!")');
175
+ await cell.execute();
176
+
177
+ const cellOutput = await cell.outputText();
178
+ expect(cellOutput).toBe('Hallo Notebook!');
179
+ });
180
+
181
+ test('Check execution count matches', async () => {
182
+ const cell = await firstCell(editor);
183
+ await cell.addEditorText('print("Hallo Notebook!")');
184
+ await cell.execute();
185
+ await cell.execute();
186
+ await cell.execute();
187
+
188
+ expect(await cell.executionCount()).toBe('3');
189
+ });
190
+
191
+ });
192
+
193
+ async function firstCell(editor: TheiaNotebookEditor): Promise<TheiaNotebookCell> {
194
+ return (await editor.cells())[0];
195
+ }
196
+
197
+ async function loadApp(args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs): Promise<TheiaApp> {
198
+ const workingDir = path.resolve();
199
+ // correct WS path. When running from IDE the path is playwright/configs with CLI it's playwright/
200
+ const prefix = workingDir.endsWith('playwright/configs') ? '../' : '';
201
+ const ws = new TheiaWorkspace([prefix + 'src/tests/resources/notebook-files']);
202
+ const app = await TheiaAppLoader.load(args, ws);
203
+ // auto-save are disabled using settings.json file
204
+ // see examples/playwright/src/tests/resources/notebook-files/.theia/settings.json
205
+
206
+ // NOTE: Workspace trust is disabled in examples/browser/package.json using default preferences.
207
+ // If workspace trust check is on, python extension will not be able to explore Python installations.
208
+ return app;
209
+ }
@@ -77,4 +77,10 @@ test.describe('Theia Quick Command', () => {
77
77
  expect(await notification.isEntryVisible('Positive Integer: 6')).toBe(true);
78
78
  });
79
79
 
80
+ test('retrieve and check visible items', async () => {
81
+ await quickCommand.type('close all tabs', false);
82
+ const listItems = await Promise.all((await quickCommand.visibleItems()).map(async item => item.textContent()));
83
+ expect(listItems).toContain('View: Close All Tabs in Main Area');
84
+ });
85
+
80
86
  });
@@ -27,7 +27,7 @@ export class TheiaMonacoEditor extends TheiaPageObject {
27
27
  await this.page.waitForSelector(this.selector, { state: 'visible' });
28
28
  }
29
29
 
30
- protected viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
30
+ protected async viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
31
31
  return this.page.$(this.selector);
32
32
  }
33
33
 
@@ -74,6 +74,36 @@ export class TheiaMonacoEditor extends TheiaPageObject {
74
74
  return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
75
75
  }
76
76
 
77
+ /**
78
+ * @returns The text content of the editor.
79
+ */
80
+ async editorText(): Promise<string | undefined> {
81
+ const lines: string[] = [];
82
+ const linesCount = await this.numberOfLines();
83
+ if (linesCount === undefined) {
84
+ return undefined;
85
+ }
86
+ for (let line = 1; line <= linesCount; line++) {
87
+ const lineText = await this.textContentOfLineByLineNumber(line);
88
+ if (lineText === undefined) {
89
+ break;
90
+ }
91
+ lines.push(lineText);
92
+ }
93
+ return lines.join('\n');
94
+ }
95
+
96
+ /**
97
+ * Adds text to the editor.
98
+ * @param text The text to add to the editor.
99
+ * @param lineNumber The line number where to add the text. Default is 1.
100
+ */
101
+ async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
102
+ const line = await this.lineByLineNumber(lineNumber);
103
+ await line?.click();
104
+ await this.page.keyboard.type(text);
105
+ }
106
+
77
107
  protected replaceEditorSymbolsWithSpace(content: string): string | Promise<string | undefined> {
78
108
  // [ ] &nbsp; => \u00a0 -- NO-BREAK SPACE
79
109
  // [·] &middot; => \u00b7 -- MIDDLE DOT
@@ -0,0 +1,232 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 TypeFox GmbH 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
+ import { ElementHandle, FrameLocator, Locator } from '@playwright/test';
17
+ import { TheiaApp } from './theia-app';
18
+ import { TheiaMonacoEditor } from './theia-monaco-editor';
19
+ import { TheiaPageObject } from './theia-page-object';
20
+
21
+ export type CellStatus = 'success' | 'error' | 'waiting';
22
+
23
+ /**
24
+ * Page object for a Theia notebook cell.
25
+ */
26
+ export class TheiaNotebookCell extends TheiaPageObject {
27
+
28
+ protected monacoEditor: TheiaEmbeddedMonacoEditor;
29
+
30
+ constructor(protected readonly locator: Locator, protected readonly notebookEditorLocator: Locator, app: TheiaApp) {
31
+ super(app);
32
+ const editorLocator = locator.locator('div.theia-notebook-cell-editor');
33
+ this.monacoEditor = new TheiaEmbeddedMonacoEditor(editorLocator, app);
34
+ }
35
+
36
+ /**
37
+ * @returns The monaco editor page object of the cell.
38
+ */
39
+ get editor(): TheiaEmbeddedMonacoEditor {
40
+ return this.monacoEditor;
41
+ }
42
+
43
+ /**
44
+ * @returns Locator for the sidebar (left) of the cell.
45
+ */
46
+ sidebar(): Locator {
47
+ return this.locator.locator('div.theia-notebook-cell-sidebar');
48
+ }
49
+
50
+ /**
51
+ * @returns Locator for the toolbar (top) of the cell.
52
+ */
53
+ toolbar(): Locator {
54
+ return this.locator.locator('div.theia-notebook-cell-toolbar');
55
+ }
56
+ /**
57
+ * @returns Locator for the statusbar (bottom) of the cell.
58
+ */
59
+ statusbar(): Locator {
60
+ return this.locator.locator('div.notebook-cell-status');
61
+ }
62
+
63
+ /**
64
+ * @returns Locator for the status icon inside the statusbar of the cell.
65
+ */
66
+ statusIcon(): Locator {
67
+ return this.statusbar().locator('span.notebook-cell-status-item');
68
+ }
69
+
70
+ /**
71
+ * @returns `true` id the cell is a code cell, `false` otherwise.
72
+ */
73
+ async isCodeCell(): Promise<boolean> {
74
+ const classAttribute = await this.mode();
75
+ return classAttribute !== 'markdown';
76
+ }
77
+
78
+ /**
79
+ * @returns The mode of the cell, e.g. 'python', 'markdown', etc.
80
+ */
81
+ async mode(): Promise<string> {
82
+ await this.locator.waitFor({ state: 'visible' });
83
+ const editorElement = await this.editor.locator.elementHandle();
84
+ if (editorElement === null) {
85
+ throw new Error('Could not find editor element for the notebook cell.');
86
+ }
87
+ const classAttribute = await editorElement.getAttribute('data-mode-id');
88
+ if (classAttribute === null) {
89
+ throw new Error('Could not find mode attribute for the notebook cell.');
90
+ }
91
+ return classAttribute;
92
+ }
93
+
94
+ /**
95
+ * @returns The text content of the cell editor.
96
+ */
97
+ async editorText(): Promise<string | undefined> {
98
+ return this.editor.editorText();
99
+ }
100
+
101
+ /**
102
+ * Adds text to the editor of the cell.
103
+ * @param text The text to add to the editor.
104
+ * @param lineNumber The line number where to add the text. Default is 1.
105
+ */
106
+ async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
107
+ await this.editor.addEditorText(text, lineNumber);
108
+ }
109
+
110
+ /**
111
+ * @param wait If `true` waits for the cell to finish execution, otherwise returns immediately.
112
+ */
113
+ async execute(wait = true): Promise<void> {
114
+ const execButton = this.sidebar().locator('[id="notebook.cell.execute-cell"]');
115
+ await execButton.waitFor({ state: 'visible' });
116
+ await execButton.click();
117
+ if (wait) {
118
+ // wait for the cell to finish execution
119
+ await this.waitForCellStatus('success', 'error');
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Splits the cell into two cells by dividing the cell text on current cursor position.
125
+ */
126
+ async splitCell(): Promise<void> {
127
+ const execButton = this.toolbar().locator('[id="notebook.cell.split"]');
128
+ await execButton.waitFor({ state: 'visible' });
129
+ await execButton.click();
130
+ }
131
+
132
+ /**
133
+ * Waits for the cell to reach a specific status.
134
+ * @param status The status to wait for. Possible values are 'success', 'error', 'waiting'.
135
+ */
136
+ async waitForCellStatus(...status: CellStatus[]): Promise<void> {
137
+ await this.statusIcon().waitFor({ state: 'visible' });
138
+ await this.statusIcon().evaluate(
139
+ (element, expect) => {
140
+ if (expect.length === 0) {
141
+ return true;
142
+ }
143
+ const classes = element.getAttribute('class');
144
+ if (classes !== null) {
145
+ const cellStatus = classes.includes('codicon-check') ? 'success'
146
+ : classes.includes('codicon-error') ? 'error'
147
+ : 'waiting';
148
+ return expect.includes(cellStatus);
149
+ }
150
+ return false;
151
+ }, status);
152
+ }
153
+
154
+ /**
155
+ * @returns The status of the cell. Possible values are 'success', 'error', 'waiting'.
156
+ */
157
+ async status(): Promise<CellStatus> {
158
+ const statusLocator = this.statusIcon();
159
+ const status = this.toCellStatus(await (await statusLocator.elementHandle())?.getAttribute('class') ?? '');
160
+ return status;
161
+ }
162
+
163
+ protected toCellStatus(classes: string): CellStatus {
164
+ return classes.includes('codicon-check') ? 'success'
165
+ : classes.includes('codicon-error') ? 'error'
166
+ : 'waiting';
167
+ }
168
+
169
+ /**
170
+ * @returns The execution count of the cell.
171
+ */
172
+ async executionCount(): Promise<string | undefined> {
173
+ const countNode = this.sidebar().locator('span.theia-notebook-code-cell-execution-order');
174
+ await countNode.waitFor({ state: 'visible' });
175
+ await this.waitForCellStatus('success', 'error');
176
+ const text = await countNode.textContent();
177
+ return text?.substring(1, text.length - 1);
178
+ }
179
+
180
+ /**
181
+ * @returns The output text of the cell.
182
+ */
183
+ async outputText(): Promise<string> {
184
+ const outputContainer = await this.outputContainer();
185
+ await outputContainer.waitFor({ state: 'visible' });
186
+ // By default just collect all spans text.
187
+ const spansLocator: Locator = outputContainer.locator('span:not(:has(*))'); // ignore nested spans
188
+ const spanTexts = await spansLocator.evaluateAll(spans => spans.map(span => span.textContent?.trim())
189
+ .filter(text => text !== undefined && text.length > 0));
190
+ return spanTexts.join('');
191
+ }
192
+
193
+ protected async outputContainer(): Promise<Locator> {
194
+ const outFrame = await this.outputFrame();
195
+ // each cell has it's own output div with a unique id = cellHandle<handle>
196
+ const cellOutput = outFrame.locator(`div#cellHandle${await this.cellHandle()}`);
197
+ return cellOutput.locator('div.output-container');
198
+ }
199
+
200
+ protected async cellHandle(): Promise<string | null> {
201
+ const handle = await this.locator.getAttribute('data-cell-handle');
202
+ if (handle === null) {
203
+ throw new Error('Could not find cell handle attribute `data-cell-handle` for the notebook cell.');
204
+ }
205
+ return handle;
206
+ }
207
+
208
+ protected async outputFrame(): Promise<FrameLocator> {
209
+ const containerDiv = this.notebookEditorLocator.locator('div.theia-notebook-cell-output-webview');
210
+ const webViewFrame = containerDiv.frameLocator('iframe.webview');
211
+ await webViewFrame.locator('iframe').waitFor({ state: 'attached' });
212
+ return webViewFrame.frameLocator('iframe');
213
+ }
214
+
215
+ }
216
+
217
+ export class TheiaEmbeddedMonacoEditor extends TheiaMonacoEditor {
218
+
219
+ constructor(public readonly locator: Locator, app: TheiaApp) {
220
+ super('', app);
221
+ }
222
+
223
+ override async waitForVisible(): Promise<void> {
224
+ // Use locator instead of page to find the editor element.
225
+ await this.locator.waitFor({ state: 'visible' });
226
+ }
227
+
228
+ protected override viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
229
+ // Use locator instead of page to find the editor element.
230
+ return this.locator.elementHandle();
231
+ }
232
+ }