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 { errors, warnings } = modelUtil.stats(action);
|
||||||
const showAttachments = !!action.attachments?.length && !!revealAttachment;
|
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 parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||||
|
|
||||||
const isSkipped = action.class === 'Test' && action.method === 'step' && action.annotations?.some(a => a.type === 'skip');
|
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)
|
else if (!isLive)
|
||||||
time = '-';
|
time = '-';
|
||||||
return <>
|
return <>
|
||||||
<div className='action-title' title={action.apiName}>
|
<div className='action-title' title={apiName}>
|
||||||
<span>{action.apiName}</span>
|
<span>{apiName}</span>
|
||||||
{parameterString &&
|
{parameterString &&
|
||||||
(parameterString.type === 'locator' ? (
|
(parameterString.type === 'locator' ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,22 @@ export class MultiTraceModel {
|
||||||
return this.actions.findLast(a => a.error);
|
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[] {
|
private _errorDescriptorsFromActions(): ErrorDescription[] {
|
||||||
const errors: ErrorDescription[] = [];
|
const errors: ErrorDescription[] = [];
|
||||||
for (const action of this.actions || []) {
|
for (const action of this.actions || []) {
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
sdkLanguage: Language;
|
sdkLanguage: Language;
|
||||||
startTimeOffset: number;
|
startTimeOffset: number;
|
||||||
onClose: () => void;
|
onClose?: () => void;
|
||||||
}> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => {
|
}> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => {
|
||||||
const [selectedTab, setSelectedTab] = React.useState('request');
|
const [selectedTab, setSelectedTab] = React.useState('request');
|
||||||
|
|
||||||
return <TabbedPane
|
return <TabbedPane
|
||||||
dataTestId='network-request-details'
|
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={[
|
tabs={[
|
||||||
{
|
{
|
||||||
id: 'request',
|
id: 'request',
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,14 @@ import { AnnotationsTab } from './annotationsTab';
|
||||||
import type { Boundaries } from './geometry';
|
import type { Boundaries } from './geometry';
|
||||||
import { InspectorTab } from './inspectorTab';
|
import { InspectorTab } from './inspectorTab';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
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 type { Entry } from '@trace/har';
|
||||||
import './workbench.css';
|
import './workbench.css';
|
||||||
import { testStatusIcon, testStatusText } from './testUtils';
|
import { testStatusIcon, testStatusText } from './testUtils';
|
||||||
import type { UITestStatus } from './testUtils';
|
import type { UITestStatus } from './testUtils';
|
||||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import type { HighlightedElement } from './snapshotTab';
|
import type { HighlightedElement } from './snapshotTab';
|
||||||
|
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: modelUtil.MultiTraceModel,
|
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 sources = React.useMemo(() => model?.sources || new Map<string, modelUtil.SourceModel>(), [model]);
|
||||||
|
|
||||||
|
const isAPITrace = useMemoWithMemory(() => model?.isAPITrace(), false, [model]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setSelectedTime(undefined);
|
setSelectedTime(undefined);
|
||||||
setRevealedError(undefined);
|
setRevealedError(undefined);
|
||||||
|
|
@ -247,15 +250,15 @@ export const Workbench: React.FunctionComponent<{
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = [
|
const tabs: TabbedPaneTabModel[] = [
|
||||||
inspectorTab,
|
!isAPITrace && inspectorTab,
|
||||||
callTab,
|
callTab,
|
||||||
logTab,
|
logTab,
|
||||||
errorsTab,
|
errorsTab,
|
||||||
consoleTab,
|
consoleTab,
|
||||||
networkTab,
|
!isAPITrace && networkTab,
|
||||||
sourceTab,
|
sourceTab,
|
||||||
attachmentsTab,
|
attachmentsTab,
|
||||||
];
|
].filter(v => !!v);
|
||||||
|
|
||||||
if (annotations !== undefined) {
|
if (annotations !== undefined) {
|
||||||
const annotationsTab: TabbedPaneTabModel = {
|
const annotationsTab: TabbedPaneTabModel = {
|
||||||
|
|
@ -320,8 +323,30 @@ export const Workbench: React.FunctionComponent<{
|
||||||
component: <MetadataView model={model}/>
|
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' } : {})}>
|
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
||||||
{!hideTimeline && <Timeline
|
{(!hideTimeline && !isAPITrace) && <Timeline
|
||||||
model={model}
|
model={model}
|
||||||
consoleEntries={consoleModel.entries}
|
consoleEntries={consoleModel.entries}
|
||||||
boundaries={boundaries}
|
boundaries={boundaries}
|
||||||
|
|
@ -341,15 +366,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
orientation='horizontal'
|
orientation='horizontal'
|
||||||
sidebarIsFirst
|
sidebarIsFirst
|
||||||
settingName='actionListSidebar'
|
settingName='actionListSidebar'
|
||||||
main={<SnapshotTabsView
|
main={isAPITrace ? networkView : snapshotsTabView}
|
||||||
action={activeAction}
|
|
||||||
model={model}
|
|
||||||
sdkLanguage={sdkLanguage}
|
|
||||||
testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
|
|
||||||
isInspecting={isInspecting}
|
|
||||||
setIsInspecting={setIsInspecting}
|
|
||||||
highlightedElement={highlightedElement}
|
|
||||||
setHighlightedElement={elementPicked} />}
|
|
||||||
sidebar={
|
sidebar={
|
||||||
<TabbedPane
|
<TabbedPane
|
||||||
tabs={[actionsTab, metadataTab]}
|
tabs={[actionsTab, metadataTab]}
|
||||||
|
|
|
||||||
|
|
@ -258,3 +258,16 @@ export function useCookies() {
|
||||||
}, []);
|
}, []);
|
||||||
return cookies;
|
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