Merge 1baef032e8 into 7a61aa25e6
This commit is contained in:
commit
453e448e6a
37
examples/todomvc/tests/api.spec.ts
Normal file
37
examples/todomvc/tests/api.spec.ts
Normal 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({});
|
||||
});
|
||||
|
|
@ -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' ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 || []) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue