This commit is contained in:
Simon Knott 2025-02-27 13:56:23 -08:00 committed by GitHub
commit 453e448e6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 108 additions and 18 deletions

View file

@ -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({});
});

View file

@ -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 <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
<div className='action-title' title={apiName}>
<span>{apiName}</span>
{parameterString &&
(parameterString.type === 'locator' ? (
<>

View file

@ -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 || []) {

View file

@ -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',

View file

@ -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<string, modelUtil.SourceModel>(), [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: <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}
@ -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]}

View file

@ -258,3 +258,16 @@ export function useCookies() {
}, []);
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);
return value;
}