diff --git a/packages/trace-viewer/src/ui/codegen.ts b/packages/trace-viewer/src/ui/codegen.ts index 17492d2087..8fe2d78591 100644 --- a/packages/trace-viewer/src/ui/codegen.ts +++ b/packages/trace-viewer/src/ui/codegen.ts @@ -165,10 +165,95 @@ class PythonCodeGen implements APIRequestCodegen { } } +class CSharpCodeGen implements APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string { + const url = new URL(request.url); + const urlParam = `${url.origin}${url.pathname}`; + const options: any = {}; + + const initLines: string[] = []; + + let method = request.method.toLowerCase(); + if (!['delete', 'get', 'head', 'post', 'put', 'patch'].includes(method)) { + options.Method = method; + method = 'fetch'; + } + + if (url.searchParams.size) + options.Params = Object.fromEntries(url.searchParams.entries()); + if (body) + options.Data = body; + if (request.headers.length) + options.Headers = Object.fromEntries(request.headers.map(header => [header.name, header.value])); + + const params = [`"${urlParam}"`]; + const hasOptions = Object.keys(options).length > 0; + if (hasOptions) + params.push(this.prettyPrintObject(options)); + + return `${initLines.join('\n')}${initLines.length ? '\n' : ''}await request.${this.toFunctionName(method)}(${params.join(', ')});`; + } + + private toFunctionName(method: string): string { + return method[0].toUpperCase() + method.slice(1) + 'Async'; + } + + private indent(v: string, level: number): string { + return v.split('\n').map(s => ' '.repeat(level) + s).join('\n'); + } + + private prettyPrintObject(obj: any, indent = 2, level = 0): string { + // Handle null and undefined + if (obj === null) + return 'null'; + if (obj === undefined) + return 'null'; + + // Handle primitive types + if (typeof obj !== 'object') { + if (typeof obj === 'string') + return `"${obj.replace(/"/g, '\\"')}"`; + if (typeof obj === 'boolean') + return obj ? 'true' : 'false'; + return String(obj); + } + + // Handle arrays + if (Array.isArray(obj)) { + if (obj.length === 0) + return 'new object[] {}'; + const spaces = ' '.repeat(level * indent); + const nextSpaces = ' '.repeat((level + 1) * indent); + + const items = obj.map(item => + `${nextSpaces}${this.prettyPrintObject(item, indent, level + 1)}` + ).join(',\n'); + + return `new object[] {\n${items}\n${spaces}}`; + } + + // Handle regular objects + if (Object.keys(obj).length === 0) + return 'new {}'; + const spaces = ' '.repeat(level * indent); + const nextSpaces = ' '.repeat((level + 1) * indent); + + const entries = Object.entries(obj).map(([key, value]) => { + const formattedValue = this.prettyPrintObject(value, indent, level + 1); + const formattedKey = level === 0 ? key : `["${key}"]`; + return `${nextSpaces}${formattedKey} = ${formattedValue}`; + }).join(',\n'); + + return `new() {\n${entries}\n${spaces}}`; + } +} + export function getAPIRequestCodeGen(language: Language): APIRequestCodegen { if (language === 'javascript') return new JSCodeGen(); if (language === 'python') return new PythonCodeGen(); + if (language === 'csharp') + return new CSharpCodeGen(); throw new Error('Unsupported language: ' + language); } diff --git a/tests/library/unit/codegen.spec.ts b/tests/library/unit/codegen.spec.ts index 6680de8796..32bc7db7ef 100644 --- a/tests/library/unit/codegen.spec.ts +++ b/tests/library/unit/codegen.spec.ts @@ -395,3 +395,88 @@ await page.request.get( }); }); + +test.describe('csharp', () => { + const impl = getAPIRequestCodeGen('csharp'); + + test('generatePlaywrightRequestCall', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo?bar=baz', + method: 'GET', + headers: [{ name: 'User-Agent', value: 'Mozilla/5.0' }, { name: 'Date', value: '2021-01-01' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, 'foo')).toEqual(` +await request.GetAsync("http://example.com/foo", new() { + Params = new() { + ["bar"] = "baz" + }, + Data = "foo", + Headers = new() { + ["User-Agent"] = "Mozilla/5.0", + ["Date"] = "2021-01-01" + } +});`.trim()); + + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo?bar=baz', + method: 'OPTIONS', + headers: [], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await request.FetchAsync("http://example.com/foo", new() { + Method = "options", + Params = new() { + ["bar"] = "baz" + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with POST method and no body', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'POST', + headers: [{ name: 'Content-Type', value: 'application/json' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await request.PostAsync("http://example.com/foo", new() { + Headers = new() { + ["Content-Type"] = "application/json" + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with PUT method and JSON body', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'PUT', + headers: [{ name: 'Content-Type', value: 'application/json' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, '{"key":"value"}')).toEqual(` +await request.PutAsync("http://example.com/foo", new() { + Data = "{\\"key\\":\\"value\\"}", + Headers = new() { + ["Content-Type"] = "application/json" + } +});`.trim()); + }); +});