fix(studio): stop GraphiQL from corrupting other Monaco editors (#47363)

GraphiQL (`@graphiql/react`) runs a second Monaco instance that injects
two global, page-wide styles which corrupt Studio's other editors once a
GraphiQL chunk has loaded (it persists across client-side navigation, so
a full reload hides it). After visiting GraphiQL and returning to e.g.
the SQL editor, the editor collapses to a ~5px sliver and its syntax
colors swap to GraphiQL's theme.

**Changed:**
- `monaco.css` — a higher-specificity counter-rule
(`.monaco-editor.monaco-editor { position: relative !important }`) beats
GraphiQL's runtime-injected `.monaco-editor { position: absolute
!important }`, which otherwise pulls Studio's `@monaco-editor/react`
wrapper out of flow and collapses it to ~5px.
- GraphiQL now uses the primary `supabase` Monaco theme instead of a
separate `supabase-graphql-*` theme, so the global `.mtk*` token palette
stays identical and syntax colors no longer bleed into other editors.

**Added:**
- E2E test (`monaco-graphiql-coexistence.spec.ts`) reproducing both bugs
via client-side SQL editor → GraphiQL → SQL editor navigation (a full
reload unloads the chunk and hides the bug).
- Component test (`CodeEditor.test.tsx`) guarding the height-class
precedence regression from #47339/#47350 — a caller height (e.g. the
email template editor's `h-96`) must win over the default `h-full`.
Covered as a component test since the email source editor isn't
reachable on self-hosted.

## To test

- Open the SQL editor → **Integrations → GraphiQL** → back to the SQL
editor (in-app navigation, not a reload). It should stay full height and
keep its own syntax colors.
- Confirm autocomplete still works in the SQL editor.
- `pnpm --prefix e2e/studio run e2e --
features/monaco-graphiql-coexistence.spec.ts`
- `pnpm --prefix apps/studio test -- CodeEditor.test`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved Monaco editor styling so GraphiQL no longer affects the SQL
editor’s theme or layout when navigating between them.
* Fixed editor sizing so a custom height now takes precedence over the
default full-height setting.
* Polished GraphiQL panel styling for more consistent spacing and
appearance across themes.

* **New Features**
* GraphiQL now uses the shared editor theme for better visual
consistency with Studio.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-06-29 08:45:09 +00:00
1 parent eb21ae25f7
commit 4fa106e53c
7 files changed
+240 -90

No files matched your search

@@ -0,0 +1,120 @@
import { expect, type Page } from '@playwright/test'
import { env } from '../env.config.js'
import { test } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
/**
* Studio's editors (@monaco-editor/react) and GraphiQL (@graphiql/react) historically ran as
* two separate Monaco instances on one page. Each injects Monaco's global CSS (.monaco-editor
* layout rules + .mtk* theme classes), and they collide: after visiting GraphiQL the SQL
* editor's Monaco wrapper got reflowed to a ~5px slit (covered by the background) and its
* syntax colors swapped to GraphiQL's theme.
*
* IMPORTANT: the bug only reproduces with *client-side* (SPA) navigation. A full `page.goto`
* reload unloads GraphiQL's CSS chunk, which removes the offending injected rule — so these
* tests navigate via the Next router to keep the chunk loaded, exactly like a real user.
*/
test.describe('Monaco / GraphiQL coexistence', () => {
test.skip(env.IS_PLATFORM, 'Self-hosted mode only — GraphiQL + SQL editor on one local project')
// Client-side navigation via the Next router (keeps already-loaded chunks/CSS in place).
const routerPush = async (page: Page, path: string) => {
await page.evaluate((p) => {
const router = (window as unknown as { next?: { router?: { push: (p: string) => void } } })
.next?.router
if (!router) throw new Error('Next router not available for client-side navigation')
router.push(p)
}, path)
}
const firstEditorHeight = async (page: Page) => {
const box = await page.locator('.monaco-editor').first().boundingBox()
return box?.height ?? 0
}
const openGraphiQL = async (page: Page, ref: string) => {
await routerPush(page, `/project/${ref}/integrations/graphiql/graphiql`)
await expect(page.locator('.graphiql-container'), 'GraphiQL container should load').toBeVisible(
{ timeout: 30000 }
)
// GraphiQL mounting its Monaco editor is what injects the global rule that breaks others.
await expect(
page.locator('.graphiql-query-editor .monaco-editor').first(),
'GraphiQL query editor (Monaco) should mount'
).toBeVisible({ timeout: 30000 })
}
test('SQL editor stays full height after visiting GraphiQL', async ({ page, ref }) => {
// Baseline: a freshly-loaded SQL editor fills its pane.
await page.goto(toUrl(`/project/${ref}/sql/new?skip=true`))
await expect(
page.locator('.monaco-editor').first(),
'SQL editor Monaco should render'
).toBeVisible({ timeout: 30000 })
await expect.poll(() => firstEditorHeight(page), { timeout: 15000 }).toBeGreaterThan(100)
// Client-side: visit GraphiQL, then come back to the SQL editor.
await openGraphiQL(page, ref)
await routerPush(page, `/project/${ref}/sql/new?skip=true`)
await expect(page, 'should navigate back to SQL editor').toHaveURL(/\/sql\//, {
timeout: 30000,
})
await page.locator('.monaco-editor').first().waitFor({ state: 'attached', timeout: 30000 })
// With the bug the wrapper collapses to ~5px. It must stay full height.
await expect
.poll(() => firstEditorHeight(page), {
timeout: 15000,
message: 'SQL editor must not collapse to a sliver after visiting GraphiQL',
})
.toBeGreaterThan(100)
})
test('SQL editor keeps its own theme after visiting GraphiQL', async ({ page, ref }) => {
await page.goto(toUrl(`/project/${ref}/sql/new?skip=true`))
await expect(
page.locator('.monaco-editor').first(),
'SQL editor Monaco should render'
).toBeVisible({ timeout: 30000 })
await openGraphiQL(page, ref)
await routerPush(page, `/project/${ref}/sql/new?skip=true`)
await expect(page, 'should navigate back to SQL editor').toHaveURL(/\/sql\//, {
timeout: 30000,
})
// Monaco's token colors are global `.mtk*` classes. GraphiQL's gql-argument color (#6c69ce)
// must never enter that palette, or every other editor gets rethemed.
await expect
.poll(
() =>
page.evaluate(() => {
const gqlColor = 'rgb(108, 105, 206)' // #6c69ce — GraphiQL's argument.identifier.gql
for (const sheet of Array.from(document.styleSheets)) {
let rules: CSSRuleList
try {
rules = sheet.cssRules
} catch {
continue
}
for (const rule of Array.from(rules)) {
const styleRule = rule as CSSStyleRule
if (
/^\.mtk\d+$/.test(styleRule.selectorText || '') &&
styleRule.style?.color === gqlColor
) {
return true
}
}
}
return false
}),
{
timeout: 15000,
message: "GraphiQL's theme must not bleed into the SQL editor's token colors",
}
)
.toBe(false)
})
})