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..7031ff3859 100644
--- a/packages/trace-viewer/src/ui/actionList.tsx
+++ b/packages/trace-viewer/src/ui/actionList.tsx
@@ -118,6 +118,16 @@ export const renderAction = (
const { errors, warnings } = modelUtil.stats(action);
const showAttachments = !!action.attachments?.length && !!revealAttachment;
+ let apiName = action.apiName;
+ if (apiName === 'apiRequestContext.get')
+ apiName = 'GET';
+ else if (apiName === 'apiRequestContext.post')
+ apiName = 'POST';
+ else if (apiName === 'apiRequestContext.put')
+ apiName = 'PUT';
+ else if (apiName === 'apiRequestContext.delete')
+ apiName = 'DELETE';
+
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
const isSkipped = action.class === 'Test' && action.method === 'step' && action.annotations?.some(a => a.type === 'skip');
@@ -129,8 +139,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 34f27d65cb..947b10583f 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;
+}