Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trace): special UI for API tests #34933

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/todomvc/tests/api.spec.ts
Original file line number Diff line number Diff line change
@@ -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({});
});
11 changes: 9 additions & 2 deletions packages/trace-viewer/src/ui/actionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -129,8 +136,8 @@ export const renderAction = (
else if (!isLive)
time = '-';
return <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
<div className='action-title' title={apiName}>
<span>{apiName}</span>
{parameterString &&
(parameterString.type === 'locator' ? (
<>
Expand Down
16 changes: 16 additions & 0 deletions packages/trace-viewer/src/ui/modelUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@
return this.actions.findLast(a => a.error);
}

/**
* Heuristic to toggle API testing UI.

Check failure on line 117 in packages/trace-viewer/src/ui/modelUtil.ts

View workflow job for this annotation

GitHub Actions / docs & lint

Trailing spaces not allowed
*/
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 || []) {
Expand Down
4 changes: 2 additions & 2 deletions packages/trace-viewer/src/ui/networkResourceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TabbedPane
dataTestId='network-request-details'
leftToolbar={[<ToolbarButton key='close' icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
leftToolbar={onClose ? [<ToolbarButton key='close' icon='close' title='Close' onClick={onClose}></ToolbarButton>] : undefined}
tabs={[
{
id: 'request',
Expand Down
45 changes: 31 additions & 14 deletions packages/trace-viewer/src/ui/workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -85,6 +86,8 @@ export const Workbench: React.FunctionComponent<{

const sources = React.useMemo(() => model?.sources || new Map<string, modelUtil.SourceModel>(), [model]);

const isAPITrace = useMemoWithMemory(() => model?.isAPITrace(), false, [model]);

React.useEffect(() => {
setSelectedTime(undefined);
setRevealedError(undefined);
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -320,8 +323,30 @@ export const Workbench: React.FunctionComponent<{
component: <MetadataView model={model}/>
};

const selectedResource = selectedAction ? networkModel.resources.findLast(r => (r._monotonicTime ?? 0) < selectedAction.endTime) : undefined;
const displayedResource = selectedResource ?? networkModel.resources[0];
const networkView = displayedResource && (
<NetworkResourceDetails
resource={displayedResource}
sdkLanguage={sdkLanguage}
startTimeOffset={0}
/>
);

const snapshotsTabView = (
<SnapshotTabsView
action={activeAction}
model={model}
sdkLanguage={sdkLanguage}
testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={elementPicked} />
);

return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
{!hideTimeline && <Timeline
{(!hideTimeline && !isAPITrace) && <Timeline
model={model}
consoleEntries={consoleModel.entries}
boundaries={boundaries}
Expand All @@ -341,15 +366,7 @@ export const Workbench: React.FunctionComponent<{
orientation='horizontal'
sidebarIsFirst
settingName='actionListSidebar'
main={<SnapshotTabsView
action={activeAction}
model={model}
sdkLanguage={sdkLanguage}
testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={elementPicked} />}
main={isAPITrace ? networkView : snapshotsTabView}
sidebar={
<TabbedPane
tabs={[actionsTab, metadataTab]}
Expand Down
13 changes: 13 additions & 0 deletions packages/web/src/uiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,16 @@
}, []);
return cookies;
}

/**
* Returns result of `fn()`. If `fn` returns undefined, returns last non-undefined value.
*/
export function useMemoWithMemory<T>(fn: () => T | undefined, initialValue: T, deps: React.DependencyList) {
const [value, setValue] = React.useState<T>(initialValue);
React.useEffect(() => {
const value = fn();
if (value !== undefined)
setValue(value);
}, deps);

Check warning on line 271 in packages/web/src/uiUtils.ts

View workflow job for this annotation

GitHub Actions / docs & lint

React Hook React.useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies

Check warning on line 271 in packages/web/src/uiUtils.ts

View workflow job for this annotation

GitHub Actions / docs & lint

React Hook React.useEffect has a missing dependency: 'fn'. Either include it or remove the dependency array. If 'fn' changes too often, find the parent component that defines it and wrap that definition in useCallback
return value;
}
Loading