Skip to content

Commit a1b55dd

Browse files
committed
fix: adapt tests after Monaco 1.108 uplift
- Add ESM loader hooks in @theia/test-setup to handle .css imports from @theia/monaco-editor-core ESM bundles during mocha test runs - Fix expose-loader to use module.exports instead of this for webpack compatibility with ESM modules, where this is undefined in arrow function wrappers - Move top-level const { timeout } declarations inside describe() blocks to avoid duplicate identifier errors when files share scope - Replace keyboard.type() with keyboard.insertText() in Playwright tests for Monaco EditContext API compatibility - Add TheiaMonacoEditor.typeText() static helper that handles multi-line text (insertText for content, Enter for newlines) - Update line-numbers selector to .margin-view-overlays .line-numbers to match Monaco 1.108 DOM - Add retry logic to navigator test cleanup on Windows to handle EBUSY errors from unreleased file handles after move operations Contributed on behalf of STMicroelectronics
1 parent ca87578 commit a1b55dd

File tree

9 files changed

+93
-15
lines changed

9 files changed

+93
-15
lines changed

dev-packages/application-manager/src/expose-loader.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ function exposeModule(modulePackage: { dir: string, name?: string }, resourcePat
4040
if (path.sep !== '/') {
4141
moduleName = moduleName.split(path.sep).join('/');
4242
}
43-
return source + `\n;(globalThis['theia'] = globalThis['theia'] || {})['${moduleName}'] = this;\n`;
43+
// Use `module.exports` with a fallback to `this` for compatibility with ESM modules.
44+
// Webpack wraps ESM modules in arrow functions where `this` is `undefined`,
45+
// but `module.exports` is available and points to the webpack exports object.
46+
return source + `\n;(globalThis['theia'] = globalThis['theia'] || {})['${moduleName}'] = (typeof module === 'object' && module.exports) || this;\n`;
4447
}
4548

4649
/**
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2026 STMicroelectronics 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+
// ESM loader hooks to handle non-JS file extensions (e.g. .css) that
18+
// are imported by ESM dependencies such as @theia/monaco-editor-core.
19+
// Without this, Node's ESM resolver throws ERR_UNKNOWN_FILE_EXTENSION
20+
// which causes mocha's import-then-require fallback to partially execute
21+
// test files before retrying, leading to duplicate side effects.
22+
23+
const STYLE_EXTENSIONS = ['.css', '.scss', '.sass', '.less'];
24+
25+
export function resolve(specifier, context, nextResolve) {
26+
return nextResolve(specifier, context);
27+
}
28+
29+
export function load(url, context, nextLoad) {
30+
if (STYLE_EXTENSIONS.some(ext => url.endsWith(ext))) {
31+
return { format: 'module', source: 'export default {};', shortCircuit: true };
32+
}
33+
return nextLoad(url, context);
34+
}

dev-packages/private-test-setup/test-setup.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,13 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17+
// Register ESM loader hooks so that non-JS imports (e.g. .css files from
18+
// @theia/monaco-editor-core ESM bundles) are handled before mocha attempts
19+
// to load test files. Without this, Node's ESM resolver fails on .css
20+
// imports, and mocha's import→require fallback causes files to be partially
21+
// executed twice, leading to side-effect duplication.
22+
const { register } = require('node:module');
23+
register('./esm-loader-hooks.mjs', require('node:url').pathToFileURL(__filename));
24+
1725
// Mock DragEvent as '@lumino/dragdrop' already requires it at require time
1826
global.DragEvent = class DragEvent { };

examples/api-tests/src/monaco-api.spec.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
const { timeout } = require('@theia/core/lib/common/promise-util');
18-
const { IOpenerService } = require('@theia/monaco-editor-core/esm/vs/platform/opener/common/opener');
19-
2017
// @ts-check
2118
describe('Monaco API', async function () {
2219
this.timeout(5000);
2320

2421
const { assert } = chai;
22+
const { timeout } = require('@theia/core/lib/common/promise-util');
23+
const { IOpenerService } = require('@theia/monaco-editor-core/esm/vs/platform/opener/common/opener');
2524

2625
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
2726
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');

examples/api-tests/src/navigator.spec.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,21 @@ describe('Navigator', function () {
4040
await fileService.createFolder(targetUri);
4141
});
4242

43-
afterEach(async () => {
44-
await fileService.delete(targetUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
43+
afterEach(async function () {
44+
// On Windows, file handles may not be released immediately after move/copy operations,
45+
// causing EBUSY errors on cleanup. Retry with a short delay to handle this.
46+
for (let attempt = 0; attempt < 3; attempt++) {
47+
try {
48+
await fileService.delete(targetUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
49+
return;
50+
} catch (e) {
51+
if (attempt < 2) {
52+
await new Promise(resolve => setTimeout(resolve, 500));
53+
} else {
54+
throw e;
55+
}
56+
}
57+
}
4558
});
4659

4760
/** @type {Array<['copy' | 'move', boolean]>} */

