refactor to support multiple languages

This commit is contained in:
Simon Knott 2024-11-04 08:44:18 +01:00
parent 6ffab68dfa
commit 7ef5d8d5d8
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
5 changed files with 201 additions and 182 deletions

View file

@ -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);
}

View file

@ -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<{
<div className='network-request-details-copy'>
<CopyToClipboardTextButton description='Copy as cURL' value={() => generateCurlCommand(resource)} />
<CopyToClipboardTextButton description='Copy as Fetch' value={() => generateFetchCall(resource)} />
{sdkLanguage === 'javascript' && <CopyToClipboardTextButton description='Copy as Playwright' value={async () => generatePlaywrightRequestCall(resource.request, requestBody?.text)} />}
<CopyToClipboardTextButton description='Copy as Playwright' value={async () => getAPIRequestCodeGen(sdkLanguage).generatePlaywrightRequestCall(resource.request, requestBody?.text)} />
</div>
{requestBody && <div className='network-request-details-header'>Request Body</div>}

View file

@ -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<Sorting | undefined>(undefined);
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);

View file

@ -225,7 +225,7 @@ export const Workbench: React.FunctionComponent<{
id: 'network',
title: 'Network',
count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry} sdkLanguage={model?.sdkLanguage} />
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry} sdkLanguage={model?.sdkLanguage ?? 'javascript'} />
};
const attachmentsTab: TabbedPaneTabModel = {
id: 'attachments',

View file

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