import React, { PureComponent, ReactNode } from 'react'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { AUTOBIND_CFG, ACTIVITY_HOME } from '../../common/constants'; import classnames from 'classnames'; import PageLayout from './page-layout'; import { Button, Dropdown, DropdownItem, ListGroup, UnitTestItem, UnitTestResultItem, } from 'insomnia-components'; import UnitTestEditable from './unit-test-editable'; import ErrorBoundary from './error-boundary'; import CodeEditor from './codemirror/code-editor'; import type { WrapperProps } from './wrapper'; import * as models from '../../models'; import type { UnitTest } from '../../models/unit-test'; import { generate, runTests, Test } 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 { getSendRequestCallback } from '../../common/send-request'; import type { GlobalActivity } from '../../common/constants'; import WorkspacePageHeader from './workspace-page-header'; import { trackSegmentEvent } from '../../common/analytics'; import { isRequestGroup } from '../../models/request-group'; import { isRequest } from '../../models/request'; interface Props { children: SidebarChildObjects; gitSyncDropdown: ReactNode; handleActivityChange: (workspaceId: string, activity: GlobalActivity) => Promise; wrapperProps: WrapperProps; } interface State { testsRunning: UnitTest[] | null; resultsError: string | null; } @autoBindMethodsForReact(AUTOBIND_CFG) class WrapperUnitTest extends PureComponent { state: State = { testsRunning: null, resultsError: null, }; // Defining it here instead of in render() so it won't act as a changed prop // when being passed to 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) { 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) { 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: '', // @ts-expect-error -- TSCONVERSION value: this.generateSendReqSnippet(unitTest.code, `'${request._id}'`), })), ], onDone: value => resolve(value), }); }); }, }, ]; } async _handleCreateTestSuite() { 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); trackSegmentEvent('Test Suite Created'); }, }); } async _handleCreateTest() { const { activeUnitTestSuite } = this.props.wrapperProps; showPrompt({ title: 'New Test', defaultValue: 'Returns 200', submitName: 'New Test', label: 'Test Name', selectText: true, onComplete: async name => { await models.unitTest.create({ parentId: activeUnitTestSuite?._id, code: this.generateSendReqSnippet('', ''), name, }); trackSegmentEvent('Unit Test Created'); }, }); } async _handleUnitTestCodeChange(unitTest: UnitTest, v: string) { await models.unitTest.update(unitTest, { code: v, }); } async _handleBreadcrumb() { const { handleActivityChange, wrapperProps: { activeWorkspace }, } = this.props; await handleActivityChange(activeWorkspace._id, ACTIVITY_HOME); } async _handleRunTests() { const { activeUnitTests } = this.props.wrapperProps; await this._runTests(activeUnitTests); trackSegmentEvent('Ran All Unit Tests'); } async _handleRunTest(unitTest: UnitTest) { await this._runTests([unitTest]); trackSegmentEvent('Ran Individual Unit Test'); } async _handleDeleteTest(unitTest: UnitTest) { showAlert({ title: `Delete ${unitTest.name}`, message: ( Really delete {unitTest.name}? ), addCancel: true, onConfirm: async () => { await models.unitTest.remove(unitTest); trackSegmentEvent('Unit Test Deleted'); }, }); } async _handleSetActiveRequest( unitTest: UnitTest, e: React.SyntheticEvent, ) { const requestId = e.currentTarget.value === '__NULL__' ? null : e.currentTarget.value; await models.unitTest.update(unitTest, { requestId, }); } async _handleDeleteUnitTestSuite(unitTestSuite: UnitTestSuite) { showAlert({ title: `Delete ${unitTestSuite.name}`, message: ( Really delete {unitTestSuite.name}? ), addCancel: true, onConfirm: async () => { await models.unitTestSuite.remove(unitTestSuite); trackSegmentEvent('Test Suite Deleted'); }, }); } async _handleSetActiveUnitTestSuite(unitTestSuite: UnitTestSuite) { const { activeWorkspace } = this.props.wrapperProps; await models.workspaceMeta.updateByParentId(activeWorkspace._id, { activeUnitTestSuiteId: unitTestSuite._id, }); } async _handleChangeTestName(unitTest: UnitTest, name: string) { await models.unitTest.update(unitTest, { name, }); } async _handleChangeActiveSuiteName(name: string) { const { activeUnitTestSuite } = this.props.wrapperProps; // @ts-expect-error -- TSCONVERSION await models.unitTestSuite.update(activeUnitTestSuite, { name, }); } async _runTests(unitTests: UnitTest[]) { const { requests, activeWorkspace, activeEnvironment } = this.props.wrapperProps; this.setState({ testsRunning: unitTests, resultsError: null, }); const tests: Test[] = []; 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 sendRequest = getSendRequestCallback(activeEnvironment ? activeEnvironment._id : null); let results; try { results = await runTests(src, { requests, // @ts-expect-error -- TSCONVERSION sendRequest, }); } catch (err) { // Set the state after a timeout so the user still sees the loading state setTimeout(() => { this.setState({ resultsError: err.message, testsRunning: null, }); }, 400); return; } await models.unitTestResult.create({ results, parentId: activeWorkspace._id, }); this.setState({ testsRunning: null, }); } buildSelectableRequests(): { name: string; request: Request; }[] { const { children } = this.props; const selectableRequests: { name: string; request: Request; }[] = []; const next = (p, children) => { for (const c of children) { if (isRequest(c.doc)) { selectableRequests.push({ name: `${p} [${c.doc.method}] ${c.doc.name}`, request: c.doc, }); } else if (isRequestGroup(c.doc)) { next(c.doc.name + ' / ', c.children); } } }; next('', children.all); return selectableRequests; } _renderResults() { const { activeUnitTestResult } = this.props.wrapperProps; const { testsRunning, resultsError } = this.state; if (resultsError) { return (

Run Failed

{resultsError}
); } if (testsRunning) { return (

Running {testsRunning.length} Tests...

); } if (!activeUnitTestResult) { return (

No Results

); } if (activeUnitTestResult.results) { const { stats, tests } = activeUnitTestResult.results; return (
{activeUnitTestResult && (
{stats.failures ? (

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

) : (

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

)}
{tests.map((t, i) => ( ))}
)}
); } return (

Awaiting Test Execution

); } renderUnitTest(unitTest: UnitTest) { const { settings } = this.props.wrapperProps; const { testsRunning } = this.state; const selectableRequests = this.buildSelectableRequests(); return ( }> this.autocompleteSnippets(unitTest)} lintOptions={WrapperUnitTest.lintOptions} onChange={this._handleUnitTestCodeChange.bind(this, unitTest)} nunjucksPowerUserMode={settings.nunjucksPowerUserMode} // @ts-expect-error -- TSCONVERSION isVariableUncovered={settings.isVariableUncovered} mode="javascript" lineWrapping={settings.editorLineWrapping} placeholder="" /> ); } _renderTestSuite() { const { activeUnitTests, activeUnitTestSuite } = this.props.wrapperProps; const { testsRunning } = this.state; if (!activeUnitTestSuite) { return
No test suite selected
; } return (

{activeUnitTests.map(this.renderUnitTest)}
); } _renderPageSidebar() { const { activeUnitTestSuites, activeUnitTestSuite } = this.props.wrapperProps; const { testsRunning } = this.state; const activeId = activeUnitTestSuite ? activeUnitTestSuite._id : 'n/a'; return (
    {activeUnitTestSuites.map(s => (
  • ( )}> {testsRunning ? 'Running... ' : 'Run Tests'} Delete Suite
  • ))}
); } _renderPageHeader() { const { wrapperProps, gitSyncDropdown, handleActivityChange } = this.props; return ( ); } render() { return ( ); } } export default WrapperUnitTest;