diff --git a/packages/injected/src/yaml.ts b/packages/injected/src/yaml.ts index e79cf58474069..5e4db22937ab6 100644 --- a/packages/injected/src/yaml.ts +++ b/packages/injected/src/yaml.ts @@ -21,9 +21,12 @@ export function yamlEscapeKeyIfNeeded(str: string): string { } export function yamlEscapeValueIfNeeded(str: string): string { - if (!yamlStringNeedsQuotes(str)) - return str; - return '"' + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => { + // Sanitize lone surrogates (valid in JS, invalid in JSON per RFC 8259 §8.2). + // Same approach as cli/driver.ts for non-JS language bindings. + const sanitized = (str as any).toWellFormed ? (str as any).toWellFormed() : str; + if (!yamlStringNeedsQuotes(sanitized)) + return sanitized; + return '"' + sanitized.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => { switch (c) { case '\\': return '\\\\'; diff --git a/packages/playwright-core/src/utils/isomorphic/yaml.ts b/packages/playwright-core/src/utils/isomorphic/yaml.ts index e79cf58474069..5e4db22937ab6 100644 --- a/packages/playwright-core/src/utils/isomorphic/yaml.ts +++ b/packages/playwright-core/src/utils/isomorphic/yaml.ts @@ -21,9 +21,12 @@ export function yamlEscapeKeyIfNeeded(str: string): string { } export function yamlEscapeValueIfNeeded(str: string): string { - if (!yamlStringNeedsQuotes(str)) - return str; - return '"' + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => { + // Sanitize lone surrogates (valid in JS, invalid in JSON per RFC 8259 §8.2). + // Same approach as cli/driver.ts for non-JS language bindings. + const sanitized = (str as any).toWellFormed ? (str as any).toWellFormed() : str; + if (!yamlStringNeedsQuotes(sanitized)) + return sanitized; + return '"' + sanitized.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => { switch (c) { case '\\': return '\\\\'; diff --git a/tests/mcp/snapshot-unicode.spec.ts b/tests/mcp/snapshot-unicode.spec.ts new file mode 100644 index 0000000000000..fdb738576d7f4 --- /dev/null +++ b/tests/mcp/snapshot-unicode.spec.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed 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. + */ + +import { test, expect } from './fixtures'; + +test('should handle lone high surrogate in snapshot', async ({ client, server }) => { + server.setContent('/', ` +
+ + `, 'text/html'); + + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + expect(response).toHaveResponse({ + snapshot: expect.any(String), + }); + + // Lone surrogates should be replaced with U+FFFD (replacement character) + expect(response.content[0].text).toContain('\uFFFD'); +}); + +test('should handle lone low surrogate in snapshot', async ({ client, server }) => { + server.setContent('/', ` + + + `, 'text/html'); + + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + expect(response).toHaveResponse({ + snapshot: expect.any(String), + }); + + // Lone surrogates should be replaced with U+FFFD + expect(response.content[0].text).toContain('\uFFFD'); +});