Skip to content

Commit

Permalink
feat(unit-tests): Unit test reordering (#7020)
Browse files Browse the repository at this point in the history
* re-orderable test suites

* re-orderable tests

* fix ts

* Use db methods for sorting

* sort suites and unit tests in inso

* fix suite rename
  • Loading branch information
gatzjames authored Jan 26, 2024
1 parent 28e5463 commit e1e3b13
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 57 deletions.
2 changes: 1 addition & 1 deletion packages/insomnia-inso/src/commands/run-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function runInsomniaTests(
suites.map(suite =>
createTestSuite(
suite,
db.UnitTest.filter(test => test.parentId === suite._id),
db.UnitTest.filter(test => test.parentId === suite._id).sort((a, b) => a.metaSortKey - b.metaSortKey),
),
),
);
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia-inso/src/db/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ApiSpec = BaseModel & BaseApiSpec;

interface BaseUnitTestSuite {
name: string;
metaSortKey: number;
}

export type UnitTestSuite = BaseModel & BaseUnitTestSuite;
Expand All @@ -25,6 +26,7 @@ interface BaseUnitTest {
name: string;
code: string;
requestId: string | null;
metaSortKey: number;
}

export type UnitTest = BaseModel & BaseUnitTest;
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia-inso/src/db/models/unit-test-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const loadTestSuites = (
const workspace = loadWorkspace(db, apiSpec?.parentId || identifier); // if identifier is for an apiSpec or a workspace, return all suites for that workspace

if (workspace) {
return db.UnitTestSuite.filter(s => s.parentId === workspace._id);
return db.UnitTestSuite.filter(s => s.parentId === workspace._id).sort((a, b) => a.metaSortKey - b.metaSortKey);
} // load particular suite

const result = loadUnitTestSuite(db, identifier);
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/models/unit-test-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const canDuplicate = true;
export const canSync = true;
export interface BaseUnitTestSuite {
name: string;
metaSortKey: number;
}

export type UnitTestSuite = BaseModel & BaseUnitTestSuite;
Expand All @@ -23,6 +24,7 @@ export const isUnitTestSuite = (model: Pick<BaseModel, 'type'>): model is UnitTe
export function init() {
return {
name: 'My Test',
metaSortKey: -1 * Date.now(),
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/models/unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface BaseUnitTest {
name: string;
code: string;
requestId: string | null;
metaSortKey: number;
}

export type UnitTest = BaseModel & BaseUnitTest;
Expand All @@ -27,6 +28,7 @@ export function init() {
requestId: null,
name: 'My Test',
code: '',
metaSortKey: -1 * Date.now(),
};
}

Expand Down
4 changes: 2 additions & 2 deletions packages/insomnia/src/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -605,11 +605,11 @@ const router = createMemoryRouter(
).deleteTestSuiteAction(...args),
},
{
path: 'rename',
path: 'update',
action: async (...args) =>
(
await import('./routes/actions')
).renameTestSuiteAction(...args),
).updateTestSuiteAction(...args),
},
{
path: 'run-all-tests',
Expand Down
33 changes: 12 additions & 21 deletions packages/insomnia/src/ui/routes/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isRemoteProject } from '../../models/project';
import { isRequest, Request } from '../../models/request';
import { isRequestGroup, isRequestGroupId } from '../../models/request-group';
import { UnitTest } from '../../models/unit-test';
import { UnitTestSuite } from '../../models/unit-test-suite';
import { isCollection, Workspace } from '../../models/workspace';
import { WorkspaceMeta } from '../../models/workspace-meta';
import { getSendRequestCallback } from '../../network/unit-test-feature';
Expand Down Expand Up @@ -516,9 +517,11 @@ export const runAllTestsAction: ActionFunction = async ({
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');

const unitTests = await database.find<UnitTest>(models.unitTest.type, {
parentId: testSuiteId,
});
const unitTests = await database.find<UnitTest>(
models.unitTest.type,
{ parentId: testSuiteId },
{ metaSortKey: 1 }
);
invariant(unitTests, 'No unit tests found');

const tests: Test[] = unitTests
Expand All @@ -545,23 +548,21 @@ export const runAllTestsAction: ActionFunction = async ({
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`);
};

export const renameTestSuiteAction: ActionFunction = async ({ request, params }) => {
export const updateTestSuiteAction: ActionFunction = async ({ request, params }) => {
const { workspaceId, projectId, testSuiteId } = params;
invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
invariant(typeof projectId === 'string', 'Project ID is required');

const formData = await request.formData();
const name = formData.get('name');
invariant(typeof name === 'string', 'Name is required');
const data = await request.json() as Partial<UnitTestSuite>;

const unitTestSuite = await database.getWhere(models.unitTestSuite.type, {
const unitTestSuite = await database.getWhere<UnitTestSuite>(models.unitTestSuite.type, {
_id: testSuiteId,
});

invariant(unitTestSuite, 'Test Suite not found');

await models.unitTestSuite.update(unitTestSuite, { name });
await models.unitTestSuite.update(unitTestSuite, data);

return null;
};
Expand Down Expand Up @@ -605,24 +606,14 @@ export const deleteTestAction: ActionFunction = async ({ params }) => {

export const updateTestAction: ActionFunction = async ({ request, params }) => {
const { testId } = params;
const formData = await request.formData();
invariant(typeof testId === 'string', 'Test ID is required');
const code = formData.get('code');
invariant(typeof code === 'string', 'Code is required');
const name = formData.get('name');
invariant(typeof name === 'string', 'Name is required');
const requestId = formData.get('requestId');

if (requestId) {
invariant(typeof requestId === 'string', 'Request ID is required');
}
const data = await request.json() as Partial<UnitTest>;

const unitTest = await database.getWhere<UnitTest>(models.unitTest.type, {
_id: testId,
});
invariant(unitTest, 'Test not found');

await models.unitTest.update(unitTest, { name, code, requestId: requestId || null });
await models.unitTest.update(unitTest, data);

return null;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/ui/routes/remote-collections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async function getSyncItems({
const wsReqs = await database.find(models.webSocketRequest.type, { parentId: { $in: listOfParentIds } });
const allRequests = [...reqs, ...reqGroups, ...grpcReqs, ...wsReqs] as (Request | RequestGroup | GrpcRequest | WebSocketRequest)[];
const testSuites = await models.unitTestSuite.findByParentId(workspaceId);
const tests = await database.find(models.unitTest.type, { parentId: { $in: testSuites.map(t => t._id) } });
const tests = await database.find<UnitTest>(models.unitTest.type, { parentId: { $in: testSuites.map(t => t._id) } });

const baseEnvironment = await models.environment.getByParentId(workspaceId);
invariant(baseEnvironment, 'Base environment not found');
Expand Down
116 changes: 93 additions & 23 deletions packages/insomnia/src/ui/routes/test-suite.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { Fragment, useRef, useState } from 'react';
import {
Button,
DropIndicator,
Heading,
ListBox,
ListBoxItem,
Popover,
Select,
SelectValue,
useDragAndDrop,
} from 'react-aria-components';
import {
LoaderFunction,
Expand All @@ -21,7 +23,7 @@ import { documentationLinks } from '../../common/documentation';
import * as models from '../../models';
import { isGrpcRequest } from '../../models/grpc-request';
import { isRequest, Request } from '../../models/request';
import { isUnitTest, UnitTest } from '../../models/unit-test';
import { UnitTest } from '../../models/unit-test';
import { UnitTestSuite } from '../../models/unit-test-suite';
import { isWebSocketRequest } from '../../models/websocket-request';
import { invariant } from '../../utils/invariant';
Expand Down Expand Up @@ -90,13 +92,12 @@ const UnitTestItemView = ({
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',
encType: 'application/json',
}
);
}
Expand All @@ -110,13 +111,12 @@ const UnitTestItemView = ({
onSelectionChange={requestId => {
updateUnitTestFetcher.submit(
{
code: unitTest.code,
name: unitTest.name,
requestId,
},
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${unitTest._id}/update`,
method: 'post',
encType: 'application/json',
}
);
}}
Expand Down Expand Up @@ -306,12 +306,11 @@ const UnitTestItemView = ({
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',
encType: 'application/json',
}
)
}
Expand Down Expand Up @@ -369,7 +368,7 @@ export const loader: LoaderFunction = async ({
const workspaceEntities = await database.withDescendants(workspace);
const requests: Request[] = workspaceEntities.filter(isRequest);

const unitTestSuite = await database.getWhere(models.unitTestSuite.type, {
const unitTestSuite = await database.getWhere<UnitTestSuite>(models.unitTestSuite.type, {
_id: testSuiteId,
});

Expand All @@ -383,8 +382,14 @@ export const loader: LoaderFunction = async ({

invariant(unitTestSuite, 'Test Suite not found');

const unitTests = (await database.withDescendants(unitTestSuite)).filter(
isUnitTest
const unitTests = await database.find<UnitTest>(
models.unitTest.type,
{
parentId: testSuiteId,
},
{
metaSortKey: 1,
}
);

return {
Expand All @@ -407,13 +412,67 @@ const TestSuiteRoute = () => {

const createUnitTestFetcher = useFetcher();
const runAllTestsFetcher = useFetcher();
const renameTestSuiteFetcher = useFetcher();
const updateTestSuiteFetcher = useFetcher();
const updateUnitTestFetcher = useFetcher();

const testsRunning = runAllTestsFetcher.state === 'submitting';

const optimisticUpdateTestSuiteName = updateTestSuiteFetcher.json && typeof updateTestSuiteFetcher.json === 'object' &&
'name' in updateTestSuiteFetcher.json && updateTestSuiteFetcher.json?.name?.toString();

const testSuiteName =
renameTestSuiteFetcher.formData?.get('name')?.toString() ??
optimisticUpdateTestSuiteName ||
unitTestSuite.name;

const unitTestsDragAndDrop = useDragAndDrop({
getItems: keys => [...keys].map(key => ({ 'text/plain': key.toString() })),
onReorder(e) {
const source = [...e.keys][0];
const sourceTest = unitTests.find(test => test._id === source);
const targetTest = unitTests.find(test => test._id === e.target.key);

if (!sourceTest || !targetTest) {
return;
}
const dropPosition = e.target.dropPosition;
if (dropPosition === 'before') {
const currentTestIndex = unitTests.findIndex(test => test._id === targetTest._id);
const previousTest = unitTests[currentTestIndex - 1];
if (!previousTest) {
sourceTest.metaSortKey = targetTest.metaSortKey - 1;
} else {
sourceTest.metaSortKey = (previousTest.metaSortKey + targetTest.metaSortKey) / 2;
}
}
if (dropPosition === 'after') {
const currentTestIndex = unitTests.findIndex(test => test._id === targetTest._id);
const nextEnv = unitTests[currentTestIndex + 1];
if (!nextEnv) {
sourceTest.metaSortKey = targetTest.metaSortKey + 1;
} else {
sourceTest.metaSortKey = (nextEnv.metaSortKey + targetTest.metaSortKey) / 2;
}
}

updateUnitTestFetcher.submit(
{ metaSortKey: sourceTest.metaSortKey },
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${sourceTest._id}/update`,
method: 'POST',
encType: 'application/json',
}
);
},
renderDropIndicator(target) {
return (
<DropIndicator
target={target}
className="outline-[--color-surprise] outline-1 outline !border-none"
/>
);
},
});

return (
<div className="flex flex-col h-full w-full overflow-hidden divide-solid divide-y divide-[--hl-md]">
<div className="flex flex-shrink-0 gap-2 p-[--padding-md]">
Expand All @@ -422,11 +481,12 @@ const TestSuiteRoute = () => {
className='w-full px-1'
onSubmit={name =>
name &&
renameTestSuiteFetcher.submit(
updateTestSuiteFetcher.submit(
{ name },
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/rename`,
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/update`,
method: 'POST',
encType: 'application/json',
}
)
}
Expand Down Expand Up @@ -505,15 +565,25 @@ const TestSuiteRoute = () => {
</div>
)}
{unitTests.length > 0 && (
<ul className="flex-1 flex flex-col divide-y divide-solid divide-[--hl-md] overflow-y-auto">
{unitTests.map(unitTest => (
<UnitTestItemView
key={unitTest._id}
unitTest={unitTest}
testsRunning={testsRunning}
/>
))}
</ul>
<ListBox
dragAndDropHooks={unitTestsDragAndDrop.dragAndDropHooks}
items={unitTests.map(unitTest => ({
...unitTest,
id: unitTest._id,
key: unitTest._id,
}))}
className="flex-1 flex flex-col divide-y divide-solid divide-[--hl-md] overflow-y-auto"
>
{unitTest => (
<ListBoxItem className="outline-none">
<Button slot="drag" className="hidden" />
<UnitTestItemView
unitTest={unitTest}
testsRunning={testsRunning}
/>
</ListBoxItem>
)}
</ListBox>
)}
</div>
);
Expand Down
Loading

0 comments on commit e1e3b13

Please sign in to comment.