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. * limitations under the License.
*/ */
import type { Language } from '@isomorphic/locatorGenerators';
import type * as har from '@trace/har'; import type * as har from '@trace/har';
export function generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string { interface APIRequestCodegen {
let method = request.method.toLowerCase(); generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string;
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(', ')});`;
} }
function prettyPrintObject(obj: any, indent = 2, level = 0): string { class JSCodeGen implements APIRequestCodegen {
// Handle null and undefined generatePlaywrightRequestCall(request: har.Request, body: string | undefined): string {
if (obj === null) let method = request.method.toLowerCase();
return 'null'; const url = new URL(request.url);
if (obj === undefined) const urlParam = `${url.origin}${url.pathname}`;
return 'undefined'; 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 const params = [`'${urlParam}'`];
if (typeof obj !== 'object') { const hasOptions = Object.keys(options).length > 0;
if (typeof obj === 'string') if (hasOptions)
return `'${obj}'`; params.push(this.prettyPrintObject(options));
return String(obj); return `await page.request.${method}(${params.join(', ')});`;
} }
// Handle arrays private prettyPrintObject(obj: any, indent = 2, level = 0): string {
if (Array.isArray(obj)) { // Handle null and undefined
if (obj.length === 0) if (obj === null)
return '[]'; 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 spaces = ' '.repeat(level * indent);
const nextSpaces = ' '.repeat((level + 1) * indent); const nextSpaces = ' '.repeat((level + 1) * indent);
const items = obj.map(item => const entries = Object.entries(obj).map(([key, value]) => {
`${nextSpaces}${prettyPrintObject(item, indent, level + 1)}` const formattedValue = this.prettyPrintObject(value, indent, level + 1);
).join(',\n'); // 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) export function getAPIRequestCodeGen(language: Language): APIRequestCodegen {
return '{}'; if (language === 'javascript')
const spaces = ' '.repeat(level * indent); return new JSCodeGen();
const nextSpaces = ' '.repeat((level + 1) * indent); throw new Error('Unsupported language: ' + language);
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}}`;
} }

View file

