From 1d3605d1fcc0f8a9e19182ab64d14564454fe6b2 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 20 Nov 2024 10:16:43 +0100 Subject: [PATCH] feat(trace viewer): add "Copy as Playwright Request" button (#33298) --- packages/trace-viewer/src/ui/codegen.ts | 302 ++++++++ .../src/ui/networkResourceDetails.tsx | 11 +- packages/trace-viewer/src/ui/networkTab.tsx | 6 +- .../src/ui/recorder/recorderView.tsx | 2 +- packages/trace-viewer/src/ui/workbench.tsx | 2 +- tests/library/unit/codegen.spec.ts | 675 ++++++++++++++++++ 6 files changed, 991 insertions(+), 7 deletions(-) create mode 100644 packages/trace-viewer/src/ui/codegen.ts create mode 100644 tests/library/unit/codegen.spec.ts diff --git a/packages/trace-viewer/src/ui/codegen.ts b/packages/trace-viewer/src/ui/codegen.ts new file mode 100644 index 0000000000..2d77177a0a --- /dev/null +++ b/packages/trace-viewer/src/ui/codegen.ts @@ -0,0 +1,302 @@ +/** + * 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 type { Language } from '@isomorphic/locatorGenerators'; +import type * as har from '@trace/har'; + +interface APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string; +} + +class JSCodeGen implements APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string { + let method = request.method.toLowerCase(); + const url = new URL(request.url); + const urlParam = `${url.origin}${url.pathname}`; + const options: any = {}; + 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 `await page.request.${method}(${params.join(', ')});`; + } + + private prettyPrintObject(obj: any, indent = 2, level = 0): string { + // Handle null and undefined + if (obj === null) + return 'null'; + if (obj === undefined) + return 'undefined'; + + // Handle primitive types + if (typeof obj !== 'object') { + if (typeof obj === 'string') + return this.stringLiteral(obj); + return String(obj); + } + + // Handle arrays + if (Array.isArray(obj)) { + if (obj.length === 0) + return '[]'; + 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 `[\n${items}\n${spaces}]`; + } + + // Handle regular objects + if (Object.keys(obj).length === 0) + return '{}'; + 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); + // Handle keys that need quotes + const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? + key : + this.stringLiteral(key); + return `${nextSpaces}${formattedKey}: ${formattedValue}`; + }).join(',\n'); + + return `{\n${entries}\n${spaces}}`; + } + + private stringLiteral(v: string): string { + v = v.replace(/\\/g, '\\\\').replace(/'/g, '\\\''); + if (v.includes('\n') || v.includes('\r') || v.includes('\t')) + return '`' + v + '`'; + return `'${v}'`; + } +} + +class PythonCodeGen implements APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string { + const url = new URL(request.url); + const urlParam = `${url.origin}${url.pathname}`; + const params: string[] = [`"${urlParam}"`]; + + + let method = request.method.toLowerCase(); + if (!['delete', 'get', 'head', 'post', 'put', 'patch'].includes(method)) { + params.push(`method="${method}"`); + method = 'fetch'; + } + + if (url.searchParams.size) + params.push(`params=${this.prettyPrintObject(Object.fromEntries(url.searchParams.entries()))}`); + if (body) + params.push(`data=${this.prettyPrintObject(body)}`); + if (request.headers.length) + params.push(`headers=${this.prettyPrintObject(Object.fromEntries(request.headers.map(header => [header.name, header.value])))}`); + + const paramsString = params.length === 1 ? params[0] : `\n${params.map(p => this.indent(p, 2)).join(',\n')}\n`; + return `await page.request.${method}(${paramsString})`; + } + + 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 'None'; + if (obj === undefined) + return 'None'; + + // Handle primitive types + if (typeof obj !== 'object') { + if (typeof obj === 'string') + return this.stringLiteral(obj); + if (typeof obj === 'boolean') + return obj ? 'True' : 'False'; + return String(obj); + } + + // Handle arrays + if (Array.isArray(obj)) { + if (obj.length === 0) + return '[]'; + 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 `[\n${items}\n${spaces}]`; + } + + // Handle regular objects + if (Object.keys(obj).length === 0) + return '{}'; + 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); + return `${nextSpaces}${this.stringLiteral(key)}: ${formattedValue}`; + }).join(',\n'); + + return `{\n${entries}\n${spaces}}`; + } + + private stringLiteral(v: string): string { + return JSON.stringify(v); + } +} + +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 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 this.stringLiteral(obj); + 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 : `[${this.stringLiteral(key)}]`; + return `${nextSpaces}${formattedKey} = ${formattedValue}`; + }).join(',\n'); + + return `new() {\n${entries}\n${spaces}}`; + } + + private stringLiteral(v: string): string { + return JSON.stringify(v); + } +} + +class JavaCodeGen implements APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string { + const url = new URL(request.url); + const params = [`"${url.origin}${url.pathname}"`]; + + const options: string[] = []; + + let method = request.method.toLowerCase(); + if (!['delete', 'get', 'head', 'post', 'put', 'patch'].includes(method)) { + options.push(`setMethod("${method}")`); + method = 'fetch'; + } + + for (const [key, value] of url.searchParams) + options.push(`setQueryParam(${this.stringLiteral(key)}, ${this.stringLiteral(value)})`); + if (body) + options.push(`setData(${this.stringLiteral(body)})`); + for (const header of request.headers) + options.push(`setHeader(${this.stringLiteral(header.name)}, ${this.stringLiteral(header.value)})`); + + if (options.length > 0) + params.push(`RequestOptions.create()\n .${options.join('\n .')}\n`); + return `request.${method}(${params.join(', ')});`; + } + + private stringLiteral(v: string): string { + return JSON.stringify(v); + } +} + +export function getAPIRequestCodeGen(language: Language): APIRequestCodegen { + if (language === 'javascript') + return new JSCodeGen(); + if (language === 'python') + return new PythonCodeGen(); + if (language === 'csharp') + return new CSharpCodeGen(); + if (language === 'java') + return new JavaCodeGen(); + throw new Error('Unsupported language: ' + language); +} diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 7fe5c7f732..9805d42c6f 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -22,11 +22,14 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { ToolbarButton } from '@web/components/toolbarButton'; import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'; import { CopyToClipboardTextButton } from './copyToClipboard'; +import { getAPIRequestCodeGen } from './codegen'; +import type { Language } from '@isomorphic/locatorGenerators'; export const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot; onClose: () => void; -}> = ({ resource, onClose }) => { + sdkLanguage: Language; +}> = ({ resource, onClose, sdkLanguage }) => { const [selectedTab, setSelectedTab] = React.useState('request'); return , + render: () => , }, { id: 'response', @@ -55,7 +58,8 @@ export const NetworkResourceDetails: React.FunctionComponent<{ const RequestTab: React.FunctionComponent<{ resource: ResourceSnapshot; -}> = ({ resource }) => { + sdkLanguage: Language; +}> = ({ resource, sdkLanguage }) => { const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null); React.useEffect(() => { @@ -96,6 +100,7 @@ const RequestTab: React.FunctionComponent<{
generateCurlCommand(resource)} /> generateFetchCall(resource)} /> + getAPIRequestCodeGen(sdkLanguage).generatePlaywrightRequestCall(resource.request, requestBody?.text)} />
{requestBody &&
Request Body
} diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index ec9156d9e6..56cf9325b4 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -26,6 +26,7 @@ import { GridView, type RenderedGridCell } from '@web/components/gridView'; import { SplitView } from '@web/components/splitView'; import type { ContextEntry } from '../types/entries'; import { NetworkFilters, defaultFilterState, type FilterState, type ResourceType } from './networkFilters'; +import type { Language } from '@isomorphic/locatorGenerators'; type NetworkTabModel = { resources: Entry[], @@ -66,7 +67,8 @@ export const NetworkTab: React.FunctionComponent<{ boundaries: Boundaries, networkModel: NetworkTabModel, onEntryHovered?: (entry: Entry | undefined) => void, -}> = ({ boundaries, networkModel, onEntryHovered }) => { + sdkLanguage: Language, +}> = ({ boundaries, networkModel, onEntryHovered, sdkLanguage }) => { const [sorting, setSorting] = React.useState(undefined); const [selectedEntry, setSelectedEntry] = React.useState(undefined); const [filterState, setFilterState] = React.useState(defaultFilterState); @@ -115,7 +117,7 @@ export const NetworkTab: React.FunctionComponent<{ sidebarIsFirst={true} orientation='horizontal' settingName='networkResourceDetails' - main={ setSelectedEntry(undefined)} />} + main={ setSelectedEntry(undefined)} sdkLanguage={sdkLanguage} />} sidebar={grid} />} ; diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.tsx b/packages/trace-viewer/src/ui/recorder/recorderView.tsx index 6ff6b665d3..e9014a6cea 100644 --- a/packages/trace-viewer/src/ui/recorder/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorder/recorderView.tsx @@ -238,7 +238,7 @@ const PropertiesView: React.FunctionComponent<{ id: 'network', title: 'Network', count: networkModel.resources.length, - render: () => + render: () => }; const tabs: TabbedPaneTabModel[] = [ diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 6d397bf411..ad8a099ea4 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -231,7 +231,7 @@ export const Workbench: React.FunctionComponent<{ id: 'network', title: 'Network', count: networkModel.resources.length, - render: () => + render: () => }; const attachmentsTab: TabbedPaneTabModel = { id: 'attachments', diff --git a/tests/library/unit/codegen.spec.ts b/tests/library/unit/codegen.spec.ts new file mode 100644 index 0000000000..a4538747bd --- /dev/null +++ b/tests/library/unit/codegen.spec.ts @@ -0,0 +1,675 @@ +/** + * 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 '@playwright/test'; +import { getAPIRequestCodeGen } from '../../../packages/trace-viewer/src/ui/codegen'; + +test.describe('javascript', () => { + const impl = getAPIRequestCodeGen('javascript'); + + 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 page.request.get('http://example.com/foo', { + params: { + bar: 'baz' + }, + data: 'foo', + headers: { + '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 page.request.fetch('http://example.com/foo', { + method: 'options', + params: { + 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 page.request.post('http://example.com/foo', { + headers: { + '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 page.request.put('http://example.com/foo', { + data: '{"key":"value"}', + headers: { + 'Content-Type': 'application/json' + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with PATCH method and form data', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'PATCH', + headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, 'key=value')).toEqual(` +await page.request.patch('http://example.com/foo', { + data: 'key=value', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with DELETE method and custom header', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'DELETE', + headers: [{ name: 'Authorization', value: 'Bearer token' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.delete('http://example.com/foo', { + headers: { + Authorization: 'Bearer token' + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with HEAD method', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'HEAD', + headers: [], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.head('http://example.com/foo');`.trim()); + }); + + test('generatePlaywrightRequestCall with complex query parameters', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo?bar=baz&qux=quux', + method: 'GET', + headers: [], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get('http://example.com/foo', { + params: { + bar: 'baz', + qux: 'quux' + } +});`.trim()); + }); + + test('generatePlaywrightRequestCall with multiple headers', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'User-Agent', value: 'Mozilla/5.0' }, + { name: 'Accept', value: 'application/json' }, + { name: 'Authorization', value: 'Bearer token' } + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get('http://example.com/foo', { + headers: { + 'User-Agent': 'Mozilla/5.0', + Accept: 'application/json', + Authorization: 'Bearer token' + } +});`.trim()); + }); + + test('escape sequences', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'F\\o', value: 'B\\r' }, + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get('http://example.com/foo', { + headers: { + 'F\\\\o': 'B\\\\r' + } +});`.trim()); + }); + +}); + +test.describe('python', () => { + const impl = getAPIRequestCodeGen('python'); + + 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 page.request.get( + "http://example.com/foo", + params={ + "bar": "baz" + }, + data="foo", + headers={ + "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 page.request.fetch( + "http://example.com/foo", + method="options", + params={ + "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 page.request.post( + "http://example.com/foo", + headers={ + "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 page.request.put( + "http://example.com/foo", + data="{\\"key\\":\\"value\\"}", + headers={ + "Content-Type": "application/json" + } +)`.trim()); + }); + + test('generatePlaywrightRequestCall with PATCH method and form data', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'PATCH', + headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, 'key=value')).toEqual(` +await page.request.patch( + "http://example.com/foo", + data="key=value", + headers={ + "Content-Type": "application/x-www-form-urlencoded" + } +)`.trim()); + }); + + test('generatePlaywrightRequestCall with DELETE method and custom header', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'DELETE', + headers: [{ name: 'Authorization', value: 'Bearer token' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.delete( + "http://example.com/foo", + headers={ + "Authorization": "Bearer token" + } +)`.trim()); + }); + + test('generatePlaywrightRequestCall with HEAD method', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'HEAD', + headers: [], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.head("http://example.com/foo")`.trim()); + }); + + test('generatePlaywrightRequestCall with complex query parameters', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo?bar=baz&qux=quux', + method: 'GET', + headers: [], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get( + "http://example.com/foo", + params={ + "bar": "baz", + "qux": "quux" + } +)`.trim()); + }); + + test('generatePlaywrightRequestCall with multiple headers', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'User-Agent', value: 'Mozilla/5.0' }, + { name: 'Accept', value: 'application/json' }, + { name: 'Authorization', value: 'Bearer token' } + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get( + "http://example.com/foo", + headers={ + "User-Agent": "Mozilla/5.0", + "Accept": "application/json", + "Authorization": "Bearer token" + } +)`.trim()); + }); + + test('escape sequences', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'F\\o', value: 'B\\r' }, + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await page.request.get( + "http://example.com/foo", + headers={ + "F\\\\o": "B\\\\r" + } +)`.trim()); + }); + +}); + +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()); + }); + + test('escape sequences', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'F\\o', value: 'B\\r' }, + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +await request.GetAsync("http://example.com/foo", new() { + Headers = new() { + ["F\\\\o"] = "B\\\\r" + } +});`.trim()); + }); +}); + +test.describe('java', () => { + const impl = getAPIRequestCodeGen('java'); + + 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(` +request.get("http://example.com/foo", RequestOptions.create() + .setQueryParam("bar", "baz") + .setData("foo") + .setHeader("User-Agent", "Mozilla/5.0") + .setHeader("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(` +request.fetch("http://example.com/foo", RequestOptions.create() + .setMethod("options") + .setQueryParam("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(` +request.post("http://example.com/foo", RequestOptions.create() + .setHeader("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(` +request.put("http://example.com/foo", RequestOptions.create() + .setData("{\\"key\\":\\"value\\"}") + .setHeader("Content-Type", "application/json") +);`.trim()); + }); + + test('generatePlaywrightRequestCall with PATCH method and form data', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'PATCH', + headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, 'key=value')).toEqual(` +request.patch("http://example.com/foo", RequestOptions.create() + .setData("key=value") + .setHeader("Content-Type", "application/x-www-form-urlencoded") +);`.trim()); + }); + + test('generatePlaywrightRequestCall with DELETE method and custom header', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'DELETE', + headers: [{ name: 'Authorization', value: 'Bearer token' }], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +request.delete("http://example.com/foo", RequestOptions.create() + .setHeader("Authorization", "Bearer token") +);`.trim()); + }); + + test('escape sequences', () => { + expect(impl.generatePlaywrightRequestCall({ + url: 'http://example.com/foo', + method: 'GET', + headers: [ + { name: 'F\\o', value: 'B\\r' }, + ], + httpVersion: '1.1', + cookies: [], + queryString: [], + headersSize: 0, + bodySize: 0, + comment: '', + }, undefined)).toEqual(` +request.get(\"http://example.com/foo\", RequestOptions.create() + .setHeader(\"F\\\\o\", \"B\\\\r\") +);`.trim()); + }); +});