From 01424323fe76a9a55ff96a333d2bd5bb118dade3 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 28 Jan 2026 14:37:24 +0100 Subject: [PATCH] feat: support testing light and dark mode --- docs/tool-reference.md | 1 + src/McpContext.ts | 15 +++++ src/McpResponse.ts | 8 +++ src/tools/ToolDefinition.ts | 1 + src/tools/emulation.ts | 23 +++++++ tests/McpResponse.test.js.snapshot | 12 ++++ tests/McpResponse.test.ts | 14 ++++ tests/tools/emulation.test.ts | 105 +++++++++++++++++++++++++++++ 8 files changed, 179 insertions(+) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index ac93d2ab1..13e0a0ff2 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -206,6 +206,7 @@ **Parameters:** +- **colorScheme** (enum: "dark", "light", "auto") _(optional)_: [`Emulate`](#emulate) the dark or the light mode. Set to "auto" to reset to the default. - **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged. - **geolocation** (unknown) _(optional)_: Geolocation to [`emulate`](#emulate). Set to null to clear the geolocation override. - **networkConditions** (enum: "No emulation", "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") _(optional)_: Throttle network. Set to "No emulation" to disable. If omitted, conditions remain unchanged. diff --git a/src/McpContext.ts b/src/McpContext.ts index 28aa3fa25..11c0a089b 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -124,6 +124,7 @@ export class McpContext implements Context { #geolocationMap = new WeakMap(); #viewportMap = new WeakMap(); #userAgentMap = new WeakMap(); + #colorSchemeMap = new WeakMap(); #dialog?: Dialog; #pageIdMap = new WeakMap(); @@ -353,6 +354,20 @@ export class McpContext implements Context { return this.#userAgentMap.get(page) ?? null; } + setColorScheme(scheme: 'dark' | 'light' | null): void { + const page = this.getSelectedPage(); + if (scheme === null) { + this.#colorSchemeMap.delete(page); + } else { + this.#colorSchemeMap.set(page, scheme); + } + } + + getColorScheme(): 'dark' | 'light' | null { + const page = this.getSelectedPage(); + return this.#colorSchemeMap.get(page) ?? null; + } + setIsRunningPerformanceTrace(x: boolean): void { this.#isRunningTrace = x; } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index a00f2887f..f17e2dc0e 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -435,6 +435,7 @@ export class McpResponse implements Response { viewport?: object; userAgent?: string; cpuThrottlingRate?: number; + colorScheme?: string; dialog?: { type: string; message: string; @@ -482,6 +483,13 @@ export class McpResponse implements Response { structuredContent.cpuThrottlingRate = cpuThrottlingRate; } + const colorScheme = context.getColorScheme(); + if (colorScheme) { + response.push(`## Color Scheme emulation`); + response.push(`Emulating: ${colorScheme}`); + structuredContent.colorScheme = colorScheme; + } + const dialog = context.getDialog(); if (dialog) { const defaultValueIfNeeded = diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 1e568c6cc..4657283f3 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -122,6 +122,7 @@ export type Context = Readonly<{ getViewport(): Viewport | null; setUserAgent(userAgent: string | null): void; getUserAgent(): string | null; + setColorScheme(scheme: 'dark' | 'light' | null): void; saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg' | 'image/webp', diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 325e05f05..637664e94 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -63,6 +63,12 @@ export const emulate = defineTool({ .describe( 'User agent to emulate. Set to null to clear the user agent override.', ), + colorScheme: zod + .enum(['dark', 'light', 'auto']) + .optional() + .describe( + 'Emulate the dark or the light mode. Set to "auto" to reset to the default.', + ), viewport: zod .object({ width: zod.number().int().min(0).describe('Page width in pixels.'), @@ -158,6 +164,23 @@ export const emulate = defineTool({ } } + if (request.params.colorScheme) { + if (request.params.colorScheme === 'auto') { + await page.emulateMediaFeatures([ + {name: 'prefers-color-scheme', value: ''}, + ]); + context.setColorScheme(null); + } else { + await page.emulateMediaFeatures([ + { + name: 'prefers-color-scheme', + value: request.params.colorScheme, + }, + ]); + context.setColorScheme(request.params.colorScheme); + } + } + if (viewport !== undefined) { if (viewport === null) { await page.setViewport(null); diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index 1181d9744..e11f94941 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -177,6 +177,18 @@ exports[`McpResponse > adds an alert dialog 2`] = ` } `; +exports[`McpResponse > adds color scheme emulation setting when it is set 1`] = ` +# test response +## Color Scheme emulation +Emulating: dark +`; + +exports[`McpResponse > adds color scheme emulation setting when it is set 2`] = ` +{ + "colorScheme": "dark" +} +`; + exports[`McpResponse > adds console messages when the setting is true 1`] = ` # test response ## Console messages diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index b03e85d18..63b0955b6 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -383,6 +383,20 @@ describe('McpResponse', () => { }); }); + it('adds color scheme emulation setting when it is set', async t => { + await withMcpContext(async (response, context) => { + context.setColorScheme('dark'); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); + t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); + }); + }); + it('adds a prompt dialog', async t => { await withMcpContext(async (response, context) => { const page = context.getSelectedPage(); diff --git a/tests/tools/emulation.test.ts b/tests/tools/emulation.test.ts index 0ad784498..f66ea221e 100644 --- a/tests/tools/emulation.test.ts +++ b/tests/tools/emulation.test.ts @@ -467,4 +467,109 @@ describe('emulation', () => { }); }); }); + + describe('colorScheme', () => { + it('emulates color scheme', async () => { + await withMcpContext(async (response, context) => { + await emulate.handler( + { + params: { + colorScheme: 'dark', + }, + }, + response, + context, + ); + + assert.strictEqual(context.getColorScheme(), 'dark'); + const page = context.getSelectedPage(); + const scheme = await page.evaluate(() => + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light', + ); + assert.strictEqual(scheme, 'dark'); + }); + }); + + it('updates color scheme', async () => { + await withMcpContext(async (response, context) => { + await emulate.handler( + { + params: { + colorScheme: 'dark', + }, + }, + response, + context, + ); + assert.strictEqual(context.getColorScheme(), 'dark'); + + await emulate.handler( + { + params: { + colorScheme: 'light', + }, + }, + response, + context, + ); + assert.strictEqual(context.getColorScheme(), 'light'); + const page = context.getSelectedPage(); + const scheme = await page.evaluate(() => + window.matchMedia('(prefers-color-scheme: light)').matches + ? 'light' + : 'dark', + ); + assert.strictEqual(scheme, 'light'); + }); + }); + + it('resets color scheme when set to auto', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPage(); + + const initial = await page.evaluate( + () => window.matchMedia('(prefers-color-scheme: dark)').matches, + ); + + await emulate.handler( + { + params: { + colorScheme: 'dark', + }, + }, + response, + context, + ); + assert.strictEqual(context.getColorScheme(), 'dark'); + // Check manually that it is dark + + assert.strictEqual( + await page.evaluate( + () => window.matchMedia('(prefers-color-scheme: dark)').matches, + ), + true, + ); + + await emulate.handler( + { + params: { + colorScheme: 'auto', + }, + }, + response, + context, + ); + + assert.strictEqual(context.getColorScheme(), null); + assert.strictEqual( + await page.evaluate( + () => window.matchMedia('(prefers-color-scheme: dark)').matches, + ), + initial, + ); + }); + }); + }); });