You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/05/31 23:48:41 UTC
(texera) branch gh-readonly-queue/main/pr-5241-2ebef323605b156bcaaeb9eb02454797059758a2 created (now e25c28225f)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-5241-2ebef323605b156bcaaeb9eb02454797059758a2
in repository https://gitbox.apache.org/repos/asf/texera.git
at e25c28225f test(frontend): browser-mode spec for code-editor.component (#5241)
This branch includes the following new commits:
new e25c28225f test(frontend): browser-mode spec for code-editor.component (#5241)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(frontend): browser-mode spec for code-editor.component (#5241)
Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5241-2ebef323605b156bcaaeb9eb02454797059758a2
in repository https://gitbox.apache.org/repos/asf/texera.git
commit e25c28225f25990a4c149b85fa48f8090900cf85
Author: Matthew B. <mg...@uci.edu>
AuthorDate: Sun May 31 16:48:20 2026 -0700
test(frontend): browser-mode spec for code-editor.component (#5241)
### What changes were proposed in this PR?
- Adds a browser-mode companion spec,
code-editor.component.browser.spec.ts, that covers the four paths the
jsdom spec cannot reach: the initializeMonacoEditor subscribe body
(readOnly toggle, MonacoBinding wiring, AI-action registration),
initializeDiffEditor's diff config path, the OpenAI gate in
setupAIAssistantActions, the type-annotation action's
getScrolledVisiblePosition branch, and the onWindowResize ->
editor.layout() handler. y-monaco's MonacoBinding is mocked via
vi.hoisted so the constructor records its args without needing a real
Y.Text + monaco TextModel.
- Adds src/browser-buffer-polyfill.ts and wires it as the first
setupFile in vitest.browser.config.ts so monaco-editor-wrapper and
monaco-vscode-files-service-override can be imported under package by
listing it in optimizeDeps.include so Vite's import-scan picks it up
despite the setup-file indirection.
- Broadens the gui:test-browser target's include glob to
**/*.browser.spec.ts in angular.json, and adds the same pattern to
gui:test's exclude list so the two runners do not double-execute.
### Any related issues, documentation, or discussions?
Closes: #5192
### How was this PR tested?
- yarn nx run gui:test-browser: 29/29 passing (7 new code-editor + 22
existing workflow-editor).
- yarn nx run gui:test: 623 passed, 2 skipped, 1 todo (no regression
from the exclude change).
### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.7 in compliance with ASF
---
frontend/angular.json | 8 +-
frontend/package.json | 1 +
.../code-editor.component.browser.spec.ts | 324 +++++++++++++++++++++
frontend/src/browser-buffer-polyfill.ts | 44 +++
frontend/vitest.browser.config.ts | 17 +-
frontend/yarn.lock | 1 +
6 files changed, 392 insertions(+), 3 deletions(-)
diff --git a/frontend/angular.json b/frontend/angular.json
index b9e9961d02..01b2efdd6f 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -94,7 +94,8 @@
"include": ["**/*.spec.ts"],
"setupFiles": ["src/jsdom-svg-polyfill.ts"],
"exclude": [
- "**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts"
+ "**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts",
+ "**/*.browser.spec.ts"
]
}
},
@@ -105,7 +106,10 @@
"runner": "vitest",
"runnerConfig": "vitest.browser.config.ts",
"tsConfig": "src/tsconfig.spec.json",
- "include": ["**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts"]
+ "include": [
+ "**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts",
+ "**/*.browser.spec.ts"
+ ]
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 481e720d97..265b0bbe4f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -122,6 +122,7 @@
"@vitest/browser": "4.1.5",
"@vitest/browser-playwright": "4.1.5",
"@vitest/coverage-v8": "4.1.5",
+ "buffer": "5.7.1",
"concurrently": "7.4.0",
"eslint": "8.57.0",
"eslint-plugin-rxjs": "5.0.3",
diff --git a/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.browser.spec.ts b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.browser.spec.ts
new file mode 100644
index 0000000000..d6a3266fd1
--- /dev/null
+++ b/frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.browser.spec.ts
@@ -0,0 +1,324 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Browser-mode companion to code-editor.component.spec.ts. The sibling jsdom
+// spec covers the constructor, language detection, getFileSuffixByLanguage,
+// onFocus, getCoeditorCursorStyles, and the accept/reject annotation paths,
+// but cannot reach anything gated on a real Monaco editor — the
+// `initAndStart` subscribe body, `initializeDiffEditor`, AI-action run
+// callbacks, `handleTypeAnnotation`'s position branch, and the resize
+// handler. This spec drives those by swapping the component's editorWrapper
+// for a fake-with-real-DOM and running in vitest's Playwright/Chromium
+// browser mode, where monaco-editor's codingame fork can be imported without
+// jsdom's missing-canvas / Node-Buffer-allocation tripwires (the Buffer/process
+// shim is wired as the first setupFile in vitest.browser.config.ts — see
+// src/browser-buffer-polyfill.ts).
+
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { FormControl } from "@angular/forms";
+import { BehaviorSubject, of } from "rxjs";
+import * as Y from "yjs";
+
+import { CodeEditorComponent } from "./code-editor.component";
+import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service";
+import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service";
+import { AIAssistantService } from "../../service/ai-assistant/ai-assistant.service";
+import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service";
+import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service";
+import { mockOperatorMetaData } from "../../service/operator-metadata/mock-operator-metadata.data";
+import { mockJavaUDFPredicate, mockPoint } from "../../service/workflow-graph/model/mock-workflow-data";
+import { OperatorSchema } from "../../types/operator-schema.interface";
+import { commonTestProviders } from "../../../common/testing/test-utils";
+import * as monaco from "monaco-editor";
+
+// y-monaco's MonacoBinding wires real listeners against the YText and a real
+// monaco TextModel. Our fake editor returns a stub model, so the binding's
+// constructor would throw on `model.onDidChangeContent(...)`. The component
+// only depends on the binding's `destroy()` (called in ngOnDestroy) and the
+// fact that the constructor was called with the right shape of args, so a
+// recording stub is sufficient.
+// `vi.mock` is hoisted to the top of the file, so any closure variables it
+// references must be declared inside `vi.hoisted` — a plain top-level `const`
+// is evaluated AFTER the mock factory runs, leaving `monacoBindingCalls`
+// undefined at the moment MonacoBinding's constructor would try to push.
+const { monacoBindingCalls } = vi.hoisted(() => ({
+ monacoBindingCalls: [] as unknown[][],
+}));
+vi.mock("y-monaco", () => ({
+ MonacoBinding: class {
+ constructor(...args: unknown[]) {
+ monacoBindingCalls.push(args);
+ }
+ destroy = vi.fn();
+ },
+}));
+
+// Re-use the augmented stub from the jsdom spec so the component constructor
+// can resolve its highlighted operator regardless of operatorType.
+const baseSchema = mockOperatorMetaData.operators.find(op => op.operatorType === "PythonUDF");
+if (!baseSchema) {
+ throw new Error(
+ "CodeEditorComponent browser spec setup expected a PythonUDF schema in mockOperatorMetaData — fixture has drifted."
+ );
+}
+const synthesizeSchema = (operatorType: string): OperatorSchema => ({ ...baseSchema, operatorType });
+const augmentedSchemas: OperatorSchema[] = [...mockOperatorMetaData.operators, synthesizeSchema("PythonUDFV2")];
+class AugmentedStubMetadataService extends StubOperatorMetadataService {
+ private readonly augmentedMetadata = of({ ...mockOperatorMetaData, operators: augmentedSchemas });
+ override getOperatorMetadata(): typeof this.augmentedMetadata {
+ return this.augmentedMetadata;
+ }
+ override getOperatorSchema(operatorType: string): OperatorSchema {
+ const schema = augmentedSchemas.find(op => op.operatorType === operatorType);
+ if (!schema) throw new Error(`unknown operatorType ${operatorType}`);
+ return schema;
+ }
+ override operatorTypeExists(operatorType: string): boolean {
+ return augmentedSchemas.some(op => op.operatorType === operatorType);
+ }
+}
+
+// A minimal, recording Monaco editor stand-in. Returns realistic values
+// where the component reads from it (`getScrolledVisiblePosition`,
+// `getSelection`, `getModel`) and records what the component asks it to do
+// (`addAction`, `updateOptions`, `layout`). The component does not introspect
+// any of these beyond truthiness, so the stub does not need to be a real
+// IStandaloneCodeEditor — TypeScript's structural check is the only gate.
+interface FakeEditor {
+ addAction: ReturnType<typeof vi.fn>;
+ updateOptions: ReturnType<typeof vi.fn>;
+ layout: ReturnType<typeof vi.fn>;
+ getSelection: ReturnType<typeof vi.fn>;
+ getModel: ReturnType<typeof vi.fn>;
+ getScrolledVisiblePosition: ReturnType<typeof vi.fn>;
+ createDecorationsCollection: ReturnType<typeof vi.fn>;
+ actions: monaco.editor.IActionDescriptor[];
+}
+
+function makeFakeEditor(): FakeEditor {
+ const actions: monaco.editor.IActionDescriptor[] = [];
+ return {
+ actions,
+ addAction: vi.fn((action: monaco.editor.IActionDescriptor) => {
+ actions.push(action);
+ return { dispose: vi.fn(), id: action.id, label: action.label };
+ }),
+ updateOptions: vi.fn(),
+ layout: vi.fn(),
+ getSelection: vi.fn(() => new monaco.Selection(1, 1, 1, 5)),
+ getModel: vi.fn(() => ({
+ getValue: () => "x = 1\ny = 2\n",
+ getValueInRange: () => "x",
+ onDidChangeContent: () => ({ dispose: () => {} }),
+ getOffsetAt: () => 0,
+ })),
+ getScrolledVisiblePosition: vi.fn(() => ({ top: 50, left: 100, height: 18 })),
+ createDecorationsCollection: vi.fn(() => ({ clear: vi.fn() })),
+ };
+}
+
+function makeFakeWrapper(editor: FakeEditor) {
+ return {
+ initAndStart: vi.fn().mockResolvedValue(undefined),
+ getEditor: vi.fn(() => editor),
+ dispose: vi.fn(),
+ };
+}
+
+describe("CodeEditorComponent (browser)", () => {
+ let displayVersionStream$: BehaviorSubject<boolean>;
+ let aiEnabled$: BehaviorSubject<string>;
+ let getTypeAnnotationsSpy: ReturnType<typeof vi.fn>;
+ let locateUnannotatedSpy: ReturnType<typeof vi.fn>;
+ let workflowActionService: WorkflowActionService;
+
+ beforeEach(async () => {
+ monacoBindingCalls.length = 0;
+ displayVersionStream$ = new BehaviorSubject<boolean>(false);
+ aiEnabled$ = new BehaviorSubject<string>("OpenAI");
+ getTypeAnnotationsSpy = vi.fn().mockReturnValue(of({ choices: [{ message: { content: ": int" } }] }));
+ locateUnannotatedSpy = vi.fn().mockReturnValue(of([]));
+
+ await TestBed.configureTestingModule({
+ providers: [
+ WorkflowActionService,
+ { provide: OperatorMetadataService, useClass: AugmentedStubMetadataService },
+ {
+ provide: AIAssistantService,
+ useValue: {
+ checkAIAssistantEnabled: () => aiEnabled$,
+ getTypeAnnotations: getTypeAnnotationsSpy,
+ locateUnannotated: locateUnannotatedSpy,
+ },
+ },
+ {
+ provide: WorkflowVersionService,
+ useValue: {
+ getDisplayParticularVersionStream: () => displayVersionStream$,
+ },
+ },
+ ...commonTestProviders,
+ ],
+ imports: [CodeEditorComponent, HttpClientTestingModule],
+ }).compileComponents();
+
+ workflowActionService = TestBed.inject(WorkflowActionService);
+ });
+
+ // Builds a fixture for the highlighted operator, but defers the
+ // detectChanges/ngAfterViewInit step so the caller can swap in the fake
+ // editor wrapper and stage `code` / `formControl` before the subscribe body
+ // fires. Returns the fixture, the fake wrapper, and the fake editor.
+ function makeFixtureWithFakes() {
+ const predicate = { ...mockJavaUDFPredicate };
+ workflowActionService.addOperator(predicate, mockPoint);
+ workflowActionService.getJointGraphWrapper().highlightOperators(predicate.operatorID);
+
+ const fixture = TestBed.createComponent(CodeEditorComponent);
+ const editor = makeFakeEditor();
+ const wrapper = makeFakeWrapper(editor);
+ const c = fixture.componentInstance as any;
+ c.editorWrapper = wrapper;
+ c.formControl = new FormControl({ value: "", disabled: false });
+ // A YText must live inside a Y.Doc to be useful; the binding stub doesn't
+ // care, but we stage it as if it came from the shared model so the
+ // subscribe body crosses the `if (!this.code) return;` gate.
+ c.code = new Y.Doc().getText("code");
+ return { fixture, wrapper, editor, c: c as CodeEditorComponent };
+ }
+
+ // The component's subscribe path runs `from(initAndStart(...)).pipe(...)`,
+ // which is microtask-async. One macrotask flush after detectChanges is
+ // enough for the RxJS chain to deliver the editor into the subscribe body.
+ async function flush(): Promise<void> {
+ await Promise.resolve();
+ await new Promise(r => setTimeout(r, 0));
+ }
+
+ it("initializeMonacoEditor: wires the editor + MonacoBinding + AI actions when code exists", async () => {
+ const { fixture, wrapper, editor } = makeFixtureWithFakes();
+
+ fixture.detectChanges();
+ await flush();
+
+ // The non-diff config branch should fire with the editor host element.
+ expect(wrapper.initAndStart).toHaveBeenCalledOnce();
+ const [userConfig, host] = wrapper.initAndStart.mock.calls[0];
+ expect((userConfig as any).wrapperConfig.editorAppConfig.useDiffEditor).toBeUndefined();
+ expect((userConfig as any).wrapperConfig.editorAppConfig.codeResources.main.uri).toMatch(/^in-memory-.*\.\.java$/);
+ expect(host).toBeInstanceOf(HTMLElement);
+
+ // The subscribe body should: push readOnly via updateOptions, construct
+ // MonacoBinding, and register the two AI actions.
+ expect(editor.updateOptions).toHaveBeenCalledWith({ readOnly: false });
+ expect(monacoBindingCalls.length).toBe(1);
+ expect(editor.addAction).toHaveBeenCalledTimes(2);
+ });
+
+ it("initializeMonacoEditor: respects formControl.disabled when toggling readOnly", async () => {
+ const { fixture, editor, c } = makeFixtureWithFakes();
+ (c as any).formControl = new FormControl({ value: "", disabled: true });
+
+ fixture.detectChanges();
+ await flush();
+
+ expect(editor.updateOptions).toHaveBeenCalledWith({ readOnly: true });
+ });
+
+ it("initializeDiffEditor: when displayParticularVersion is true, runs the diff config path", async () => {
+ const { fixture, wrapper } = makeFixtureWithFakes();
+ // Seed the stream BEFORE detectChanges so the subscribe in ngAfterViewInit
+ // picks `true` on first emission and takes the diff branch.
+ displayVersionStream$.next(true);
+
+ fixture.detectChanges();
+ await flush();
+
+ expect(wrapper.initAndStart).toHaveBeenCalledOnce();
+ const [userConfig] = wrapper.initAndStart.mock.calls[0];
+ expect((userConfig as any).wrapperConfig.editorAppConfig.useDiffEditor).toBe(true);
+ // `original` is the previous-version source, only set on the diff path.
+ expect((userConfig as any).wrapperConfig.editorAppConfig.codeResources.original).toBeDefined();
+ });
+
+ it("setupAIAssistantActions: registers only the 'all' action when the gate is not OpenAI", async () => {
+ aiEnabled$.next("none");
+ const { fixture, editor } = makeFixtureWithFakes();
+
+ fixture.detectChanges();
+ await flush();
+
+ expect(editor.addAction).toHaveBeenCalledTimes(1);
+ expect(editor.addAction.mock.calls[0][0].id).toBe("all-type-annotation-action");
+ });
+
+ it("type-annotation-action: invoking the run callback populates suggestion state from position", async () => {
+ const { fixture, editor } = makeFixtureWithFakes();
+ fixture.detectChanges();
+ await flush();
+
+ // The first registered action is "type-annotation-action" (the OpenAI
+ // gate fires). Pull its `run` callback and invoke it directly — the
+ // alternative is dispatching via the Monaco command palette, which our
+ // fake editor doesn't implement.
+ const typeAction = editor.actions.find(a => a.id === "type-annotation-action");
+ expect(typeAction).toBeDefined();
+ typeAction!.run(editor as unknown as monaco.editor.ICodeEditor);
+
+ await flush();
+
+ expect(getTypeAnnotationsSpy).toHaveBeenCalledOnce();
+ // getScrolledVisiblePosition returns {top: 50, left: 100}; the component
+ // adds +100 on each axis when staging the suggestion.
+ expect(fixture.componentInstance.suggestionTop).toBe(150);
+ expect(fixture.componentInstance.suggestionLeft).toBe(200);
+ expect(fixture.componentInstance.showAnnotationSuggestion).toBe(true);
+ expect(fixture.componentInstance.currentSuggestion).toBe(": int");
+ });
+
+ it("all-type-annotation-action: locateUnannotated returns empty list, action no-ops cleanly", async () => {
+ const { fixture, editor } = makeFixtureWithFakes();
+ fixture.detectChanges();
+ await flush();
+
+ const allAction = editor.actions.find(a => a.id === "all-type-annotation-action");
+ expect(allAction).toBeDefined();
+ allAction!.run(editor as unknown as monaco.editor.ICodeEditor);
+
+ await flush();
+
+ expect(locateUnannotatedSpy).toHaveBeenCalledOnce();
+ // Empty unannotated list -> processNextVariable never invoked; the
+ // multi-variable state stays in its initial shape.
+ expect((fixture.componentInstance as any).isMultipleVariables).toBe(false);
+ });
+
+ it("onWindowResize: calls editor.layout() through adjustEditorSize", async () => {
+ const { fixture, editor } = makeFixtureWithFakes();
+ fixture.detectChanges();
+ await flush();
+
+ // Reset layout's call count so we don't pick up Monaco's own init layout.
+ editor.layout.mockClear();
+ fixture.componentInstance.onWindowResize();
+
+ expect(editor.layout).toHaveBeenCalledOnce();
+ });
+});
diff --git a/frontend/src/browser-buffer-polyfill.ts b/frontend/src/browser-buffer-polyfill.ts
new file mode 100644
index 0000000000..981615df24
--- /dev/null
+++ b/frontend/src/browser-buffer-polyfill.ts
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// monaco-editor-wrapper + monaco-vscode-files-service-override dereference
+// Node's Buffer (including Buffer.allocUnsafe) at module-evaluation time.
+// Under jsdom-mode Vitest, the test runs on Node and Buffer is built-in. In
+// browser-mode (Playwright/Chromium), Buffer doesn't exist, so importing
+// monaco-editor-wrapper crashes at load. The `buffer` npm package is the
+// browser polyfill of Node's API; expose its `Buffer` (and a minimal
+// `process` shim) on the global so monaco's internals find them at the same
+// names they'd find Node's. Has to run before any monaco import is
+// evaluated, which is why this is wired into vitest.browser.config.ts as the
+// FIRST setupFile — setupFiles run in order, before any test module loads.
+//
+// We use a namespace import (rather than a named or default import) because
+// Vite's dep-optimizer rewrites `buffer@5`'s CJS exports in a way that
+// exposes neither a `Buffer` named export nor a `default` export at
+// module-eval time. The namespace object always has the optimizer-injected
+// shape, so we read `Buffer` off it dynamically.
+import * as bufferModule from "buffer";
+
+const { Buffer } = bufferModule as unknown as { Buffer: typeof globalThis.Buffer };
+
+(globalThis as unknown as { Buffer: typeof Buffer }).Buffer = Buffer;
+(globalThis as unknown as { global: typeof globalThis }).global = globalThis;
+if (!(globalThis as unknown as { process?: { env: Record<string, string> } }).process) {
+ (globalThis as unknown as { process: { env: Record<string, string> } }).process = { env: {} };
+}
diff --git a/frontend/vitest.browser.config.ts b/frontend/vitest.browser.config.ts
index eee0624ff1..eb609d1cda 100644
--- a/frontend/vitest.browser.config.ts
+++ b/frontend/vitest.browser.config.ts
@@ -41,6 +41,15 @@ export default defineConfig({
{ find: /^lib0\/webcrypto$/, replacement: lib0Webcrypto },
],
},
+ // `buffer` is imported only by the test setup file (src/browser-buffer-
+ // polyfill.ts), which Vite's import-scan via the @angular/build:unit-test
+ // builder does NOT crawl — without this hint the dep is served raw and
+ // `require()` calls inside the CJS package crash on first import.
+ // Explicit-include forces esbuild to pre-bundle it (alongside its
+ // `base64-js` + `ieee754` transitive deps) into a browser-runnable ESM.
+ optimizeDeps: {
+ include: ["buffer"],
+ },
test: {
// Emit a JUnit-XML report alongside the default console reporter so
// Codecov Test Analytics can ingest browser-mode failures and detect
@@ -48,7 +57,13 @@ export default defineConfig({
// can disambiguate it from the unit-test report.
reporters: ["default", ["junit", { outputFile: "junit-browser.xml" }]],
globals: true,
- setupFiles: ["src/test-zone-setup.ts"],
+ // browser-buffer-polyfill must run FIRST: it puts Buffer/process on
+ // globalThis before any test module loads, which is required for
+ // monaco-editor-wrapper's eager Buffer.allocUnsafe references (under
+ // jsdom-mode Vitest these are satisfied by Node's built-in Buffer; in
+ // browser-mode the runtime has neither, so we install the `buffer` npm
+ // package as a shim).
+ setupFiles: ["src/browser-buffer-polyfill.ts", "src/test-zone-setup.ts"],
browser: {
enabled: true,
provider: playwright(),
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index b929da0c62..70c89eb2ef 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -11094,6 +11094,7 @@ __metadata:
"@vitest/coverage-v8": "npm:4.1.5"
ai: "npm:5.0.93"
ajv: "npm:8.10.0"
+ buffer: "npm:5.7.1"
concaveman: "npm:2.0.0"
concurrently: "npm:7.4.0"
d3-shape: "npm:2.1.0"