diff --git a/src/server/supplements/recorder/csharp.ts b/src/server/supplements/recorder/csharp.ts index a550a0edc6..1d69c386d2 100644 --- a/src/server/supplements/recorder/csharp.ts +++ b/src/server/supplements/recorder/csharp.ts @@ -18,7 +18,7 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; -import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; export class CSharpLanguageGenerator implements LanguageGenerator { @@ -266,5 +266,5 @@ class CSharpFormatter { } function quote(text: string) { - return `"${text.replace(/["]/g, '\\"')}"`; + return escapeWithQuotes(text, '\"'); } \ No newline at end of file diff --git a/src/server/supplements/recorder/java.ts b/src/server/supplements/recorder/java.ts index 8ba210290e..38eebd4b06 100644 --- a/src/server/supplements/recorder/java.ts +++ b/src/server/supplements/recorder/java.ts @@ -18,7 +18,7 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; -import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; @@ -228,12 +228,6 @@ function formatClickOptions(options: MouseClickOptions, isPage: boolean) { return lines.join('\n'); } -function quote(text: string, char: string = '\"') { - if (char === '\'') - return char + text.replace(/[']/g, '\\\'') + char; - if (char === '"') - return char + text.replace(/["]/g, '\\"') + char; - if (char === '`') - return char + text.replace(/[`]/g, '\\`') + char; - throw new Error('Invalid escape char'); +function quote(text: string) { + return escapeWithQuotes(text, '\"'); } diff --git a/src/server/supplements/recorder/javascript.ts b/src/server/supplements/recorder/javascript.ts index bef65fafa7..1f735bef5b 100644 --- a/src/server/supplements/recorder/javascript.ts +++ b/src/server/supplements/recorder/javascript.ts @@ -18,7 +18,7 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; -import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; export class JavaScriptLanguageGenerator implements LanguageGenerator { @@ -275,12 +275,6 @@ export class JavaScriptFormatter { } } -function quote(text: string, char: string = '\'') { - if (char === '\'') - return char + text.replace(/[']/g, '\\\'') + char; - if (char === '"') - return char + text.replace(/["]/g, '\\"') + char; - if (char === '`') - return char + text.replace(/[`]/g, '\\`') + char; - throw new Error('Invalid escape char'); +function quote(text: string) { + return escapeWithQuotes(text, '\''); } diff --git a/src/server/supplements/recorder/python.ts b/src/server/supplements/recorder/python.ts index 9c8d088178..acf4abedcb 100644 --- a/src/server/supplements/recorder/python.ts +++ b/src/server/supplements/recorder/python.ts @@ -18,7 +18,7 @@ import type { BrowserContextOptions } from '../../../..'; import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language'; import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; -import { MouseClickOptions, toModifiers } from './utils'; +import { escapeWithQuotes, MouseClickOptions, toModifiers } from './utils'; import deviceDescriptors from '../../deviceDescriptors'; export class PythonLanguageGenerator implements LanguageGenerator { @@ -264,12 +264,6 @@ class PythonFormatter { } } -function quote(text: string, char: string = '\"') { - if (char === '\'') - return char + text.replace(/[']/g, '\\\'') + char; - if (char === '"') - return char + text.replace(/["]/g, '\\"') + char; - if (char === '`') - return char + text.replace(/[`]/g, '\\`') + char; - throw new Error('Invalid escape char'); +function quote(text: string) { + return escapeWithQuotes(text, '\"'); } diff --git a/src/server/supplements/recorder/utils.ts b/src/server/supplements/recorder/utils.ts index 283aa3fef5..ec6a57d422 100644 --- a/src/server/supplements/recorder/utils.ts +++ b/src/server/supplements/recorder/utils.ts @@ -56,3 +56,15 @@ export function describeFrame(frame: Frame): { frameName?: string, frameUrl: str return { isMainFrame: false, frameUrl: frame.url(), frameName: frame.name() }; return { isMainFrame: false, frameUrl: frame.url() }; } + +export function escapeWithQuotes(text: string, char: string = '\'') { + const stringified = JSON.stringify(text); + const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"'); + if (char === '\'') + return char + escapedText.replace(/[']/g, '\\\'') + char; + if (char === '"') + return char + escapedText.replace(/["]/g, '\\"') + char; + if (char === '`') + return char + escapedText.replace(/[`]/g, '`') + char; + throw new Error('Invalid escape char'); +} diff --git a/tests/inspector/cli-codegen-2.spec.ts b/tests/inspector/cli-codegen-2.spec.ts index a0e7103ca1..4904e17627 100644 --- a/tests/inspector/cli-codegen-2.spec.ts +++ b/tests/inspector/cli-codegen-2.spec.ts @@ -645,4 +645,40 @@ test.describe('cli codegen', () => { await cli.exited; expect(fs.existsSync(traceFileName)).toBeTruthy(); }); + + test('should fill tricky characters', async ({ page, openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(``); + const selector = await recorder.focusElement('textarea'); + expect(selector).toBe('textarea[name="name"]'); + + const [message, sources] = await Promise.all([ + page.waitForEvent('console', msg => msg.type() !== 'error'), + recorder.waitForOutput('JavaScript', 'fill'), + page.fill('textarea', 'Hello\'\"\`\nWorld') + ]); + + expect(sources.get('JavaScript').text).toContain(` + // Fill textarea[name="name"] + await page.fill('textarea[name="name"]', 'Hello\\'"\`\\nWorld');`); + expect(sources.get('Java').text).toContain(` + // Fill textarea[name="name"] + page.fill("textarea[name=\\\"name\\\"]", "Hello'\\"\`\\nWorld");`); + + expect(sources.get('Python').text).toContain(` + # Fill textarea[name="name"] + page.fill(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\")`); + + expect(sources.get('Python Async').text).toContain(` + # Fill textarea[name="name"] + await page.fill(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\")`); + + expect(sources.get('C#').text).toContain(` + // Fill textarea[name="name"] + await page.FillAsync(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\");`); + + expect(message.text()).toBe('Hello\'\"\`\nWorld'); + }); + });