POC for Unit Test UI (#2315)

* Start working on insomnia-testing package to generate Mocha code

* Moved some things around

* Integration tests and ability to run multiple files

* Fix integration tests

* Rename runSuite to runMochaTests

* Add types for test results

* Fix lint errors

* Play with Chia assertions

* insomnia and chai globals, plus a bunch of improvements

* Stub chai and axios Flow types

* Ability to reference requests by ID

* Fix chai flow-typed location

* Address PR comments (small tweaks)

* Basic UI going

* Lots and lots and lots

* Pretty-much done with the unit test UI

* Minor CSS tweak

* Activity bar triple toggle and more

* Minor tweak

* Unit Test and Suite deletion

* Bump Designer version

* Fix eslint stuff

* Fix insomnia-testing tests

* Fix tests

* lib@2.2.9

* Remove tests tab from response pane

* Hook up Insomnia networking

* Fix linting

* Bump version for alpha

* Remove extra ActivityToggleSwitch

* Remove unused import

* Add test:pre-release script that excludes CLI tests

* Less repetition

* Clean some things up

* Tweaks after self-cr

* Undo request ID tag

* Swap out switch for new activity toggle component

* Extra check

* Remove dead code

* Delete dead test

* Oops, revert example code

* PR feedback

* More PR comment addresses
This commit is contained in:
Gregory Schier authored and GitHub committed 2020-07-01 14:17:14 -07:00
1 parent 48348e04c1
commit 3883dc6feb
30 files changed
+1086 -88

No files matched your search

@@ -14,6 +14,9 @@ import * as _requestVersion from './request-version';
import * as _response from './response';
import * as _settings from './settings';
import * as _stats from './stats';
import * as _unitTest from './unit-test';
import * as _unitTestResult from './unit-test-result';
import * as _unitTestSuite from './unit-test-suite';
import * as _workspace from './workspace';
import * as _workspaceMeta from './workspace-meta';
import { generateId } from '../common/misc';
@@ -42,6 +45,9 @@ export const requestVersion = _requestVersion;
export const response = _response;
export const settings = _settings;
export const stats = _stats;
export const unitTest = _unitTest;
export const unitTestSuite = _unitTestSuite;
export const unitTestResult = _unitTestResult;
export const workspace = _workspace;
export const workspaceMeta = _workspaceMeta;
@@ -64,6 +70,9 @@ export function all() {
oAuth2Token,
clientCertificate,
pluginData,
unitTestSuite,
unitTestResult,
unitTest,
];
}
@@ -57,6 +57,7 @@ type BaseSettings = {
// Feature flags
enableSyncBeta: boolean,
enableUnitTestingBeta: boolean,
};
export type Settings = BaseModel & BaseSettings;
@@ -112,6 +113,7 @@ export function init(): BaseSettings {
// Feature flags
enableSyncBeta: false,
enableUnitTestingBeta: false,
};
}
@@ -0,0 +1,45 @@
// @flow
import * as db from '../common/database';
import type { BaseModel } from './index';
export const name = 'Unit Test Result';
export const type = 'UnitTestResult';
export const prefix = 'utr';
export const canDuplicate = false;
export const canSync = false;
type BaseUnitTestResult = {
results: Object,
};
export type UnitTestResult = BaseModel & BaseUnitTestResult;
export function init() {
return {
results: null,
};
}
export function migrate(doc: UnitTestResult): UnitTestResult {
return doc;
}
export function create(patch: $Shape<UnitTestResult> = {}) {
if (!patch.parentId) {
throw new Error('New UnitTestResult missing `parentId` ' + JSON.stringify(patch));
}
return db.docCreate(type, patch);
}
export function update(unitTest: UnitTestResult, patch: $Shape<UnitTestResult>) {
return db.docUpdate(unitTest, patch);
}
export function getByParentId(parentId: string) {
return db.getWhere(type, { parentId });
}
export function all(): Promise<Array<UnitTestResult>> {
return db.all(type);
}
@@ -0,0 +1,52 @@
// @flow
import * as db from '../common/database';
import type { BaseModel } from './index';
export const name = 'Unit Test Suite';
export const type = 'UnitTestSuite';
export const prefix = 'uts';
export const canDuplicate = true;
export const canSync = true;
type BaseUnitTestSuite = {
name: string,
};
export type UnitTestSuite = BaseModel & BaseUnitTestSuite;
export function init() {
return {
name: 'My Test',
};
}
export function migrate(doc: UnitTestSuite): UnitTestSuite {
return doc;
}
export function create(patch: $Shape<UnitTestSuite> = {}) {
if (!patch.parentId) {
throw new Error('New UnitTestSuite missing `parentId` ' + JSON.stringify(patch));
}
return db.docCreate(type, patch);
}
export function update(
unitTestSuite: UnitTestSuite,
patch: $Shape<UnitTestSuite> = {},
): Promise<UnitTestSuite> {
return db.docUpdate(unitTestSuite, patch);
}
export function remove(unitTestSuite: UnitTestSuite): Promise<void> {
return db.remove(unitTestSuite);
}
export function getByParentId(parentId: string): Promise<UnitTestSuite | null> {
return db.getWhere(type, { parentId });
}
export function all(): Promise<Array<UnitTestSuite>> {
return db.all(type);
}
@@ -0,0 +1,53 @@
// @flow
import * as db from '../common/database';
import type { BaseModel } from './index';
export const name = 'Unit Test';
export const type = 'UnitTest';
export const prefix = 'ut';
export const canDuplicate = true;
export const canSync = true;
type BaseUnitTest = {
name: string,
code: string,
requestId: string | null,
};
export type UnitTest = BaseModel & BaseUnitTest;
export function init() {
return {
requestId: null,
name: 'My Test',
code: '',
};
}
export function migrate(doc: UnitTest): UnitTest {
return doc;
}
export function create(patch: $Shape<UnitTest> = {}): Promise<UnitTest> {
if (!patch.parentId) {
throw new Error('New UnitTest missing `parentId` ' + JSON.stringify(patch));
}
return db.docCreate(type, patch);
}
export function remove(unitTest: UnitTest): Promise<void> {
return db.remove(unitTest);
}
export function update(unitTest: UnitTest, patch: $Shape<UnitTest> = {}): Promise<UnitTest> {
return db.docUpdate(unitTest, patch);
}
export function getByParentId(parentId: string): Promise<UnitTest | null> {
return db.getWhere(type, { parentId });
}
export function all(): Promise<Array<UnitTest>> {
return db.all(type);
}
@@ -14,41 +14,43 @@ export const canDuplicate = false;
export const canSync = false;
type BaseWorkspaceMeta = {
activeRequestId: string | null,
activeEnvironmentId: string | null,
activeActivity: string | null,
sidebarFilter: string,
sidebarHidden: boolean,
previewHidden: boolean,
sidebarWidth: number,
paneWidth: number,
paneHeight: number,
hasSeen: boolean,
gitRepositoryId: string | null,
cachedGitRepositoryBranch: string | null,
activeEnvironmentId: string | null,
activeRequestId: string | null,
activeUnitTestSuiteId: string | null,
cachedGitLastAuthor: string | null,
cachedGitLastCommitTime: number | null,
cachedGitRepositoryBranch: string | null,
gitRepositoryId: string | null,
hasSeen: boolean,
paneHeight: number,
paneWidth: number,
previewHidden: boolean,
sidebarFilter: string,
sidebarHidden: boolean,
sidebarWidth: number,
};
export type WorkspaceMeta = BaseWorkspaceMeta & BaseModel;
export function init(): BaseWorkspaceMeta {
return {
parentId: null,
activeRequestId: null,
activeEnvironmentId: null,
activeActivity: null,
sidebarFilter: '',
sidebarHidden: false,
previewHidden: false,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
paneWidth: DEFAULT_PANE_WIDTH,
paneHeight: DEFAULT_PANE_HEIGHT,
hasSeen: true,
gitRepositoryId: null,
cachedGitRepositoryBranch: null,
activeEnvironmentId: null,
activeRequestId: null,
activeUnitTestSuiteId: null,
cachedGitLastAuthor: null,
cachedGitLastCommitTime: null,
cachedGitRepositoryBranch: null,
gitRepositoryId: null,
hasSeen: true,
paneHeight: DEFAULT_PANE_HEIGHT,
paneWidth: DEFAULT_PANE_WIDTH,
parentId: null,
previewHidden: false,
sidebarFilter: '',
sidebarHidden: false,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
};
}
@@ -24,6 +24,8 @@ const WHITE_LIST = {
[models.request.type]: true,
[models.requestGroup.type]: true,
[models.environment.type]: true,
[models.unitTest.type]: true,
[models.unitTestSuite.type]: true,
// These can be overridden in sync config
[models.cookieJar.type]: true,
@@ -39,6 +39,8 @@ describe.each(['win32', 'posix'])('NeDBPlugin using path.%s', type => {
models.environment.type,
models.request.type,
models.requestGroup.type,
models.unitTest.type,
models.unitTestSuite.type,
models.workspace.type,
]);
@@ -125,6 +125,8 @@ export default class NeDBPlugin {
models.requestGroup.type,
models.request.type,
models.apiSpec.type,
models.unitTestSuite.type,
models.unitTest.type,
];
} else if (type !== null && id === null) {
const workspace = await db.get(models.workspace.type, this._workspaceId);
@@ -54,6 +54,7 @@ import './modes/nunjucks';
import './modes/curl';
import './modes/openapi';
import './lint/openapi';
import './lint/javascript-async-lint';
import './extensions/autocomplete';
import './extensions/clickable';
import './extensions/nunjucks-tags';
@@ -62,6 +63,6 @@ import './extensions/nunjucks-tags';
import 'codemirror/lib/codemirror.css';
import '../../css/editor/index.less';
// Make jsonlint available to the jsonlint plugin
// Make jsonlint available to the jsonlint addon
import { parser as jsonlint } from 'jsonlint';
global.jsonlint = jsonlint;
@@ -0,0 +1,72 @@
// This linter is a fork of Codemirror's built-in javascript linter that enables top-level
// await usage. It does this by wrapping the text in an async function, then offsetting any
// lint errors to account for the wrapper.
//
// The major modifications are marked in comments starting with "CHANGED ---"
import CodeMirror from 'codemirror';
import { JSHINT } from 'jshint';
CodeMirror.registerHelper('lint', 'javascript', validator);
function validator(text, options) {
if (!options.indent) {
// JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
options.indent = 1;
} // JSHint default value is 4
// CHANGED ----------------------------------------------------------- //
// Wrap text with async function to "enable" top-level await
const textWithWrapper = `async function asyncWrapper() {\n${text}\n}`;
JSHINT(textWithWrapper, options, options.globals);
const errors = JSHINT.data().errors;
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
function parseErrors(errors, output) {
for (let i = 0; i < errors.length; i++) {
const error = errors[i];
if (error) {
if (error.line <= 0) {
if (window.console) {
window.console.warn(
'Cannot display JSHint error (invalid line ' + error.line + ')',
error,
);
}
continue;
}
const start = error.character - 1;
let end = start + 1;
if (error.evidence) {
const index = error.evidence.substring(start).search(/.\b/);
if (index > -1) {
end += index;
}
}
// Convert to format expected by validation service
const hint = {
message: error.reason,
severity: error.code ? (error.code.startsWith('W') ? 'warning' : 'error') : 'error',
from: CodeMirror.Pos(error.line - 1, start),
to: CodeMirror.Pos(error.line - 1, end),
};
// CHANGED ----------------------------------------------------------- //
// Offset an extra line to account for async wrapper function
hint.from.line -= 1;
hint.to.line -= 1;
output.push(hint);
}
}
}
@@ -107,7 +107,6 @@ class PageLayout extends React.PureComponent<Props, State> {
enableSyncBeta={settings.enableSyncBeta}
environmentHighlightColorStyle={settings.environmentHighlightColorStyle}
handleInitializeEntities={handleInitializeEntities}
handleNavigateHome={this._handleSetHomeActivity}
handleSetActiveEnvironment={handleSetActiveEnvironment}
handleSetActiveWorkspace={handleSetActiveWorkspace}
hidden={sidebarHidden || false}
@@ -8,7 +8,6 @@ import type {
} from '../../models/request';
import type { Workspace } from '../../models/workspace';
import type { OAuth2Token } from '../../models/o-auth-2-token';
import autobind from 'autobind-decorator';
import { deconstructQueryStringToParams, extractQueryStringFromUrl } from 'insomnia-url';
import * as React from 'react';
@@ -31,6 +31,7 @@ import type { RequestVersion } from '../../models/request-version';
import { showError } from '../components/modals/index';
import { json as jsonPrettify } from 'insomnia-prettify';
import type { Environment } from '../../models/environment';
import type { UnitTestResult } from '../../models/unit-test-result';
type Props = {
// Functions
@@ -61,6 +62,7 @@ type Props = {
request: ?Request,
response: ?Response,
environment: ?Environment,
unitTestResult: ?UnitTestResult,
};
@autobind
@@ -4,11 +4,13 @@ import * as fontScanner from 'font-manager';
import * as electron from 'electron';
import autobind from 'autobind-decorator';
import HelpTooltip from '../help-tooltip';
import type { HttpVersion } from '../../../common/constants';
import {
EDITOR_KEY_MAP_DEFAULT,
EDITOR_KEY_MAP_EMACS,
EDITOR_KEY_MAP_SUBLIME,
EDITOR_KEY_MAP_VIM,
getAppId,
HttpVersions,
isLinux,
isMac,
@@ -21,7 +23,7 @@ import { setFont } from '../../../plugins/misc';
import * as session from '../../../account/session';
import Tooltip from '../tooltip';
import CheckForUpdatesButton from '../check-for-updates-button';
import type { HttpVersion } from '../../../common/constants';
import { APP_ID_DESIGNER } from '../../../../config';
// Font family regex to match certain monospace fonts that don't get
// recognized as monospace
@@ -479,13 +481,6 @@ class General extends React.PureComponent<Props, State> {
<br />
{session.isLoggedIn() && (
<React.Fragment>
<hr />
{this.renderBooleanSetting('Enable version control beta', 'enableSyncBeta', '', true)}
</React.Fragment>
)}
<hr className="pad-top" />
<h2>Data Sharing</h2>
<div className="form-control form-control--thin">
@@ -508,6 +503,25 @@ class General extends React.PureComponent<Props, State> {
as request data, names, etc.
</p>
</div>
{session.isLoggedIn() && (
<React.Fragment>
<hr />
{this.renderBooleanSetting('Enable version control beta', 'enableSyncBeta', '', true)}
</React.Fragment>
)}
{getAppId() === APP_ID_DESIGNER && (
<>
<hr />
{this.renderBooleanSetting(
'Enable unit testing beta',
'enableUnitTestingBeta',
'',
false,
)}
</>
)}
</div>
);
}
@@ -16,7 +16,6 @@ type Props = {|
children: React.Node,
enableSyncBeta: boolean,
environmentHighlightColorStyle: string,
handleNavigateHome: () => any,
handleSetActiveEnvironment: Function,
handleSetActiveWorkspace: Function,
hidden: boolean,
@@ -120,6 +120,8 @@ class WrapperDebug extends React.PureComponent<Props> {
activeRequest,
activeRequestResponses,
activeResponse,
activeUnitTest,
activeUnitTestResult,
activeWorkspace,
environments,
enableSyncBeta,
@@ -265,6 +267,7 @@ class WrapperDebug extends React.PureComponent<Props> {
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
oAuth2Token={oAuth2Token}
request={activeRequest}
unitTest={activeUnitTest}
settings={settings}
updateRequestAuthentication={handleUpdateRequestAuthentication}
updateRequestBody={handleUpdateRequestBody}
@@ -320,6 +323,7 @@ class WrapperDebug extends React.PureComponent<Props> {
requestVersions={requestVersions}
response={activeResponse}
responses={activeRequestResponses}
unitTestResult={activeUnitTestResult}
/>
</ErrorBoundary>
</React.Fragment>
@@ -19,9 +19,8 @@ import generateConfigIcon from '../images/icn-gear.svg';
import * as models from '../../models/index';
import { parseApiSpec } from '../../common/api-specs';
import { getConfigGenerators } from '../../plugins';
import AlertModal from './modals/alert-modal';
import { ACTIVITY_HOME } from '../../common/constants';
import type { GlobalActivity } from '../../common/constants';
import { ACTIVITY_HOME } from '../../common/constants';
import ActivityToggle from './activity-toggle';
const spectral = new Spectral();
@@ -73,32 +72,6 @@ class WrapperDesign extends React.PureComponent<Props, State> {
showModal(GenerateConfigModal, { apiSpec: activeApiSpec });
}
async _handleDebugSpec(errors, e): Pomise<void> {
e.preventDefault();
if (errors) {
showModal(AlertModal, {
title: 'Error Generating Configuration',
message:
'Some requests may not be available due to errors found in the specification. We recommend fixing errors before proceeding. 🤗',
okLabel: 'Proceed',
addCancel: true,
onConfirm: async () => {
const {
handleSetDebugActivity,
wrapperProps: { activeApiSpec },
} = this.props;
await handleSetDebugActivity(activeApiSpec);
},
});
} else {
const {
handleSetDebugActivity,
wrapperProps: { activeApiSpec },
} = this.props;
await handleSetDebugActivity(activeApiSpec);
}
}
async _handleTogglePreview() {
await this.setState(
prevState => ({ previewHidden: !prevState.previewHidden }),
@@ -0,0 +1,482 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import PageLayout from './page-layout';
import { Breadcrumb, Button, Dropdown, DropdownItem, Header, SvgIcon } from 'insomnia-components';
import ErrorBoundary from './error-boundary';
import CodeEditor from './codemirror/code-editor';
import designerLogo from '../images/insomnia-designer-logo.svg';
import type { WrapperProps } from './wrapper';
import * as models from '../../models';
import type { UnitTest } from '../../models/unit-test';
import { generate, runTests } from 'insomnia-testing';
import { showAlert, showModal, showPrompt } from './modals';
import Editable from './base/editable';
import type { SidebarChildObjects } from './sidebar/sidebar-children';
import SelectModal from './modals/select-modal';
import type { UnitTestSuite } from '../../models/unit-test-suite';
import ActivityToggle from './activity-toggle';
import { getSendRequestCallback } from '../../common/send-request';
import type { GlobalActivity } from '../../common/constants';
import { ACTIVITY_HOME } from '../../common/constants';
type Props = {|
children: SidebarChildObjects,
gitSyncDropdown: React.Node,
handleActivityChange: (workspaceId: string, activity: GlobalActivity) => Promise<void>,
wrapperProps: WrapperProps,
|};
type State = {|
testsRunning: Array<UnitTest> | null,
|};
@autobind
class WrapperUnitTest extends React.PureComponent<Props, State> {
state = {
testsRunning: null,
};
// Defining it here instead of in render() so it won't act as a changed prop
// when being passed to <CodeEditor> again
static lintOptions = {
globals: {
// https://jshint.com/docs/options/
insomnia: true,
expect: true,
chai: true,
debugger: true,
},
asi: true, // Don't require semicolons
undef: true, // Prevent undefined usages
node: true, // Enable NodeJS globals
esversion: 8, // ES8 syntax (async/await, etc)
};
generateSendReqSnippet(existingCode: string, requestId: string): string {
let variableName = 'response';
for (let i = 1; i < 100; i++) {
variableName = `response${i}`;
// Try next one if code already contains this variable
if (existingCode.includes(`const ${variableName} =`)) {
continue;
}
// Found variable that doesn't exist in code yet
break;
}
return (
`const ${variableName} = await insomnia.send(${requestId});\n` +
`expect(${variableName}.status).to.equal(200);`
);
}
autocompleteSnippets(unitTest: UnitTest): Array<{ name: string, value: () => Promise<string> }> {
return [
{
name: 'Send Current Request',
displayValue: '',
value: this.generateSendReqSnippet(unitTest.code, ''),
},
{
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__' },
...this.buildSelectableRequests().map(({ name, request }) => ({
name: name,
displayValue: '',
value: this.generateSendReqSnippet(unitTest.code, `'${request._id}'`),
})),
],
onDone: v => resolve(v),
});
});
},
},
];
}
async _handleCreateTestSuite(): Promise<void> {
const { activeWorkspace } = this.props.wrapperProps;
showPrompt({
title: 'New Test Suite',
defaultValue: 'New Suite',
submitName: 'Create Suite',
label: 'Test Suite Name',
selectText: true,
onComplete: async name => {
const unitTestSuite = await models.unitTestSuite.create({
parentId: activeWorkspace._id,
name,
});
await this._handleSetActiveUnitTestSuite(unitTestSuite);
},
});
}
async _handleCreateTest(): Promise<void> {
const { activeUnitTestSuite } = this.props.wrapperProps;
showPrompt({
title: 'New Test',
defaultValue: 'Returns 200',
submitName: 'Create Test',
label: 'Test Name',
selectText: true,
onComplete: async name => {
await models.unitTest.create({
parentId: activeUnitTestSuite._id,
code: this.generateSendReqSnippet('', ''),
name,
});
},
});
}
async _handleUnitTestCodeChange(unitTest: UnitTest, v: string): Promise<void> {
await models.unitTest.update(unitTest, { code: v });
}
async _handleBreadcrumb(): void {
const {
handleActivityChange,
wrapperProps: { activeWorkspace },
} = this.props;
await handleActivityChange(activeWorkspace._id, ACTIVITY_HOME);
}
async _handleRunTests(): Promise<void> {
const { activeUnitTests } = this.props.wrapperProps;
await this._runTests(activeUnitTests);
}
async _handleRunTest(unitTest: UnitTest): Promise<void> {
await this._runTests([unitTest]);
}
async _handleDeleteTest(unitTest: UnitTest): Promise<void> {
await models.unitTest.remove(unitTest);
}
async _handleSetActiveRequest(
unitTest: UnitTest,
e: SyntheticEvent<HTMLSelectElement>,
): Promise<void> {
const requestId = e.currentTarget.value === '__NULL__' ? null : e.currentTarget.value;
await models.unitTest.update(unitTest, { requestId });
}
async _handleDeleteUnitTestSuite(unitTestSuite: UnitTestSuite): Promise<void> {
showAlert({
title: `Delete ${unitTestSuite.name}`,
message: (
<span>
Really delete <strong>{unitTestSuite.name}</strong>?
</span>
),
addCancel: true,
onConfirm: async () => {
await models.unitTestSuite.remove(unitTestSuite);
},
});
}
async _handleSetActiveUnitTestSuite(unitTestSuite: UnitTestSuite): Promise<void> {
const { activeWorkspace } = this.props.wrapperProps;
await models.workspaceMeta.updateByParentId(activeWorkspace._id, {
activeUnitTestSuiteId: unitTestSuite._id,
});
}
async _handleChangeTestName(unitTest: UnitTest, name: string): Promise<void> {
await models.unitTest.update(unitTest, { name });
}
async _runTests(unitTests: Array<UnitTest>): Promise<void> {
const { requests, activeWorkspace, activeEnvironment } = this.props.wrapperProps;
this.setState({ testsRunning: unitTests });
const tests = [];
for (const t of unitTests) {
tests.push({
name: t.name,
code: t.code,
defaultRequestId: t.requestId,
});
}
const src = await generate([{ name: 'My Suite', suites: [], tests }]);
const results = await runTests(src, {
requests,
sendRequest: getSendRequestCallback(activeEnvironment ? activeEnvironment._id : null),
});
await models.unitTestResult.create({ results, parentId: activeWorkspace._id });
this.setState({ testsRunning: null });
}
buildSelectableRequests(): Array<{ name: string, request: Request }> {
const { children } = this.props;
const selectableRequests: Array<{ name: string, request: Request }> = [];
const next = (p, children) => {
for (const c of children) {
if (c.doc.type === models.request.type) {
selectableRequests.push({
name: `${p} [${c.doc.method}] ${c.doc.name}`,
request: c.doc,
});
} else if (c.doc.type === models.requestGroup.type) {
next(c.doc.name + ' / ', c.children);
}
}
};
next('', children.all);
return selectableRequests;
}
renderResults(): React.Node {
const { activeUnitTestResult } = this.props.wrapperProps;
const { testsRunning } = this.state;
if (testsRunning) {
return (
<div className="unit-tests__results">
<h2>Running {testsRunning.length} Tests...</h2>
</div>
);
}
if (!activeUnitTestResult) {
return null;
}
const { stats, tests } = activeUnitTestResult.results;
return (
<div className="unit-tests__results">
{activeUnitTestResult && (
<div key={activeUnitTestResult._id}>
{stats.failures ? (
<h2 className="warning">
Tests Failed {stats.failures}/{stats.tests}
</h2>
) : (
<h2 className="success">
Tests Passed {stats.passes}/{stats.tests}
</h2>
)}
<ul>
{tests.map((t, i) => (
<li key={i}>
<SvgIcon icon={t.err.message ? 'error' : 'success'} /> {t.fullTitle} ({t.duration}{' '}
ms)
{t.err.message && (
<>
<br />
<code className="text-danger">{t.err.message}</code>
</>
)}
</li>
))}
</ul>
</div>
)}
</div>
);
}
renderUnitTest(unitTest: UnitTest): React.Node {
const { settings } = this.props.wrapperProps;
const { testsRunning } = this.state;
const selectableRequests = this.buildSelectableRequests();
return (
<div key={unitTest._id} className="unit-tests__tests__block">
<div className="unit-tests__tests__block__header">
<h2 className="pad-left-md">
<Editable
singleClick
onSubmit={this._handleChangeTestName.bind(this, unitTest)}
value={unitTest.name}
/>
</h2>
<div className="form-control form-control--outlined">
<select
name="request"
id="request"
onChange={this._handleSetActiveRequest.bind(this, unitTest)}
value={unitTest.requestId || '__NULL__'}>
<option value="__NULL__">
{selectableRequests.length ? '-- Select Request --' : '-- No Requests --'}
</option>
{selectableRequests.map(({ name, request }) => (
<option key={request._id} value={request._id}>
{name}
</option>
))}
</select>
</div>
<Dropdown
renderButton={() => (
<Button variant="outlined">
<SvgIcon icon="gear" />
</Button>
)}>
<DropdownItem
disabled={testsRunning && testsRunning.find(t => t._id === unitTest._id)}
onClick={() => this._handleRunTest(unitTest)}>
Run Test
</DropdownItem>
<DropdownItem onClick={() => this._handleDeleteTest(unitTest)}>Delete</DropdownItem>
</Dropdown>
</div>
<CodeEditor
dynamicHeight
manualPrettify
fontSize={settings.editorFontSize}
indentSize={settings.editorIndentSize}
indentWithTabs={settings.editorIndentWithTabs}
keyMap={settings.editorKeyMap}
defaultValue={unitTest ? unitTest.code : ''}
getAutocompleteSnippets={() => this.autocompleteSnippets(unitTest)}
lintOptions={WrapperUnitTest.lintOptions}
onChange={this._handleUnitTestCodeChange.bind(this, unitTest)}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
isVariableUncovered={settings.isVariableUncovered}
mode="javascript"
lineWrapping={settings.editorLineWrapping}
placeholder=""
/>
</div>
);
}
renderPageBody(): React.Node {
const { activeUnitTests, activeUnitTestSuite } = this.props.wrapperProps;
if (!activeUnitTestSuite) {
return (
<div className="unit-tests layout-body--sidebar theme--pane">
<div className="unit-tests__tests theme--pane__body">No test suite selected</div>
</div>
);
}
return (
<div className="unit-tests layout-body--sidebar theme--pane">
<div className="unit-tests__tests theme--pane__body">
{activeUnitTests.map(this.renderUnitTest)}
<div className="pad">
<Button variant="contained" bg="surprise" onClick={this._handleCreateTest}>
Create Test
</Button>
</div>
</div>
{this.renderResults()}
</div>
);
}
renderPageSidebar(): React.Node {
const { activeUnitTestSuites, activeUnitTestSuite } = this.props.wrapperProps;
const activeId = activeUnitTestSuite ? activeUnitTestSuite._id : 'n/a';
return (
<ErrorBoundary showAlert>
<div className="unit-tests__sidebar">
<div className="pad-sm">
<Button variant="outlined" bg="surprise" onClick={this._handleCreateTestSuite}>
New Test Suite
</Button>
</div>
<ul>
{activeUnitTestSuites.map(s => (
<li key={s._id} className={classnames({ active: s._id === activeId })}>
<button key={s._id} onClick={this._handleSetActiveUnitTestSuite.bind(this, s)}>
{s.name}
</button>
<Dropdown
right
renderButton={() => (
<button className="unit-tests__sidebar__action">
<i className="fa fa-caret-down" />
</button>
)}>
<DropdownItem onClick={this._handleDeleteUnitTestSuite.bind(this, s)}>
Delete Suite
</DropdownItem>
</Dropdown>
</li>
))}
</ul>
</div>
</ErrorBoundary>
);
}
render() {
const { handleActivityChange, gitSyncDropdown } = this.props;
const { activeWorkspace, activity, settings, activeApiSpec } = this.props.wrapperProps;
const { testsRunning } = this.state;
return (
<PageLayout
wrapperProps={this.props.wrapperProps}
renderPageSidebar={this.renderPageSidebar}
renderPageBody={this.renderPageBody}
renderPageHeader={() => (
<Header
className="app-header"
gridLeft={
<React.Fragment>
<img src={designerLogo} alt="Insomnia" width="32" height="32" />
<Breadcrumb
className="breadcrumb"
crumbs={['Documents', activeApiSpec.fileName]}
onClick={this._handleBreadcrumb}
/>
</React.Fragment>
}
gridCenter={
<ActivityToggle
activity={activity}
handleActivityChange={handleActivityChange}
settings={settings}
workspace={activeWorkspace}
/>
}
gridRight={
<>
<Button
variant="contained"
onClick={this._handleRunTests}
size="default"
disabled={testsRunning}>
{testsRunning ? 'Running... ' : 'Run Tests'}
</Button>
{gitSyncDropdown}
</>
}
/>
)}
/>
);
}
}
export default WrapperUnitTest;
@@ -12,7 +12,6 @@ import type {
RequestParameter,
} from '../../models/request';
import type { SidebarChildObjects } from './sidebar/sidebar-children';
import * as React from 'react';
import autobind from 'autobind-decorator';
import { registerModal, showModal } from './modals/index';
@@ -71,21 +70,26 @@ import { trackPageView } from '../../common/analytics';
import type { GitRepository } from '../../models/git-repository';
import WrapperHome from './wrapper-home';
import WrapperDesign from './wrapper-design';
import WrapperUnitTest from './wrapper-unit-test';
import WrapperOnboarding from './wrapper-onboarding';
import WrapperDebug from './wrapper-debug';
import { importRaw } from '../../common/import';
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
import { DropdownButton } from './base/dropdown';
import type { ForceToWorkspace } from '../redux/modules/helpers';
import type { UnitTest } from '../../models/unit-test';
import type { UnitTestResult } from '../../models/unit-test-result';
import type { UnitTestSuite } from '../../models/unit-test-suite';
import type { GlobalActivity } from '../../common/constants';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
ACTIVITY_INSOMNIA,
ACTIVITY_SPEC,
ACTIVITY_UNIT_TEST,
getAppName,
} from '../../common/constants';
import { Spectral } from '@stoplight/spectral';
import type { GlobalActivity } from '../../common/constants';
const spectral = new Spectral();
@@ -177,9 +181,12 @@ export type WrapperProps = {
activeCookieJar: CookieJar,
activeEnvironment: Environment | null,
activeGitRepository: GitRepository | null,
activeUnitTestResult: UnitTestResult | null,
activeUnitTestSuites: Array<UnitTestSuite>,
activeUnitTests: Array<UnitTest>,
activeWorkspaceClientCertificates: Array<ClientCertificate>,
isVariableUncovered: boolean,
headerEditorKey: string,
isVariableUncovered: boolean,
vcs: VCS | null,
gitVCS: GitVCS | null,
gitRepositories: Array<GitRepository>,
@@ -305,7 +312,8 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
await models.workspaceMeta.updateByParentId(workspaceId, { activeActivity: nextActivity });
}
if (activity !== ACTIVITY_SPEC) {
const notEditingASpec = activity !== ACTIVITY_SPEC;
if (notEditingASpec) {
handleSetActiveActivity(nextActivity);
return;
}
@@ -350,12 +358,6 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
this.props.handleImportFileToWorkspace(this.props.activeWorkspace._id, forceToWorkspace);
}
_handleUpdateSettingsUseBulkParametersEditor(
useBulkParametersEditor: boolean,
): Promise<Settings> {
return sUpdate(this.props.settings, { useBulkParametersEditor });
}
_handleImportUri(uri: string, forceToWorkspace?: ForceToWorkspace): void {
this.props.handleImportUriToWorkspace(this.props.activeWorkspace._id, uri, forceToWorkspace);
}
@@ -783,6 +785,15 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
/>
)}
{activity === ACTIVITY_UNIT_TEST && (
<WrapperUnitTest
gitSyncDropdown={gitSyncDropdown}
wrapperProps={this.props}
handleActivityChange={this._handleWorkspaceActivityChange}
children={sidebarChildren}
/>
)}
{(activity === ACTIVITY_DEBUG || activity === ACTIVITY_INSOMNIA) && (
<WrapperDebug
forceRefreshKey={this.state.forceRefreshKey}
+22 -8
View File
@@ -15,6 +15,8 @@ import CookiesModal from '../components/modals/cookies-modal';
import RequestSwitcherModal from '../components/modals/request-switcher-modal';
import SettingsModal, { TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
import {
ACTIVITY_HOME,
ACTIVITY_INSOMNIA,
COLLAPSE_SIDEBAR_REMS,
DEFAULT_PANE_HEIGHT,
DEFAULT_PANE_WIDTH,
@@ -26,10 +28,8 @@ import {
MIN_PANE_WIDTH,
MIN_SIDEBAR_REMS,
PREVIEW_MODE_SOURCE,
getAppName,
getAppId,
ACTIVITY_INSOMNIA,
ACTIVITY_HOME,
getAppName,
} from '../../common/constants';
import * as globalActions from '../redux/modules/global';
import * as entitiesActions from '../redux/modules/entities';
@@ -43,6 +43,10 @@ import {
selectActiveRequestMeta,
selectActiveRequestResponses,
selectActiveResponse,
selectActiveUnitTestResult,
selectActiveUnitTests,
selectActiveUnitTestSuite,
selectActiveUnitTestSuites,
selectActiveWorkspace,
selectActiveWorkspaceClientCertificates,
selectActiveWorkspaceMeta,
@@ -1363,15 +1367,15 @@ function mapStateToProps(state, props) {
// Entities
const entitiesLists = selectEntitiesLists(state, props);
const {
workspaces,
workspaceMetas,
apiSpecs,
environments,
requests,
gitRepositories,
requestGroups,
requestMetas,
requestVersions,
apiSpecs,
gitRepositories,
requests,
workspaceMetas,
workspaces,
} = entitiesLists;
const settings = entitiesLists.settings[0];
@@ -1423,6 +1427,12 @@ function mapStateToProps(state, props) {
// Api spec stuff
const activeApiSpec = apiSpecs.find(s => s.parentId === activeWorkspace._id);
// Test stuff
const activeUnitTests = selectActiveUnitTests(state, props);
const activeUnitTestSuite = selectActiveUnitTestSuite(state, props);
const activeUnitTestSuites = selectActiveUnitTestSuites(state, props);
const activeUnitTestResult = selectActiveUnitTestResult(state, props);
return Object.assign({}, state, {
activity: activeActivity,
activeApiSpec,
@@ -1432,6 +1442,10 @@ function mapStateToProps(state, props) {
activeRequest,
activeRequestResponses,
activeResponse,
activeUnitTestResult,
activeUnitTestSuite,
activeUnitTestSuites,
activeUnitTests,
activeWorkspace,
activeWorkspaceClientCertificates,
activeWorkspaceMeta,
@@ -0,0 +1,153 @@
.unit-tests {
display: grid;
width: 100%;
height: 100%;
grid-template-areas: 'appheader appheader' 'body-left body-right';
grid-template-rows: auto 1fr;
grid-template-columns: 2fr 1fr;
border-left: 1px solid var(--hl-md);
&.preview-hidden {
grid-template-columns: 1fr 0;
}
&__header {
grid-area: header;
background: var(--color-bg);
color: var(--color-font);
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--hl-md);
&__buttons {
margin-left: auto;
& > button {
align-self: stretch;
height: 100%;
background: none;
border: none !important;
}
}
h1 {
padding: 0 var(--padding-md);
margin: 0;
}
}
&__tests {
grid-area: body-left;
background: var(--color-bg);
color: var(--color-font);
display: flex;
flex-direction: column;
overflow-y: auto;
&__block {
height: auto;
width: 100%;
border-bottom: 1px dashed var(--hl-md);
padding-bottom: var(--padding-md);
.editor {
overflow-y: hidden !important;
}
&__header {
display: flex;
flex-direction: row;
align-items: center;
h2 {
font-size: var(--font-size-lg);
white-space: nowrap;
}
& > * {
margin-top: var(--padding-md);
margin-bottom: var(--padding-md);
margin-right: var(--padding-md);
}
& > *:first-child {
margin-right: auto;
margin-left: var(--padding-md);
}
.form-control {
width: 100%;
max-width: 18rem;
* {
margin: 0;
}
}
}
}
}
&__results {
padding-left: var(--padding-md);
border-left: 1px solid var(--hl-md);
grid-area: body-right;
background: var(--color-bg);
color: var(--color-font);
display: grid;
grid-template-columns: 100%;
grid-template-rows: 1fr auto;
li {
display: block;
padding: var(--padding-xs) 0;
}
}
&__sidebar {
display: flex;
flex-direction: column;
li {
display: flex;
color: var(--hl);
&:first-child {
width: 100%;
align-items: center;
}
.unit-tests__sidebar__action {
display: none;
color: var(--hl);
&:hover {
color: var(--color-font);
}
}
&:hover .unit-tests__sidebar__action {
display: block;
}
&:hover {
background-color: var(--hl-sm);
color: var(--color-font);
}
&.active {
background-color: var(--hl-xs);
color: var(--color-font);
}
button {
padding: 0 var(--padding-sm);
height: var(--line-height-xs);
width: 100%;
box-sizing: border-box;
}
}
}
}
@@ -52,6 +52,7 @@
@import 'components/shortcuts';
@import 'components/sidebar';
@import 'components/spec-editor';
@import 'components/unit-tests';
@import 'components/spec-editor-sidebar';
@import 'components/tabs';
@import 'components/tag';
@@ -115,5 +115,8 @@ export async function allDocs() {
...(await models.oAuth2Token.all()),
...(await models.clientCertificate.all()),
...(await models.apiSpec.all()),
...(await models.unitTestSuite.all()),
...(await models.unitTest.all()),
...(await models.unitTestResult.all()),
];
}
@@ -324,6 +324,63 @@ export const selectActiveResponse = createSelector(
},
);
export const selectActiveUnitTestResult = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => {
let recentResult = null;
for (const r of entities.unitTestResults) {
if (r.parentId !== activeWorkspace._id) {
continue;
}
if (!recentResult) {
recentResult = r;
continue;
}
if (r.created > recentResult.created) {
recentResult = r;
}
}
return recentResult;
},
);
export const selectActiveUnitTestSuite = createSelector(
selectEntitiesLists,
selectActiveWorkspaceMeta,
(entities, activeWorkspaceMeta) => {
if (!activeWorkspaceMeta) {
return null;
}
const id = activeWorkspaceMeta.activeUnitTestSuiteId;
return entities.unitTestSuites.find(s => s._id === id) || null;
},
);
export const selectActiveUnitTests = createSelector(
selectEntitiesLists,
selectActiveUnitTestSuite,
(entities, activeUnitTestSuite) => {
if (!activeUnitTestSuite) {
return [];
}
return entities.unitTests.filter(s => s.parentId === activeUnitTestSuite._id);
},
);
export const selectActiveUnitTestSuites = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => {
return entities.unitTestSuites.filter(s => s.parentId === activeWorkspace._id);
},
);
export const selectSyncItems = createSelector(selectActiveWorkspaceEntities, workspaceEntities =>
workspaceEntities.filter(models.canSync).map(doc => ({
key: doc._id,
@@ -1,5 +1,5 @@
{
"version": "2020.2.2",
"version": "2020.3.0-alpha.2",
"name": "insomnia-designer",
"executableName": "insomnia-designer",
"appId": "com.insomnia.designer",
@@ -98,11 +98,6 @@ const StyledButton: React.ComponentType<Props> = styled.button`
&:disabled {
opacity: 60%;
}
svg {
display: inline-block !important;
margin-left: 0.4em;
}
`;
const Button = ({ variant, bg, size, ...props }: Props) => (
@@ -66,6 +66,39 @@ describe('integration', () => {
expect(stats2.passes).toBe(2);
});
it('generates and runs more than once', async () => {
const testSrc = await generate([
{
name: 'Example TestSuite',
suites: [],
tests: [
{
name: 'should return -1 when the value is not present',
code: 'expect([1, 2, 3].indexOf(4)).to.equal(-1);\nexpect(true).to.be.true;',
defaultRequestId: null,
},
{
name: 'is an empty test',
code: '',
defaultRequestId: null,
},
],
},
]);
const { stats, failures } = await runTests(testSrc);
expect(failures).toEqual([]);
expect(stats.tests).toBe(2);
expect(stats.failures).toBe(0);
expect(stats.passes).toBe(2);
const { stats: stats2, failures: failures2 } = await runTests(testSrc);
expect(failures2).toEqual([]);
expect(stats2.tests).toBe(2);
expect(stats2.failures).toBe(0);
expect(stats2.passes).toBe(2);
});
it('sends an HTTP request', async () => {
axios.__setResponse('GET', '200.insomnia.rest', {
status: 200,
@@ -37,7 +37,7 @@ module.exports = {
notice: '#c18401',
warning: '#c18401',
danger: '#e45649',
surprise: '#a626a4',
surprise: '#6030BF',
info: '#0184bc',
},
foreground: {
@@ -86,6 +86,23 @@ describe('plugin', () => {
expect(result).toBe('bar');
});
});
describe('RequestExtension name', () => {
it('should get name', async () => {
const requests = [
{
_id: 'req_1',
name: 'Foo',
parameters: [{ name: 'foo', value: '{{ foo }}' }],
url: 'https://insomnia.rest/foo/bar',
},
];
const context = _getTestContext([{ _id: 'wrk_1' }], requests);
const result = await tag.run(context, 'name');
expect(result).toBe('Foo');
});
});
});
function _getTestContext(workspaces, requests, jars) {