diff --git a/examples/todomvc/tests/api.spec.ts b/examples/todomvc/tests/api.spec.ts new file mode 100644 index 0000000000..f6b69471c9 --- /dev/null +++ b/examples/todomvc/tests/api.spec.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; + +test.use({ + baseURL: 'https://jsonplaceholder.typicode.com', +}); + +test('posts', async ({ request }) => { + const get = await request.get('/posts'); + expect(get.ok()).toBeTruthy(); + expect(await get.json()).toBeInstanceOf(Array); + + const post = await request.post('/posts'); + expect(post.ok()).toBeTruthy(); + expect(await post.json()).toEqual({ + id: expect.any(Number), + }); + + const del = await request.delete('/posts/1'); + expect(del.ok()).toBeTruthy(); + expect(await del.json()).toEqual({}); +}); diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 4f3a8128e8..bf2b0c723c 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -118,6 +118,13 @@ export const renderAction = ( const { errors, warnings } = modelUtil.stats(action); const showAttachments = !!action.attachments?.length && !!revealAttachment; + const apiName = { + 'apiRequestContext.get': 'GET', + 'apiRequestContext.post': 'POST', + 'apiRequestContext.put': 'PUT', + 'apiRequestContext.delete': 'DELETE', + }[action.apiName] ?? action.apiName; + const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); const isSkipped = action.class === 'Test' && action.method === 'step' && action.annotations?.some(a => a.type === 'skip'); @@ -129,8 +136,8 @@ export const renderAction = ( else if (!isLive) time = '-'; return <> -
- {action.apiName} +
+ {apiName} {parameterString && (parameterString.type === 'locator' ? ( <> diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 01af841748..9ef6b3ef7e 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -113,6 +113,22 @@ export class MultiTraceModel { return this.actions.findLast(a => a.error); } + /** + * Heuristic to toggle API testing UI. + */ + isAPITrace(): boolean | undefined { + if (this.browserName) + return false; + + if (this.hasStepData) { + const setupDone = this.actions.some(a => a.apiName === 'Before Hooks' && a.endTime > 0); + if (!setupDone) // until the setup is done, we can't tell if it's an API test. + return undefined; + } + + return true; + } + private _errorDescriptorsFromActions(): ErrorDescription[] { const errors: ErrorDescription[] = []; for (const action of this.actions || []) { diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index aaa78d1786..5dee81a667 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -30,13 +30,13 @@ export const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot; sdkLanguage: Language; startTimeOffset: number; - onClose: () => void; + onClose?: () => void; }> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => { const [selectedTab, setSelectedTab] = React.useState('request'); return ]} + leftToolbar={onClose ? [] : undefined} tabs={[ { id: 'request', diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 3e361ad14c..a9419a02d6 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -36,13 +36,14 @@ import { AnnotationsTab } from './annotationsTab'; import type { Boundaries } from './geometry'; import { InspectorTab } from './inspectorTab'; import { ToolbarButton } from '@web/components/toolbarButton'; -import { useSetting, msToString, clsx } from '@web/uiUtils'; +import { useSetting, msToString, clsx, useMemoWithMemory } from '@web/uiUtils'; import type { Entry } from '@trace/har'; import './workbench.css'; import { testStatusIcon, testStatusText } from './testUtils'; import type { UITestStatus } from './testUtils'; import type { AfterActionTraceEventAttachment } from '@trace/trace'; import type { HighlightedElement } from './snapshotTab'; +import { NetworkResourceDetails } from './networkResourceDetails'; export const Workbench: React.FunctionComponent<{ model?: modelUtil.MultiTraceModel, @@ -85,6 +86,8 @@ export const Workbench: React.FunctionComponent<{ const sources = React.useMemo(() => model?.sources || new Map(), [model]); + const isAPITrace = useMemoWithMemory(() => model?.isAPITrace(), false, [model]); + React.useEffect(() => { setSelectedTime(undefined); setRevealedError(undefined); @@ -247,15 +250,15 @@ export const Workbench: React.FunctionComponent<{ }; const tabs: TabbedPaneTabModel[] = [ - inspectorTab, + !isAPITrace && inspectorTab, callTab, logTab, errorsTab, consoleTab, - networkTab, + !isAPITrace && networkTab, sourceTab, attachmentsTab, - ]; + ].filter(v => !!v); if (annotations !== undefined) { const annotationsTab: TabbedPaneTabModel = { @@ -320,8 +323,30 @@ export const Workbench: React.FunctionComponent<{ component: }; + const selectedResource = selectedAction ? networkModel.resources.findLast(r => (r._monotonicTime ?? 0) < selectedAction.endTime) : undefined; + const displayedResource = selectedResource ?? networkModel.resources[0]; + const networkView = displayedResource && ( + + ); + + const snapshotsTabView = ( + + ); + return
- {!hideTimeline && } + main={isAPITrace ? networkView : snapshotsTabView} sidebar={ (fn: () => T | undefined, initialValue: T, deps: React.DependencyList) { + const [value, setValue] = React.useState(initialValue); + React.useEffect(() => { + const value = fn(); + if (value !== undefined) + setValue(value); + }, deps); + return value; +}