From 64905d1804de6fc8c8c2c4e84ddc638dbdcdb38a Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 19 Apr 2026 08:51:22 +0900 Subject: [PATCH 1/3] test: add Next.js head change e2e coverage Co-authored-by: WillBooster (Codex CLI) --- e2e/next-app/app/editorProbe.tsx | 51 +------------------ e2e/next-app/fixtures/mockMonaco.ts | 52 +++++++++++++++++++ e2e/next-app/next-env.d.ts | 1 + e2e/next-app/next.e2e.ts | 27 ++++++++++ e2e/next-app/pages/issue272.tsx | 78 +++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 49 deletions(-) create mode 100644 e2e/next-app/fixtures/mockMonaco.ts create mode 100644 e2e/next-app/pages/issue272.tsx diff --git a/e2e/next-app/app/editorProbe.tsx b/e2e/next-app/app/editorProbe.tsx index 3936c86..ae73a25 100644 --- a/e2e/next-app/app/editorProbe.tsx +++ b/e2e/next-app/app/editorProbe.tsx @@ -3,57 +3,10 @@ import { useState } from 'react'; import MonacoEditor, { DiffEditor, loader, useMonaco, type Monaco } from '@willbooster/monaco-react'; +import { mockMonaco } from '../fixtures/mockMonaco'; type LoaderConfig = Parameters[0]; - -const model = { - uri: { path: '/e2e.ts' }, - dispose: () => {}, - getFullModelRange: () => ({}), -}; - -const codeEditor = { - dispose: () => {}, - executeEdits: () => {}, - getModel: () => model, - getOption: () => false, - getValue: () => 'const answer = 42;', - onDidChangeModelContent: () => ({ dispose: () => {} }), - pushUndoStop: () => {}, - restoreViewState: () => {}, - revealLine: () => {}, - saveViewState: () => ({}), - setModel: () => {}, - updateOptions: () => {}, -}; - -const diffEditor = { - dispose: () => {}, - getModel: () => ({ original: model, modified: model }), - getModifiedEditor: () => codeEditor, - getOriginalEditor: () => codeEditor, - setModel: () => {}, - updateOptions: () => {}, -}; - -const monaco = { - editor: { - create: () => codeEditor, - createDiffEditor: () => diffEditor, - createModel: () => model, - EditorOption: { - readOnly: 'readOnly', - }, - getModel: () => {}, - getModelMarkers: () => [], - onDidChangeMarkers: () => ({ dispose: () => {} }), - setModelLanguage: () => {}, - setTheme: () => {}, - }, - Uri: { - parse: (path: string) => ({ path }), - }, -} as unknown as Monaco; +const monaco = mockMonaco; loader.config({ monaco: monaco as LoaderConfig['monaco'] }); diff --git a/e2e/next-app/fixtures/mockMonaco.ts b/e2e/next-app/fixtures/mockMonaco.ts new file mode 100644 index 0000000..09ffb4d --- /dev/null +++ b/e2e/next-app/fixtures/mockMonaco.ts @@ -0,0 +1,52 @@ +import type { Monaco } from '@willbooster/monaco-react'; + +const model = { + uri: { path: '/e2e.ts' }, + dispose: () => {}, + getFullModelRange: () => ({}), + isDisposed: () => false, + setValue: () => {}, +}; + +const codeEditor = { + dispose: () => {}, + executeEdits: () => {}, + getModel: () => model, + getOption: () => false, + getValue: () => 'const answer = 42;', + onDidChangeModelContent: () => ({ dispose: () => {} }), + pushUndoStop: () => {}, + restoreViewState: () => {}, + revealLine: () => {}, + saveViewState: () => ({}), + setModel: () => {}, + updateOptions: () => {}, +}; + +const diffEditor = { + dispose: () => {}, + getModel: () => ({ original: model, modified: model }), + getModifiedEditor: () => codeEditor, + getOriginalEditor: () => codeEditor, + setModel: () => {}, + updateOptions: () => {}, +}; + +export const mockMonaco = { + editor: { + create: () => codeEditor, + createDiffEditor: () => diffEditor, + createModel: () => model, + EditorOption: { + readOnly: 'readOnly', + }, + getModel: () => {}, + getModelMarkers: () => [], + onDidChangeMarkers: () => ({ dispose: () => {} }), + setModelLanguage: () => {}, + setTheme: () => {}, + }, + Uri: { + parse: (path: string) => ({ path }), + }, +} as unknown as Monaco; diff --git a/e2e/next-app/next-env.d.ts b/e2e/next-app/next-env.d.ts index 9edff1c..2d5420e 100644 --- a/e2e/next-app/next-env.d.ts +++ b/e2e/next-app/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited diff --git a/e2e/next-app/next.e2e.ts b/e2e/next-app/next.e2e.ts index 9479e3f..819eb6e 100644 --- a/e2e/next-app/next.e2e.ts +++ b/e2e/next-app/next.e2e.ts @@ -43,3 +43,30 @@ test('loads monaco-react through the Next.js app router', async ({ page }) => { await expect(page.getByTestId('diff-status')).toHaveText('diff-ok'); expect(errors).toEqual([]); }); + +test('keeps Monaco stylesheet after Next.js Head changes', async ({ page }) => { + const errors: string[] = []; + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()); + } + }); + page.on('pageerror', (error) => errors.push(error.message)); + + await page.goto('/issue272'); + + await expect(page.getByTestId('editor-status')).toHaveText('editor-ok'); + await expect(page.getByTestId('stylesheet-count')).toHaveText('1'); + + const remountButton = page.getByRole('button', { name: 'Remount editor' }); + await remountButton.click(); + await expect(page.getByTestId('head-revision')).toHaveText('1'); + await expect(page.getByTestId('editor-status')).toHaveText('editor-ok'); + await expect(page.getByTestId('stylesheet-count')).toHaveText('1'); + + await remountButton.click(); + await expect(page.getByTestId('head-revision')).toHaveText('2'); + await expect(page.getByTestId('editor-status')).toHaveText('editor-ok'); + await expect(page.getByTestId('stylesheet-count')).toHaveText('1'); + expect(errors).toEqual([]); +}); diff --git a/e2e/next-app/pages/issue272.tsx b/e2e/next-app/pages/issue272.tsx new file mode 100644 index 0000000..e5617f4 --- /dev/null +++ b/e2e/next-app/pages/issue272.tsx @@ -0,0 +1,78 @@ +import Head from 'next/head'; +import { useEffect, useState } from 'react'; + +import MonacoEditor, { loader } from '@willbooster/monaco-react'; +import { mockMonaco } from '../fixtures/mockMonaco'; + +type LoaderConfig = Parameters[0]; + +declare global { + var __issue272StylesheetInjected: boolean | undefined; +} + +loader.config({ monaco: mockMonaco as LoaderConfig['monaco'] }); + +export default function Issue272Page() { + const [headRevision, setHeadRevision] = useState(0); + const [isEditorVisible, setIsEditorVisible] = useState(true); + const [editorStatus, setEditorStatus] = useState('editor-pending'); + const [stylesheetCount, setStylesheetCount] = useState(0); + const faviconHref = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Ctext%3E${headRevision}%3C/text%3E%3C/svg%3E`; + + useEffect(() => { + injectMonacoStylesheetOnce(); + setStylesheetCount(countMonacoStylesheets()); + }, [headRevision, isEditorVisible]); + + function remountEditorWithHeadChange() { + setEditorStatus('editor-pending'); + setIsEditorVisible(false); + setHeadRevision((currentRevision) => currentRevision + 1); + requestAnimationFrame(() => setIsEditorVisible(true)); + } + + return ( + <> + + Issue 272 + + +
+ +

{headRevision}

+

{stylesheetCount}

+

{editorStatus}

+ {isEditorVisible && ( + { + setStylesheetCount(countMonacoStylesheets()); + setEditorStatus(monaco === mockMonaco ? 'editor-ok' : 'editor-mismatch'); + }} + /> + )} +
+ + ); +} + +function injectMonacoStylesheetOnce() { + if (globalThis.__issue272StylesheetInjected) return; + if (document.querySelector('link[data-name="vs/editor/editor.main"]')) return; + + const stylesheet = document.createElement('link'); + stylesheet.rel = 'stylesheet'; + stylesheet.type = 'text/css'; + stylesheet.dataset.name = 'vs/editor/editor.main'; + stylesheet.href = 'data:text/css,.issue272-monaco{}'; + document.head.append(stylesheet); + globalThis.__issue272StylesheetInjected = true; +} + +function countMonacoStylesheets() { + return document.querySelectorAll('link[data-name="vs/editor/editor.main"]').length; +} From 07a119d2291a9a7703c3c4af8e902d778f5ce933 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 19 Apr 2026 08:51:58 +0900 Subject: [PATCH 2/3] test: remove unused e2e import Co-authored-by: WillBooster (Codex CLI) --- e2e/next-app/app/editorProbe.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/next-app/app/editorProbe.tsx b/e2e/next-app/app/editorProbe.tsx index ae73a25..a5c1bfc 100644 --- a/e2e/next-app/app/editorProbe.tsx +++ b/e2e/next-app/app/editorProbe.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; -import MonacoEditor, { DiffEditor, loader, useMonaco, type Monaco } from '@willbooster/monaco-react'; +import MonacoEditor, { DiffEditor, loader, useMonaco } from '@willbooster/monaco-react'; import { mockMonaco } from '../fixtures/mockMonaco'; type LoaderConfig = Parameters[0]; From a88ad1f7c4ca4d986b0d800ae8366f2ded74da48 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 19 Apr 2026 08:54:59 +0900 Subject: [PATCH 3/3] test: use real Monaco in Next e2e Co-authored-by: WillBooster (Codex CLI) --- e2e/next-app/app/editorProbe.tsx | 14 +++----- e2e/next-app/fixtures/mockMonaco.ts | 52 ----------------------------- e2e/next-app/pages/issue272.tsx | 29 ++-------------- 3 files changed, 7 insertions(+), 88 deletions(-) delete mode 100644 e2e/next-app/fixtures/mockMonaco.ts diff --git a/e2e/next-app/app/editorProbe.tsx b/e2e/next-app/app/editorProbe.tsx index a5c1bfc..3d9268d 100644 --- a/e2e/next-app/app/editorProbe.tsx +++ b/e2e/next-app/app/editorProbe.tsx @@ -2,13 +2,7 @@ import { useState } from 'react'; -import MonacoEditor, { DiffEditor, loader, useMonaco } from '@willbooster/monaco-react'; -import { mockMonaco } from '../fixtures/mockMonaco'; - -type LoaderConfig = Parameters[0]; -const monaco = mockMonaco; - -loader.config({ monaco: monaco as LoaderConfig['monaco'] }); +import MonacoEditor, { DiffEditor, useMonaco } from '@willbooster/monaco-react'; export default function EditorProbe() { const [editorStatus, setEditorStatus] = useState('editor-pending'); @@ -17,7 +11,7 @@ export default function EditorProbe() { return ( <> -

{loadedMonaco === monaco ? 'hook-ok' : 'hook-pending'}

+

{loadedMonaco?.editor ? 'hook-ok' : 'hook-pending'}

{editorStatus}
{ void editor; - setEditorStatus(mountedMonaco === monaco ? 'editor-ok' : 'editor-mismatch'); + setEditorStatus(mountedMonaco.editor ? 'editor-ok' : 'editor-mismatch'); }} />
{diffStatus}
@@ -36,7 +30,7 @@ export default function EditorProbe() { language="typescript" onMount={(editor, mountedMonaco) => { void editor; - setDiffStatus(mountedMonaco === monaco ? 'diff-ok' : 'diff-mismatch'); + setDiffStatus(mountedMonaco.editor ? 'diff-ok' : 'diff-mismatch'); }} /> diff --git a/e2e/next-app/fixtures/mockMonaco.ts b/e2e/next-app/fixtures/mockMonaco.ts deleted file mode 100644 index 09ffb4d..0000000 --- a/e2e/next-app/fixtures/mockMonaco.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Monaco } from '@willbooster/monaco-react'; - -const model = { - uri: { path: '/e2e.ts' }, - dispose: () => {}, - getFullModelRange: () => ({}), - isDisposed: () => false, - setValue: () => {}, -}; - -const codeEditor = { - dispose: () => {}, - executeEdits: () => {}, - getModel: () => model, - getOption: () => false, - getValue: () => 'const answer = 42;', - onDidChangeModelContent: () => ({ dispose: () => {} }), - pushUndoStop: () => {}, - restoreViewState: () => {}, - revealLine: () => {}, - saveViewState: () => ({}), - setModel: () => {}, - updateOptions: () => {}, -}; - -const diffEditor = { - dispose: () => {}, - getModel: () => ({ original: model, modified: model }), - getModifiedEditor: () => codeEditor, - getOriginalEditor: () => codeEditor, - setModel: () => {}, - updateOptions: () => {}, -}; - -export const mockMonaco = { - editor: { - create: () => codeEditor, - createDiffEditor: () => diffEditor, - createModel: () => model, - EditorOption: { - readOnly: 'readOnly', - }, - getModel: () => {}, - getModelMarkers: () => [], - onDidChangeMarkers: () => ({ dispose: () => {} }), - setModelLanguage: () => {}, - setTheme: () => {}, - }, - Uri: { - parse: (path: string) => ({ path }), - }, -} as unknown as Monaco; diff --git a/e2e/next-app/pages/issue272.tsx b/e2e/next-app/pages/issue272.tsx index e5617f4..7d5626a 100644 --- a/e2e/next-app/pages/issue272.tsx +++ b/e2e/next-app/pages/issue272.tsx @@ -1,16 +1,7 @@ import Head from 'next/head'; import { useEffect, useState } from 'react'; -import MonacoEditor, { loader } from '@willbooster/monaco-react'; -import { mockMonaco } from '../fixtures/mockMonaco'; - -type LoaderConfig = Parameters[0]; - -declare global { - var __issue272StylesheetInjected: boolean | undefined; -} - -loader.config({ monaco: mockMonaco as LoaderConfig['monaco'] }); +import MonacoEditor from '@willbooster/monaco-react'; export default function Issue272Page() { const [headRevision, setHeadRevision] = useState(0); @@ -20,7 +11,6 @@ export default function Issue272Page() { const faviconHref = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Ctext%3E${headRevision}%3C/text%3E%3C/svg%3E`; useEffect(() => { - injectMonacoStylesheetOnce(); setStylesheetCount(countMonacoStylesheets()); }, [headRevision, isEditorVisible]); @@ -51,7 +41,7 @@ export default function Issue272Page() { defaultLanguage="typescript" onMount={(_, monaco) => { setStylesheetCount(countMonacoStylesheets()); - setEditorStatus(monaco === mockMonaco ? 'editor-ok' : 'editor-mismatch'); + setEditorStatus(monaco.editor ? 'editor-ok' : 'editor-mismatch'); }} /> )} @@ -60,19 +50,6 @@ export default function Issue272Page() { ); } -function injectMonacoStylesheetOnce() { - if (globalThis.__issue272StylesheetInjected) return; - if (document.querySelector('link[data-name="vs/editor/editor.main"]')) return; - - const stylesheet = document.createElement('link'); - stylesheet.rel = 'stylesheet'; - stylesheet.type = 'text/css'; - stylesheet.dataset.name = 'vs/editor/editor.main'; - stylesheet.href = 'data:text/css,.issue272-monaco{}'; - document.head.append(stylesheet); - globalThis.__issue272StylesheetInjected = true; -} - function countMonacoStylesheets() { - return document.querySelectorAll('link[data-name="vs/editor/editor.main"]').length; + return document.querySelectorAll('link[rel="stylesheet"][href*="/vs/editor/editor.main.css"]').length; }