From 7ef5d8d5d8b8a081f0f21fa29f1d66307b9a7d8f Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 4 Nov 2024 08:44:18 +0100 Subject: [PATCH] refactor to support multiple languages --- packages/trace-viewer/src/ui/codegen.ts | 127 +++++---- .../src/ui/networkResourceDetails.tsx | 8 +- packages/trace-viewer/src/ui/networkTab.tsx | 2 +- packages/trace-viewer/src/ui/workbench.tsx | 2 +- tests/library/unit/codegen.spec.ts | 244 +++++++++--------- 5 files changed, 201 insertions(+), 182 deletions(-) diff --git a/packages/trace-viewer/src/ui/codegen.ts b/packages/trace-viewer/src/ui/codegen.ts index b6c23fdffe..64c52f949e 100644 --- a/packages/trace-viewer/src/ui/codegen.ts +++ b/packages/trace-viewer/src/ui/codegen.ts @@ -14,73 +14,86 @@ * limitations under the License. */ +import type { Language } from '@isomorphic/locatorGenerators'; import type * as har from '@trace/har'; -export function 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(prettyPrintObject(options)); - return `await page.request.${method}(${params.join(', ')});`; +interface APIRequestCodegen { + generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string; } -function prettyPrintObject(obj: any, indent = 2, level = 0): string { - // Handle null and undefined - if (obj === null) - return 'null'; - if (obj === undefined) - return 'undefined'; +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])); - // Handle primitive types - if (typeof obj !== 'object') { - if (typeof obj === 'string') - return `'${obj}'`; - return String(obj); + const params = [`'${urlParam}'`]; + const hasOptions = Object.keys(options).length > 0; + if (hasOptions) + params.push(this.prettyPrintObject(options)); + return `await page.request.${method}(${params.join(', ')});`; } - // Handle arrays - if (Array.isArray(obj)) { - if (obj.length === 0) - return '[]'; + 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 `'${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 items = obj.map(item => - `${nextSpaces}${prettyPrintObject(item, indent, level + 1)}` - ).join(',\n'); + 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 : + `'${key}'`; + return `${nextSpaces}${formattedKey}: ${formattedValue}`; + }).join(',\n'); - return `[\n${items}\n${spaces}]`; + return `{\n${entries}\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 = prettyPrintObject(value, indent, level + 1); - // Handle keys that need quotes - const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? - key : - `'${key}'`; - return `${nextSpaces}${formattedKey}: ${formattedValue}`; - }).join(',\n'); - - return `{\n${entries}\n${spaces}}`; +} + +export function getAPIRequestCodeGen(language: Language): APIRequestCodegen { + if (language === 'javascript') + return new JSCodeGen(); + 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 00dc443749..9805d42c6f 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -22,13 +22,13 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { ToolbarButton } from '@web/components/toolbarButton'; import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'; import { CopyToClipboardTextButton } from './copyToClipboard'; -import { generatePlaywrightRequestCall } from '@isomorphic/codegen'; +import { getAPIRequestCodeGen } from './codegen'; import type { Language } from '@isomorphic/locatorGenerators'; export const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot; onClose: () => void; - sdkLanguage?: Language; + sdkLanguage: Language; }> = ({ resource, onClose, sdkLanguage }) => { const [selectedTab, setSelectedTab] = React.useState('request'); @@ -58,7 +58,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{ const RequestTab: React.FunctionComponent<{ resource: ResourceSnapshot; - sdkLanguage?: Language; + sdkLanguage: Language; }> = ({ resource, sdkLanguage }) => { const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null); @@ -100,7 +100,7 @@ const RequestTab: React.FunctionComponent<{
generateCurlCommand(resource)} /> generateFetchCall(resource)} /> - {sdkLanguage === 'javascript' && generatePlaywrightRequestCall(resource.request, requestBody?.text)} />} + 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 104a039fe8..56cf9325b4 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -67,7 +67,7 @@ export const NetworkTab: React.FunctionComponent<{ boundaries: Boundaries, networkModel: NetworkTabModel, onEntryHovered?: (entry: Entry | undefined) => void, - sdkLanguage?: Language, + sdkLanguage: Language, }> = ({ boundaries, networkModel, onEntryHovered, sdkLanguage }) => { const [sorting, setSorting] = React.useState(undefined); const [selectedEntry, setSelectedEntry] = React.useState(undefined); diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 916bfec390..bce691bd14 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -225,7 +225,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 index ea21853e69..1dd7c3b85a 100644 --- a/tests/library/unit/codegen.spec.ts +++ b/tests/library/unit/codegen.spec.ts @@ -15,20 +15,24 @@ */ import { test, expect } from '@playwright/test'; -import { generatePlaywrightRequestCall } from '../../../packages/trace-viewer/src/ui/codegen'; +import { getAPIRequestCodeGen } from '../../../packages/trace-viewer/src/ui/codegen'; -test('generatePlaywrightRequestCall', () => { - expect(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(` +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' @@ -40,154 +44,154 @@ await page.request.get('http://example.com/foo', { } });`.trim()); - expect(generatePlaywrightRequestCall({ - url: 'http://example.com/foo?bar=baz', - method: 'OPTIONS', - headers: [], - httpVersion: '1.1', - cookies: [], - queryString: [], - headersSize: 0, - bodySize: 0, - comment: '', - }, undefined)).toEqual(` + 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(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(` + 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(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(` + 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(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(` + 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(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(` + 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(generatePlaywrightRequestCall({ - url: 'http://example.com/foo', - method: 'HEAD', - headers: [], - httpVersion: '1.1', - cookies: [], - queryString: [], - headersSize: 0, - bodySize: 0, - comment: '', - }, undefined)).toEqual(` + 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(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(` + 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(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(` + 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', @@ -195,4 +199,6 @@ await page.request.get('http://example.com/foo', { Authorization: 'Bearer token' } });`.trim()); + }); + });