examples/api-tests/src/scm.spec.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
const { timeout } = require('@theia/core/lib/common/promise-util');
18-
1917
// @ts-check
2018
describe('SCM', function () {
2119

2220
const { assert } = chai;
21+
const { timeout } = require('@theia/core/lib/common/promise-util');
2322

2423
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
2524
const Uri = require('@theia/core/lib/common/uri');

examples/playwright/src/tests/theia-notebook-editor.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ test.describe('Theia Notebook Cell interaction', () => {
231231
// second cell is selected after creation
232232
expect(await secondCell.isSelected()).toBe(true);
233233
// select cell above
234-
await editor.page.keyboard.type('second cell');
234+
await editor.page.keyboard.insertText('second cell');
235235
await secondCell.editor.page.keyboard.press('ArrowUp');
236236
expect(await cell.isSelected()).toBe(true);
237237

@@ -306,7 +306,7 @@ test.describe('Theia Notebook Cell interaction', () => {
306306
await cell.selectCell();
307307
await cell.page.keyboard.press('l');
308308
// NOTE: div.line-numbers is not visible
309-
await cell.editor.locator.locator('.overflow-guard > div.line-numbers').waitFor({ state: 'attached' });
309+
await cell.editor.locator.locator('.margin-view-overlays .line-numbers').waitFor({ state: 'attached' });
310310
});
311311

312312
test('Check Collapse output switch `o` works', async () => {

examples/playwright/src/theia-monaco-editor.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
import { ElementHandle, Locator } from '@playwright/test';
17+
import { ElementHandle, Locator, Page } from '@playwright/test';
1818
import { TheiaPageObject } from './theia-page-object';
1919
import { TheiaApp } from './theia-app';
2020

@@ -162,7 +162,29 @@ export class TheiaMonacoEditor extends TheiaPageObject {
162162
async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
163163
const line = await this.line(lineNumber);
164164
await line?.click();
165-
await this.page.keyboard.type(text);
165+
await TheiaMonacoEditor.typeText(this.page, text);
166+
}
167+
168+
/**
169+
* Types text into a focused Monaco editor using `keyboard.insertText()`.
170+
*
171+
* Monaco 1.108+ uses the native EditContext API by default instead of a hidden textarea.
172+
* `keyboard.type()` dispatches individual key events which are not reliably processed by EditContext,
173+
* causing characters to be lost. `keyboard.insertText()` dispatches an `InputEvent` which is handled
174+
* correctly by both the legacy textarea and the native EditContext input mechanisms.
175+
*
176+
* Newlines in the text are handled by pressing Enter between segments.
177+
*/
178+
static async typeText(page: Page, text: string): Promise<void> {
179+
const segments = text.split('\n');
180+
for (let i = 0; i < segments.length; i++) {
181+
if (i > 0) {
182+
await page.keyboard.press('Enter');
183+
}
184+
if (segments[i].length > 0) {
185+
await page.keyboard.insertText(segments[i]);
186+
}
187+
}
166188
}
167189

168190
/**

examples/playwright/src/theia-text-editor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class TheiaTextEditor extends TheiaEditor {
5050
}
5151

5252
protected async typeTextAndHitEnter(text: string): Promise<void> {
53-
await this.page.keyboard.type(text);
53+
await TheiaMonacoEditor.typeText(this.page, text);
5454
await this.page.keyboard.press('Enter');
5555
}
5656

@@ -107,15 +107,15 @@ export class TheiaTextEditor extends TheiaEditor {
107107
await this.placeCursorInLine(existingLine);
108108
await this.page.keyboard.press('End');
109109
await this.page.keyboard.press('Enter');
110-
await this.page.keyboard.type(newText);
110+
await TheiaMonacoEditor.typeText(this.page, newText);
111111
}
112112

113113
async addTextToNewLineAfterLineByLineNumber(lineNumber: number, newText: string): Promise<void> {
114114
const existingLine = await this.monacoEditor.line(lineNumber);
115115
await this.placeCursorInLine(existingLine);
116116
await this.page.keyboard.press('End');
117117
await this.page.keyboard.press('Enter');
118-
await this.page.keyboard.type(newText);
118+
await TheiaMonacoEditor.typeText(this.page, newText);
119119
}
120120

121121
protected async selectLine(lineLocator: Locator | undefined): Promise<void> {

0 commit comments

Comments
 (0)