@ -22,13 +22,13 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'; import { generateCurlCommand, generateFetchCall } from '../third_party/devtools';
import { CopyToClipboardTextButton } from './copyToClipboard'; import { CopyToClipboardTextButton } from './copyToClipboard';
import { generatePlaywrightRequestCall } from '@isomorphic/codegen'; import { getAPIRequestCodeGen } from './codegen';
import type { Language } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators';
export const NetworkResourceDetails: React.FunctionComponent<{ export const NetworkResourceDetails: React.FunctionComponent<{
resource: ResourceSnapshot; resource: ResourceSnapshot;
onClose: () => void; onClose: () => void;
sdkLanguage?: Language; sdkLanguage: Language;
}> = ({ resource, onClose, sdkLanguage }) => { }> = ({ resource, onClose, sdkLanguage }) => {
const [selectedTab, setSelectedTab] = React.useState('request'); const [selectedTab, setSelectedTab] = React.useState('request');
@ -58,7 +58,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const RequestTab: React.FunctionComponent<{ const RequestTab: React.FunctionComponent<{
resource: ResourceSnapshot; resource: ResourceSnapshot;
sdkLanguage?: Language; sdkLanguage: Language;
}> = ({ resource, sdkLanguage }) => { }> = ({ resource, sdkLanguage }) => {
const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null); 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'> <div className='network-request-details-copy'>
<CopyToClipboardTextButton description='Copy as cURL' value={() => generateCurlCommand(resource)} /> <CopyToClipboardTextButton description='Copy as cURL' value={() => generateCurlCommand(resource)} />
<CopyToClipboardTextButton description='Copy as Fetch' value={() => generateFetchCall(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> </div>
{requestBody && <div className='network-request-details-header'>Request Body</div>} {requestBody && <div className='network-request-details-header'>Request Body</div>}

View file

@ -67,7 +67,7 @@ export const NetworkTab: React.FunctionComponent<{
boundaries: Boundaries, boundaries: Boundaries,
networkModel: NetworkTabModel, networkModel: NetworkTabModel,
onEntryHovered?: (entry: Entry | undefined) => void, onEntryHovered?: (entry: Entry | undefined) => void,
sdkLanguage?: Language, sdkLanguage: Language,
}> = ({ boundaries, networkModel, onEntryHovered, sdkLanguage }) => { }> = ({ boundaries, networkModel, onEntryHovered, sdkLanguage }) => {
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined); const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined); const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);

View file

@ -225,7 +225,7 @@ export const Workbench: React.FunctionComponent<{
id: 'network', id: 'network',
title: 'Network', title: 'Network',
count: networkModel.resources.length, 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 = { const attachmentsTab: TabbedPaneTabModel = {
id: 'attachments', id: 'attachments',

View file

@ -15,20 +15,24 @@
*/ */
import { test, expect } from '@playwright/test'; 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', () => { test.describe('javascript', () => {
expect(generatePlaywrightRequestCall({ const impl = getAPIRequestCodeGen('javascript');
url: 'http://example.com/foo?bar=baz',
method: 'GET', test('generatePlaywrightRequestCall', () => {
headers: [{ name: 'User-Agent', value: 'Mozilla/5.0' }, { name: 'Date', value: '2021-01-01' }],
httpVersion: '1.1', expect(impl.generatePlaywrightRequestCall({
cookies: [], url: 'http://example.com/foo?bar=baz',
queryString: [], method: 'GET',
headersSize: 0, headers: [{ name: 'User-Agent', value: 'Mozilla/5.0' }, { name: 'Date', value: '2021-01-01' }],
bodySize: 0, httpVersion: '1.1',
comment: '', cookies: [],
}, 'foo')).toEqual(` queryString: [],
headersSize: 0,
bodySize: 0,
comment: '',
}, 'foo')).toEqual(`
await page.request.get('http://example.com/foo', { await page.request.get('http://example.com/foo', {
params: { params: {
bar: 'baz' bar: 'baz'
@ -40,154 +44,154 @@ await page.request.get('http://example.com/foo', {
} }
});`.trim()); });`.trim());
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo?bar=baz', url: 'http://example.com/foo?bar=baz',
method: 'OPTIONS', method: 'OPTIONS',
headers: [], headers: [],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.fetch('http://example.com/foo', { await page.request.fetch('http://example.com/foo', {
method: 'options', method: 'options',
params: { params: {
bar: 'baz' bar: 'baz'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with POST method and no body', () => { test('generatePlaywrightRequestCall with POST method and no body', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'POST', method: 'POST',
headers: [{ name: 'Content-Type', value: 'application/json' }], headers: [{ name: 'Content-Type', value: 'application/json' }],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.post('http://example.com/foo', { await page.request.post('http://example.com/foo', {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with PUT method and JSON body', () => { test('generatePlaywrightRequestCall with PUT method and JSON body', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'PUT', method: 'PUT',
headers: [{ name: 'Content-Type', value: 'application/json' }], headers: [{ name: 'Content-Type', value: 'application/json' }],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, '{"key":"value"}')).toEqual(` }, '{"key":"value"}')).toEqual(`
await page.request.put('http://example.com/foo', { await page.request.put('http://example.com/foo', {
data: '{"key":"value"}', data: '{"key":"value"}',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with PATCH method and form data', () => { test('generatePlaywrightRequestCall with PATCH method and form data', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'PATCH', method: 'PATCH',
headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, 'key=value')).toEqual(` }, 'key=value')).toEqual(`
await page.request.patch('http://example.com/foo', { await page.request.patch('http://example.com/foo', {
data: 'key=value', data: 'key=value',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with DELETE method and custom header', () => { test('generatePlaywrightRequestCall with DELETE method and custom header', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'DELETE', method: 'DELETE',
headers: [{ name: 'Authorization', value: 'Bearer token' }], headers: [{ name: 'Authorization', value: 'Bearer token' }],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.delete('http://example.com/foo', { await page.request.delete('http://example.com/foo', {
headers: { headers: {
Authorization: 'Bearer token' Authorization: 'Bearer token'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with HEAD method', () => { test('generatePlaywrightRequestCall with HEAD method', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'HEAD', method: 'HEAD',
headers: [], headers: [],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.head('http://example.com/foo');`.trim()); await page.request.head('http://example.com/foo');`.trim());
}); });
test('generatePlaywrightRequestCall with complex query parameters', () => { test('generatePlaywrightRequestCall with complex query parameters', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo?bar=baz&qux=quux', url: 'http://example.com/foo?bar=baz&qux=quux',
method: 'GET', method: 'GET',
headers: [], headers: [],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.get('http://example.com/foo', { await page.request.get('http://example.com/foo', {
params: { params: {
bar: 'baz', bar: 'baz',
qux: 'quux' qux: 'quux'
} }
});`.trim()); });`.trim());
}); });
test('generatePlaywrightRequestCall with multiple headers', () => { test('generatePlaywrightRequestCall with multiple headers', () => {
expect(generatePlaywrightRequestCall({ expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo', url: 'http://example.com/foo',
method: 'GET', method: 'GET',
headers: [ headers: [
{ name: 'User-Agent', value: 'Mozilla/5.0' }, { name: 'User-Agent', value: 'Mozilla/5.0' },
{ name: 'Accept', value: 'application/json' }, { name: 'Accept', value: 'application/json' },
{ name: 'Authorization', value: 'Bearer token' } { name: 'Authorization', value: 'Bearer token' }
], ],
httpVersion: '1.1', httpVersion: '1.1',
cookies: [], cookies: [],
queryString: [], queryString: [],
headersSize: 0, headersSize: 0,
bodySize: 0, bodySize: 0,
comment: '', comment: '',
}, undefined)).toEqual(` }, undefined)).toEqual(`
await page.request.get('http://example.com/foo', { await page.request.get('http://example.com/foo', {
headers: { headers: {
'User-Agent': 'Mozilla/5.0', 'User-Agent': 'Mozilla/5.0',
@ -195,4 +199,6 @@ await page.request.get('http://example.com/foo', {
Authorization: 'Bearer token' Authorization: 'Bearer token'
} }
});`.trim()); });`.trim());
});
}); });