From 90b4a875b466258cd7766cdee7296bbdaeecd39a Mon Sep 17 00:00:00 2001 From: James Gatz Date: Mon, 11 Sep 2023 17:55:46 +0200 Subject: [PATCH] Improve UI for test view (#6477) * test-results * cleanup * improve types * fix some stuff --- .../insomnia/src/models/unit-test-result.ts | 4 +- .../src/ui/components/editable-input.tsx | 92 ++++ .../src/ui/components/list-group/index.ts | 7 - .../components/list-group/list-group-item.tsx | 36 -- .../ui/components/list-group/list-group.tsx | 20 - .../components/list-group/unit-test-item.tsx | 118 ----- .../list-group/unit-test-request-selector.tsx | 59 --- .../list-group/unit-test-result-badge.tsx | 39 -- .../list-group/unit-test-result-item.tsx | 65 --- .../list-group/unit-test-result-timestamp.tsx | 33 -- .../components/proto-file/proto-file-list.tsx | 66 +-- .../insomnia/src/ui/routes/test-results.tsx | 79 ++- .../insomnia/src/ui/routes/test-suite.tsx | 479 ++++++++++-------- packages/insomnia/src/ui/routes/unit-test.tsx | 272 ++++++---- 14 files changed, 625 insertions(+), 744 deletions(-) create mode 100644 packages/insomnia/src/ui/components/editable-input.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/index.ts delete mode 100644 packages/insomnia/src/ui/components/list-group/list-group-item.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/list-group.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/unit-test-item.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/unit-test-request-selector.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/unit-test-result-badge.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/unit-test-result-item.tsx delete mode 100644 packages/insomnia/src/ui/components/list-group/unit-test-result-timestamp.tsx diff --git a/packages/insomnia/src/models/unit-test-result.ts b/packages/insomnia/src/models/unit-test-result.ts index 2f7e080359..1fdf32b75f 100644 --- a/packages/insomnia/src/models/unit-test-result.ts +++ b/packages/insomnia/src/models/unit-test-result.ts @@ -1,3 +1,5 @@ +import type { TestResults } from 'insomnia-testing'; + import { database as db } from '../common/database'; import type { BaseModel } from './index'; @@ -12,7 +14,7 @@ export const canDuplicate = false; export const canSync = false; export interface BaseUnitTestResult { - results: Record; + results: TestResults; } export type UnitTestResult = BaseModel & BaseUnitTestResult; diff --git a/packages/insomnia/src/ui/components/editable-input.tsx b/packages/insomnia/src/ui/components/editable-input.tsx new file mode 100644 index 0000000000..f848cefd78 --- /dev/null +++ b/packages/insomnia/src/ui/components/editable-input.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useState } from 'react'; +import { FocusScope } from 'react-aria'; +import { Button, Input } from 'react-aria-components'; + +export const EditableInput = ({ + value, + ariaLabel, + name, + onChange, +}: { + value: string; + ariaLabel?: string; + name?: string; + onChange: (value: string) => void; +}) => { + const [isEditable, setIsEditable] = useState(false); + + useEffect(() => { + if (!isEditable) { + return; + } + + const keysToLock = [ + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'ArrowDown', + 'Tab', + ' ', + ]; + + function lockKeyDownToInput(e: KeyboardEvent) { + if (keysToLock.includes(e.key)) { + e.stopPropagation(); + } + } + + window.addEventListener('keydown', lockKeyDownToInput, { capture: true }); + + return () => { + window.removeEventListener('keydown', lockKeyDownToInput, { + capture: true, + }); + }; + }, [isEditable]); + + return ( + <> + + {isEditable && ( + + { + const value = e.currentTarget.value; + if (e.key === 'Enter') { + e.stopPropagation(); + onChange(value); + setIsEditable(false); + } + + if (e.key === 'Escape') { + e.stopPropagation(); + setIsEditable(false); + } + }} + onBlur={e => { + const value = e.currentTarget.value; + onChange(value); + setIsEditable(false); + }} + /> + + )} + + ); +}; diff --git a/packages/insomnia/src/ui/components/list-group/index.ts b/packages/insomnia/src/ui/components/list-group/index.ts deleted file mode 100644 index 20b4ec0e0d..0000000000 --- a/packages/insomnia/src/ui/components/list-group/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { ListGroupItem, type ListGroupItemProps } from './list-group-item'; -export { ListGroup, type ListGroupProps } from './list-group'; -export { UnitTestItem, type UnitTestItemProps, type TestItem } from './unit-test-item'; -export { UnitTestRequestSelector, type UnitTestRequestSelectorProps } from './unit-test-request-selector'; -export { UnitTestResultBadge, type UnitTestResultBadgeProps } from './unit-test-result-badge'; -export { UnitTestResultItem, type UnitTestResultItemProps } from './unit-test-result-item'; -export { UnitTestResultTimestamp, type UnitTestResultTimestampProps } from './unit-test-result-timestamp'; diff --git a/packages/insomnia/src/ui/components/list-group/list-group-item.tsx b/packages/insomnia/src/ui/components/list-group/list-group-item.tsx deleted file mode 100644 index 64148e7f2c..0000000000 --- a/packages/insomnia/src/ui/components/list-group/list-group-item.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { FC } from 'react'; -import styled, { css } from 'styled-components'; - -export interface ListGroupItemProps extends React.LiHTMLAttributes { - isSelected?: boolean; - selectable?: boolean; - indentLevel?: number; -} - -const StyledListGroupItem = styled.li` - border-bottom: 1px solid var(--hl-xs); - padding: var(--padding-sm) var(--padding-sm); - - ${({ selectable }) => - selectable && - css` - &:hover { - background-color: var(--hl-sm) !important; - } - `} - - ${({ isSelected }) => - isSelected && - css` - background-color: var(--hl-xs) !important; - font-weight: bold; - `} - - ${({ indentLevel }) => - indentLevel && - css` - padding-left: calc(var(--padding-sm) + var(--padding-md) * ${indentLevel}); - `}; -`; - -export const ListGroupItem: FC = props => ; diff --git a/packages/insomnia/src/ui/components/list-group/list-group.tsx b/packages/insomnia/src/ui/components/list-group/list-group.tsx deleted file mode 100644 index 28fd948659..0000000000 --- a/packages/insomnia/src/ui/components/list-group/list-group.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react'; -import styled from 'styled-components'; - -export interface ListGroupProps { - children?: ReactNode; - bordered?: boolean; -} - -export const ListGroup = styled.ul` - list-style-type: none; - margin: 0; - padding: 0; - - ${({ bordered }) => - bordered && - `border: 1px solid var(--hl-sm); - border-radius: var(--radius-sm); - li:last-of-type {border-bottom:none;}; - `} -`; diff --git a/packages/insomnia/src/ui/components/list-group/unit-test-item.tsx b/packages/insomnia/src/ui/components/list-group/unit-test-item.tsx deleted file mode 100644 index 964eabcfd4..0000000000 --- a/packages/insomnia/src/ui/components/list-group/unit-test-item.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { FunctionComponent, ReactNode } from 'react'; -import useToggle from 'react-use/lib/useToggle'; -import styled from 'styled-components'; - -import { Request } from '../../../models/request'; -import { SvgIcon } from '../svg-icon'; -import { Button } from '../themed-button'; -import { ListGroupItem } from './list-group-item'; -import { UnitTestRequestSelector } from './unit-test-request-selector'; - -export interface TestItem { - _id: string; -} - -export interface UnitTestItemProps { - item: TestItem; - children?: ReactNode; - onDeleteTest?: () => void; - onRunTest?: () => void; - testNameEditable?: ReactNode; - testsRunning?: boolean; - onSetActiveRequest: React.ChangeEventHandler; - selectedRequestId?: string | null; - selectableRequests: Request[]; -} - -const StyledResultListItem = styled(ListGroupItem)` - && { - padding: 0 var(--padding-sm); - - > div:first-of-type { - display: flex; - flex-direction: row; - align-items: flex-start; - align-items: center; - } - - svg { - fill: var(--hl-xl); - } - - h2 { - font-size: var(--font-size-md); - font-weight: var(--font-weight-normal); - margin: 0px; - } - - button { - padding: 0px var(--padding-sm); - } - } -`; - -const StyledUnitTestContent = styled.div` - display: block; - height: 0px; - overflow: hidden; -`; - -export const UnitTestItem: FunctionComponent = ({ - children, - onDeleteTest, - onRunTest, - testNameEditable, - testsRunning, - onSetActiveRequest, - selectedRequestId, - selectableRequests, -}) => { - const [isToggled, toggle] = useToggle(false); - const toggleIconRotation = -90; - - return ( - -
- - - - -

{testNameEditable}

- - - - - - -
- - - {isToggled &&
{children}
} -
-
- ); -}; diff --git a/packages/insomnia/src/ui/components/list-group/unit-test-request-selector.tsx b/packages/insomnia/src/ui/components/list-group/unit-test-request-selector.tsx deleted file mode 100644 index a4ba5c6f0b..0000000000 --- a/packages/insomnia/src/ui/components/list-group/unit-test-request-selector.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import styled from 'styled-components'; - -import { Request } from '../../../models/request'; - -export interface UnitTestRequestSelectorProps { - onSetActiveRequest: React.ChangeEventHandler; - selectedRequestId?: string | null; - selectableRequests: Request[]; -} - -const StyledUnitTestRequestSelector = styled.div` - padding: 0 var(--padding-sm); - border: 1px solid var(--hl-md); - border-radius: var(--radius-sm); - margin: var(--padding-sm) var(--padding-sm) var(--padding-sm) auto; - max-width: 18rem; - flex: 0 0 auto; - select { - max-width: 18rem; - height: var(--line-height-xs); - border: none !important; - background: none !important; - color: var(--font-color); - - &:focus { - outline: 0; - } - } - * { - margin: 0px; - } -`; - -export const UnitTestRequestSelector: FunctionComponent = ({ - onSetActiveRequest, - selectedRequestId, - selectableRequests, -}) => { - return ( - - - - ); -}; diff --git a/packages/insomnia/src/ui/components/list-group/unit-test-result-badge.tsx b/packages/insomnia/src/ui/components/list-group/unit-test-result-badge.tsx deleted file mode 100644 index 6b145226c2..0000000000 --- a/packages/insomnia/src/ui/components/list-group/unit-test-result-badge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import styled from 'styled-components'; - -export interface UnitTestResultBadgeProps { - failed?: boolean; -} - -const StyledBadge = styled.span` - padding: var(--padding-xs) var(--padding-sm); - border: 1px solid var(--color-success); - background-color: var(--color-bg); - color: var(--color-success); - font-weight: var(--font-weight-bold); - border-radius: var(--radius-sm); - flex-basis: 3.5em; - flex-shrink: 0; - text-align: center; - text-transform: capitalize; -`; - -const StyledFailedBadge = styled(StyledBadge)` - && { - border-color: var(--color-danger); - color: var(--color-danger); - } -`; - -const StyledPassedBadge = styled(StyledBadge)` - && { - border-color: var(--color-success); - color: var(--color-success); - } -`; - -export const UnitTestResultBadge: FunctionComponent = ({ failed }) => failed ? ( - Failed -) : ( - Passed -); diff --git a/packages/insomnia/src/ui/components/list-group/unit-test-result-item.tsx b/packages/insomnia/src/ui/components/list-group/unit-test-result-item.tsx deleted file mode 100644 index 255d80bb2b..0000000000 --- a/packages/insomnia/src/ui/components/list-group/unit-test-result-item.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import styled from 'styled-components'; - -import { ListGroupItem } from './list-group-item'; -import { UnitTestResultBadge } from './unit-test-result-badge'; -import { UnitTestResultTimestamp } from './unit-test-result-timestamp'; - -export interface UnitTestResultItemProps { - item: { - duration: string; - err?: { - message: string; - }; - title: string; - }; -} - -const StyledResultListItem = styled(ListGroupItem)` - && { - > div:first-of-type { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - - > *:not(:first-child) { - margin: var(--padding-xs) 0 var(--padding-xs) var(--padding-sm); - } - } - - code { - background-color: var(--hl-xs); - padding: var(--padding-sm) var(--padding-md) var(--padding-sm) var(--padding-md); - color: var(--color-danger); - display: block; - margin: var(--padding-sm) 0 0 0; - } - - p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex-grow: 1; - } - } -`; - -export const UnitTestResultItem: FunctionComponent = ({ - item: { - err = {}, - title, - duration, - }, -}) => { - return ( - -
- -

{title}

- -
- {err.message && {err.message}} -
- ); -}; diff --git a/packages/insomnia/src/ui/components/list-group/unit-test-result-timestamp.tsx b/packages/insomnia/src/ui/components/list-group/unit-test-result-timestamp.tsx deleted file mode 100644 index 6efe9482e2..0000000000 --- a/packages/insomnia/src/ui/components/list-group/unit-test-result-timestamp.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import styled from 'styled-components'; - -import { SvgIcon } from '../svg-icon'; - -export interface UnitTestResultTimestampProps { - timeMs: String; -} - -const StyledTimestamp = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - flex-shrink: 0; - font-size: var(--font-size-xs); - color: var(--hl-xl); - - svg { - fill: var(--hl-xl); - margin-right: var(--padding-xxs); - } -`; - -export const UnitTestResultTimestamp: FunctionComponent = ({ timeMs }) => { - return ( - - {' '} - -
{timeMs} ms
-
- ); -}; diff --git a/packages/insomnia/src/ui/components/proto-file/proto-file-list.tsx b/packages/insomnia/src/ui/components/proto-file/proto-file-list.tsx index 82b8cf6e17..982216795e 100644 --- a/packages/insomnia/src/ui/components/proto-file/proto-file-list.tsx +++ b/packages/insomnia/src/ui/components/proto-file/proto-file-list.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; import { ProtoDirectory } from '../../../models/proto-directory'; import type { ProtoFile } from '../../../models/proto-file'; -import { ListGroup, ListGroupItem } from '../list-group'; import { Button } from '../themed-button'; export type SelectProtoFileHandler = (id: string) => void; @@ -11,7 +10,7 @@ export type DeleteProtoFileHandler = (protofile: ProtoFile) => void; export type DeleteProtoDirectoryHandler = (protoDirectory: ProtoDirectory) => void; export type UpdateProtoFileHandler = (protofile: ProtoFile) => Promise; export type RenameProtoFileHandler = (protoFile: ProtoFile, name?: string) => Promise; -export const ProtoListItem = styled(ListGroupItem).attrs(() => ({ +export const ProtoListItem = styled('li').attrs(() => ({ className: 'row-spaced', }))` button i.fa { @@ -42,10 +41,14 @@ const recursiveRender = ( handleUpdate: UpdateProtoFileHandler, handleDelete: DeleteProtoFileHandler, handleDeleteDirectory: DeleteProtoDirectoryHandler, - selectedId?: string, -): React.ReactNode => ([ + selectedId?: string +): React.ReactNode => [ dir && ( - + {dir.name} @@ -65,14 +68,12 @@ const recursiveRender = ( )} - ), + + ), ...files.map(f => ( handleSelect(f._id)} - indentLevel={indent + 1} > <> @@ -106,29 +107,34 @@ const recursiveRender = ( )), - ...subDirs.map(sd => recursiveRender( - indent + 1, - sd, - handleSelect, - handleUpdate, - handleDelete, - handleDeleteDirectory, - selectedId, - ))]); + ...subDirs.map(sd => + recursiveRender( + indent + 1, + sd, + handleSelect, + handleUpdate, + handleDelete, + handleDeleteDirectory, + selectedId + ) + ), +]; export const ProtoFileList: FunctionComponent = props => ( - +
    {!props.protoDirectories.length && ( - No proto files exist for this workspace +
  • No proto files exist for this workspace
  • )} - {props.protoDirectories.map(dir => recursiveRender( - 0, - dir, - props.handleSelect, - props.handleUpdate, - props.handleDelete, - props.handleDeleteDirectory, - props.selectedId - ))} - + {props.protoDirectories.map(dir => + recursiveRender( + 0, + dir, + props.handleSelect, + props.handleUpdate, + props.handleDelete, + props.handleDeleteDirectory, + props.selectedId + ) + )} +
); diff --git a/packages/insomnia/src/ui/routes/test-results.tsx b/packages/insomnia/src/ui/routes/test-results.tsx index dab77fe7ae..42b6b03577 100644 --- a/packages/insomnia/src/ui/routes/test-results.tsx +++ b/packages/insomnia/src/ui/routes/test-results.tsx @@ -1,11 +1,12 @@ import React, { FC } from 'react'; +import { Heading } from 'react-aria-components'; import { LoaderFunction, redirect, useRouteLoaderData } from 'react-router-dom'; import { database } from '../../common/database'; import * as models from '../../models'; import { UnitTestResult } from '../../models/unit-test-result'; import { invariant } from '../../utils/invariant'; -import { ListGroup, UnitTestResultItem } from '../components/list-group'; +import { Icon } from '../components/icon'; interface TestResultsData { testResult: UnitTestResult; @@ -43,29 +44,63 @@ export const loader: LoaderFunction = async ({ export const TestRunStatus: FC = () => { const { testResult } = useRouteLoaderData(':testResultId') as TestResultsData; + if (!testResult) { + return null; + } const { stats, tests } = testResult.results; + return ( -
- {testResult && ( -
-
- {stats.failures ? ( -

- Tests Failed {stats.failures}/{stats.tests} -

- ) : ( -

- Tests Passed {stats.passes}/{stats.tests} -

- )} -
- - {tests.map((t: any, i: number) => ( - - ))} - -
- )} +
+ 0 + ? 'text-[--color-danger]' + : 'text-[--color-success]' + }`} + > + 0 ? 'exclamation-triangle' : 'check-square'} + /> + + {stats.failures > 0 ? 'Tests failed' : 'Tests passed'}{' '} + + {stats.failures > 0 ? stats.failures : stats.passes}/{stats.tests} + +
+ {tests.map((test, i) => { + const errorMessage = 'message' in test.err ? test.err.message : ''; + return ( +
+
+
+ + {errorMessage ? 'Failed' : 'Passed'} + +
+
{test.title}
+
{test.duration} ms
+
+ {errorMessage && ( +
+ {errorMessage} +
+ )} +
+ ); + })} +
); }; diff --git a/packages/insomnia/src/ui/routes/test-suite.tsx b/packages/insomnia/src/ui/routes/test-suite.tsx index 9ed4290a69..56b62a8b3a 100644 --- a/packages/insomnia/src/ui/routes/test-suite.tsx +++ b/packages/insomnia/src/ui/routes/test-suite.tsx @@ -1,4 +1,13 @@ -import React, { useRef } from 'react'; +import React, { Fragment, useRef, useState } from 'react'; +import { + Button, + Heading, + Item, + ListBox, + Popover, + Select, + SelectValue, +} from 'react-aria-components'; import { LoaderFunction, redirect, @@ -6,7 +15,6 @@ import { useParams, useRouteLoaderData, } from 'react-router-dom'; -import styled from 'styled-components'; import { database } from '../../common/database'; import { documentationLinks } from '../../common/documentation'; @@ -15,34 +23,23 @@ import { isRequest, Request } from '../../models/request'; import { isUnitTest, UnitTest } from '../../models/unit-test'; import { UnitTestSuite } from '../../models/unit-test-suite'; import { invariant } from '../../utils/invariant'; -import { Editable } from '../components/base/editable'; -import { CodeEditor, CodeEditorHandle } from '../components/codemirror/code-editor'; -import { ListGroup, UnitTestItem } from '../components/list-group'; -import { showModal, showPrompt } from '../components/modals'; -import { SelectModal } from '../components/modals/select-modal'; -import { EmptyStatePane } from '../components/panes/empty-state-pane'; -import { SvgIcon } from '../components/svg-icon'; -import { Button } from '../components/themed-button'; -import { UnitTestEditable } from '../components/unit-test-editable'; - -const HeaderButton = styled(Button)({ - '&&': { - marginRight: 'var(--padding-md)', - }, -}); +import { + CodeEditor, + CodeEditorHandle, +} from '../components/codemirror/code-editor'; +import { EditableInput } from '../components/editable-input'; +import { Icon } from '../components/icon'; const UnitTestItemView = ({ unitTest, - testsRunning, }: { unitTest: UnitTest; testsRunning: boolean; }) => { const editorRef = useRef(null); - const { projectId, workspaceId, testSuiteId, organizationId } = useParams() as { + const { projectId, workspaceId, organizationId } = useParams() as { workspaceId: string; projectId: string; - testSuiteId: string; organizationId: string; }; const { unitTestSuite, requests } = useRouteLoaderData( @@ -70,55 +67,170 @@ const UnitTestItemView = ({ esversion: 8, // ES8 syntax (async/await, etc) }; + const [isOpen, setIsOpen] = useState(false); + return ( - - updateUnitTestFetcher.submit( - { - code: unitTest.code, - name: unitTest.name, - requestId: - event.currentTarget.value === '__NULL__' - ? '' - : event.currentTarget.value, - }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${unitTest._id}/update`, - method: 'post', - } - ) - } - onDeleteTest={() => - deleteUnitTestFetcher.submit( - {}, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test/${unitTest._id}/delete`, - method: 'post', - } - ) - } - onRunTest={() => - runTestFetcher.submit( - {}, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test/${unitTest._id}/run`, - method: 'post', - } - ) - } - testsRunning={testsRunning || runTestFetcher.state === 'submitting'} - selectedRequestId={unitTest.requestId} - selectableRequests={requests} - testNameEditable={ - - name && +
+
+ + + { + if (name) { + updateUnitTestFetcher.submit( + { + code: unitTest.code, + name, + requestId: unitTest.requestId || '', + }, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${unitTest._id}/update`, + method: 'POST', + } + ); + } + }} + value={unitTest.name} + /> + + + + + +
+ {isOpen && ( + { + const value = editorRef.current?.getValue() || ''; + const variables = value + .split('const ') + .filter(x => x) + .map(x => x.split(' ')[0]); + const numbers = variables + .map(x => parseInt(x.match(/(\d+)/)?.[0] || '')) + ?.filter(x => !isNaN(x)); + const highestNumberedConstant = Math.max(...numbers); + const variableName = 'response' + (highestNumberedConstant + 1); + return [ + { + name: 'Send: Current request', + displayValue: '', + value: + `const ${variableName} = await insomnia.send();\n` + + `expect(${variableName}.status).to.equal(200);`, + }, + ...requests.map(({ name, _id }) => ({ + name: `Send: ${name}`, + displayValue: '', + value: + `const ${variableName} = await insomnia.send('${_id}');\n` + + `expect(${variableName}.status).to.equal(200);`, + })), + ]; + }} + lintOptions={lintOptions} + onChange={code => + updateUnitTestFetcher.submit( + { + code, + name: unitTest.name, requestId: unitTest.requestId || '', }, { @@ -127,75 +239,11 @@ const UnitTestItemView = ({ } ) } - value={`${updateUnitTestFetcher.formData?.get('name') ?? ''}` || unitTest.name} + mode="javascript" + placeholder="" /> - } - > - { - const value = editorRef.current?.getValue() || ''; - const variables = value.split('const ').filter(x => x).map(x => x.split(' ')[0]); - const numbers = variables.map(x => parseInt(x.match(/(\d+)/)?.[0] || ''))?.filter(x => !isNaN(x)); - const highestNumberedConstant = Math.max(...numbers); - const variableName = 'response' + (highestNumberedConstant + 1); - return [ - { - name: 'Send Current Request', - displayValue: '', - value: `const ${variableName} = await insomnia.send();\n` + - `expect(${variableName}.status).to.equal(200);`, - }, - { - name: 'Send Request By ID', - displayValue: '', - value: async () => { - return new Promise(resolve => { - showModal(SelectModal, { - title: 'Select Request', - message: 'Select a request to fill', - value: '__NULL__', - options: [ - { - name: '-- Select Request --', - value: '__NULL__', - }, - ...requests.map(({ name, _id }) => ({ - name, - displayValue: '', - value: `const ${variableName} = await insomnia.send('${_id}');\n` + - `expect(${variableName}.status).to.equal(200);`, - })), - ], - onDone: (value: string | null) => resolve(value), - }); - }); - }, - }, - ]; - }} - lintOptions={lintOptions} - onChange={code => - updateUnitTestFetcher.submit( - { - code, - name: unitTest.name, - requestId: unitTest.requestId || '', - }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${unitTest._id}/update`, - method: 'post', - } - ) - } - mode="javascript" - placeholder="" - /> - + )} +
); }; @@ -207,7 +255,9 @@ export const indexLoader: LoaderFunction = async ({ params }) => { const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); if (workspaceMeta?.activeUnitTestSuiteId) { - const unitTestSuite = await models.unitTestSuite.getById(workspaceMeta.activeUnitTestSuiteId); + const unitTestSuite = await models.unitTestSuite.getById( + workspaceMeta.activeUnitTestSuiteId + ); if (unitTestSuite) { return redirect( @@ -230,7 +280,9 @@ interface LoaderData { unitTestSuite: UnitTestSuite; requests: Request[]; } -export const loader: LoaderFunction = async ({ params }): Promise => { +export const loader: LoaderFunction = async ({ + params, +}): Promise => { const { workspaceId, testSuiteId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -273,7 +325,9 @@ const TestSuiteRoute = () => { workspaceId: string; testSuiteId: string; }; - const { unitTestSuite, unitTests } = useRouteLoaderData(':testSuiteId') as LoaderData; + const { unitTestSuite, unitTests } = useRouteLoaderData( + ':testSuiteId' + ) as LoaderData; const createUnitTestFetcher = useFetcher(); const runAllTestsFetcher = useFetcher(); @@ -281,90 +335,109 @@ const TestSuiteRoute = () => { const testsRunning = runAllTestsFetcher.state === 'submitting'; - const testSuiteName = renameTestSuiteFetcher.formData?.get('name')?.toString() ?? unitTestSuite.name; + const testSuiteName = + renameTestSuiteFetcher.formData?.get('name')?.toString() ?? + unitTestSuite.name; return ( -
-
-

- name && renameTestSuiteFetcher.submit( - { name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/rename`, - method: 'post', - } - ) +
+
+ + + name && + renameTestSuiteFetcher.submit( + { name }, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/rename`, + method: 'POST', + } + ) } value={testSuiteName} /> -

- { - showPrompt({ - title: 'New Test', - defaultValue: 'Returns 200', - submitName: 'New Test', - label: 'Test Name', - selectText: true, - onComplete: name => { - createUnitTestFetcher.submit( - { - name, - }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/new`, - } - ); + + +
- {unitTests.length === 0 ? ( -
- } - documentationLinks={[ - documentationLinks.unitTesting, - documentationLinks.introductionToInsoCLI, - ]} - title="Add unit tests to verify your API" - secondaryAction="You can run these tests in CI with Inso CLI" - /> + {unitTests.length === 0 && ( +
+ + + Add unit tests to verify your API + +
+

+ + + You can run these tests in CI with Inso CLI + +

+ +
- ) : null} - - {unitTests.map(unitTest => ( - - ))} - + )} + {unitTests.length > 0 && ( +
    + {unitTests.map(unitTest => ( + + ))} +
+ )}
); }; diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index ff0b33859f..602f1cab20 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -1,5 +1,14 @@ -import classnames from 'classnames'; +import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { FC, Suspense } from 'react'; +import { + Button, + GridList, + Heading, + Item, + Menu, + MenuTrigger, + Popover, +} from 'react-aria-components'; import { LoaderFunction, Route, @@ -14,12 +23,11 @@ import { import * as models from '../../models'; import type { UnitTestSuite } from '../../models/unit-test-suite'; import { invariant } from '../../utils/invariant'; -import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../components/base/dropdown'; import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; +import { EditableInput } from '../components/editable-input'; import { ErrorBoundary } from '../components/error-boundary'; -import { showPrompt } from '../components/modals'; +import { Icon } from '../components/icon'; import { SidebarFooter, SidebarLayout } from '../components/sidebar-layout'; -import { Button } from '../components/themed-button'; import { TestRunStatus } from './test-results'; import TestSuiteRoute from './test-suite'; @@ -45,15 +53,17 @@ export const loader: LoaderFunction = async ({ const TestRoute: FC = () => { const { unitTestSuites } = useLoaderData() as LoaderData; - const { organizationId, projectId, workspaceId, testSuiteId } = useParams() as { - organizationId: string; - projectId: string; - workspaceId: string; - testSuiteId: string; - }; + const { organizationId, projectId, workspaceId, testSuiteId } = + useParams() as { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + }; const createUnitTestSuiteFetcher = useFetcher(); const deleteUnitTestSuiteFetcher = useFetcher(); + const renameTestSuiteFetcher = useFetcher(); const runAllTestsFetcher = useFetcher(); const runningTests = useFetchers() .filter( @@ -65,102 +75,149 @@ const TestRoute: FC = () => { const navigate = useNavigate(); + const testSuiteActionList: { + id: string; + name: string; + icon: IconName; + action: (suiteId: string) => void; + }[] = [ + { + id: 'run-tests', + name: 'Run tests', + icon: 'play', + action: suiteId => { + runAllTestsFetcher.submit( + {}, + { + method: 'POST', + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${suiteId}/run-all-tests`, + } + ); + }, + }, + { + id: 'delete-suite', + name: 'Delete suite', + icon: 'trash', + action: suiteId => { + deleteUnitTestSuiteFetcher.submit( + {}, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${suiteId}/delete`, + method: 'POST', + } + ); + }, + }, + ]; + return ( -
-
+
+
-
    - {unitTestSuites.map(suite => ( -
  • - - - - - - } - > - - { - runAllTestsFetcher.submit( - {}, +
    + + { + renameTestSuiteFetcher.submit( + { name }, { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${suite._id}/run-all-tests`, + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${item._id}/rename`, + method: 'POST', } ); }} /> - - - - deleteUnitTestSuiteFetcher.submit( - {}, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${suite._id}/delete`, - method: 'post', - } - ) - } - /> - - -
  • - ))} -
+ + + + + { + testSuiteActionList + .find(({ id }) => key === id) + ?.action(item._id); + }} + items={testSuiteActionList} + className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + > + {item => ( + + + {item.name} + + )} + + + +
+ + ); + }} +
@@ -180,9 +237,7 @@ const TestRoute: FC = () => { - No test suite selected -
+
No test suite selected
} /> @@ -193,11 +248,9 @@ const TestRoute: FC = () => { path="test-suite/:testSuiteId/test-result/:testResultId" element={ runningTests ? ( -
-
-

Running Tests...

-
-
+ + Running tests... + ) : ( ) @@ -206,19 +259,16 @@ const TestRoute: FC = () => { -
-

Running Tests...

-
-
- ) : ( -
-
-

No Results

-
-
- ) + + {runningTests ? ( + <> + Running + tests... + + ) : ( + 'No test results' + )} + } />