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());
+ });
+
});