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,9 +14,15 @@
* 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 {
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}`;
@ -35,11 +41,11 @@ export function generatePlaywrightRequestCall(request: har.Request, body: string
const params = [`'${urlParam}'`];
const hasOptions = Object.keys(options).length > 0;
if (hasOptions)
params.push(prettyPrintObject(options));
params.push(this.prettyPrintObject(options));
return `await page.request.${method}(${params.join(', ')});`;
}
}
function prettyPrintObject(obj: any, indent = 2, level = 0): string {
private prettyPrintObject(obj: any, indent = 2, level = 0): string {
// Handle null and undefined
if (obj === null)
return 'null';
@ -61,7 +67,7 @@ function prettyPrintObject(obj: any, indent = 2, level = 0): string {
const nextSpaces = ' '.repeat((level + 1) * indent);
const items = obj.map(item =>
`${nextSpaces}${prettyPrintObject(item, indent, level + 1)}`
`${nextSpaces}${this.prettyPrintObject(item, indent, level + 1)}`
).join(',\n');
return `[\n${items}\n${spaces}]`;
@ -74,7 +80,7 @@ function prettyPrintObject(obj: any, indent = 2, level = 0): string {
const nextSpaces = ' '.repeat((level + 1) * indent);
const entries = Object.entries(obj).map(([key, value]) => {
const formattedValue = prettyPrintObject(value, indent, level + 1);
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 :
@ -83,4 +89,11 @@ function prettyPrintObject(obj: any, indent = 2, level = 0): string {
}).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,10 +15,14 @@
*/
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({
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' }],
@ -40,7 +44,7 @@ await page.request.get('http://example.com/foo', {
}
});`.trim());
expect(generatePlaywrightRequestCall({
expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo?bar=baz',
method: 'OPTIONS',
headers: [],
@ -57,10 +61,10 @@ await page.request.fetch('http://example.com/foo', {
bar: 'baz'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with POST method and no body', () => {
expect(generatePlaywrightRequestCall({
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' }],
@ -76,10 +80,10 @@ await page.request.post('http://example.com/foo', {
'Content-Type': 'application/json'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with PUT method and JSON body', () => {
expect(generatePlaywrightRequestCall({
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' }],
@ -96,10 +100,10 @@ await page.request.put('http://example.com/foo', {
'Content-Type': 'application/json'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with PATCH method and form data', () => {
expect(generatePlaywrightRequestCall({
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' }],
@ -116,10 +120,10 @@ await page.request.patch('http://example.com/foo', {
'Content-Type': 'application/x-www-form-urlencoded'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with DELETE method and custom header', () => {
expect(generatePlaywrightRequestCall({
test('generatePlaywrightRequestCall with DELETE method and custom header', () => {
expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo',
method: 'DELETE',
headers: [{ name: 'Authorization', value: 'Bearer token' }],
@ -135,10 +139,10 @@ await page.request.delete('http://example.com/foo', {
Authorization: 'Bearer token'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with HEAD method', () => {
expect(generatePlaywrightRequestCall({
test('generatePlaywrightRequestCall with HEAD method', () => {
expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo',
method: 'HEAD',
headers: [],
@ -150,10 +154,10 @@ test('generatePlaywrightRequestCall with HEAD method', () => {
comment: '',
}, undefined)).toEqual(`
await page.request.head('http://example.com/foo');`.trim());
});
});
test('generatePlaywrightRequestCall with complex query parameters', () => {
expect(generatePlaywrightRequestCall({
test('generatePlaywrightRequestCall with complex query parameters', () => {
expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo?bar=baz&qux=quux',
method: 'GET',
headers: [],
@ -170,10 +174,10 @@ await page.request.get('http://example.com/foo', {
qux: 'quux'
}
});`.trim());
});
});
test('generatePlaywrightRequestCall with multiple headers', () => {
expect(generatePlaywrightRequestCall({
test('generatePlaywrightRequestCall with multiple headers', () => {
expect(impl.generatePlaywrightRequestCall({
url: 'http://example.com/foo',
method: 'GET',
headers: [
@ -195,4 +199,6 @@ await page.request.get('http://example.com/foo', {
Authorization: 'Bearer token'
}
});`.trim());
});
});