From e38badfbf39a4d4b694e0d7ea34a0e315ddab0da Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Tue, 2 Apr 2024 15:42:39 +0200 Subject: [PATCH] mock-extraction feedback (#7207) * save point * both cases work * use action and hack * throw error on naming collision * fix type * fix test --- .../tests/smoke/mock.test.ts | 17 +-- .../editors/mock-response-extractor.tsx | 101 +++++++++++++++--- packages/insomnia/src/ui/routes/actions.tsx | 21 +++- .../insomnia/src/ui/routes/mock-route.tsx | 24 ++++- .../insomnia/src/ui/routes/mock-server.tsx | 23 +++- 5 files changed, 154 insertions(+), 32 deletions(-) diff --git a/packages/insomnia-smoke-test/tests/smoke/mock.test.ts b/packages/insomnia-smoke-test/tests/smoke/mock.test.ts index 95bcd29864..c349fef4b7 100644 --- a/packages/insomnia-smoke-test/tests/smoke/mock.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/mock.test.ts @@ -1,22 +1,15 @@ -import { loadFixture } from '../../playwright/paths'; import { test } from '../../playwright/test'; -test('can make a mock route', async ({ app, page }) => { +test('can make a mock route', async ({ page }) => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); - const text = await loadFixture('smoke-test-collection.yaml'); - await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - - await page.getByRole('button', { name: 'Create in project' }).click(); - await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.locator('[data-test-id="import-from-clipboard"]').click(); - await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByLabel('New Mock Server').click(); await page.getByRole('button', { name: 'Create', exact: true }).click(); await page.getByRole('button', { name: 'New Mock Route' }).click(); - await page.getByText('GET/').click(); - await page.getByTestId('CodeEditor').getByRole('textbox').fill('123'); + await page.getByLabel('Project Actions').click(); + await page.getByText('Rename').click(); + await page.locator('#prompt-input').fill('/123'); + await page.getByRole('button', { name: 'Rename' }).click(); await page.getByRole('button', { name: 'Test' }).click(); await page.getByText('No body returned for response').click(); diff --git a/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx b/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx index 1b99484af1..5e0d9debd6 100644 --- a/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx +++ b/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx @@ -2,21 +2,32 @@ import fs from 'fs/promises'; import React, { useState } from 'react'; import { Button } from 'react-aria-components'; import { useNavigate, useParams } from 'react-router-dom'; -import { useRouteLoaderData } from 'react-router-dom'; +import { + useFetcher, + useRouteLoaderData, +} from 'react-router-dom'; +import { invariant } from '../../../utils/invariant'; import { useMockRoutePatcher } from '../../routes/mock-route'; import { RequestLoaderData } from '../../routes/request'; +import { WorkspaceLoaderData } from '../../routes/workspace'; import { HelpTooltip } from '../help-tooltip'; import { Icon } from '../icon'; +import { showPrompt } from '../modals'; export const MockResponseExtractor = () => { + const { + activeWorkspace, + } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const { mockServerAndRoutes, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData; const patchMockRoute = useMockRoutePatcher(); const navigate = useNavigate(); const { organizationId, projectId, + workspaceId, } = useParams(); + const fetcher = useFetcher(); const [selectedMockServer, setSelectedMockServer] = useState(''); const [selectedMockRoute, setSelectedMockRoute] = useState(''); return ( @@ -30,21 +41,84 @@ export const MockResponseExtractor = () => {
{ e.preventDefault(); - if (!selectedMockServer || !selectedMockRoute) { + if (selectedMockServer && selectedMockRoute) { + if (activeResponse) { + // TODO: move this out of the renderer, and upsert mock + const body = await fs.readFile(activeResponse.bodyPath); + + patchMockRoute(selectedMockRoute, { + body: body.toString(), + mimeType: activeResponse.contentType, + statusCode: activeResponse.statusCode, + headers: activeResponse.headers, + }); + } return; } + let path = '/new-route'; + try { + path = activeResponse ? new URL(activeResponse.url).pathname : '/new-route'; + } catch (e) { + console.log(e); + } + if (!selectedMockServer) { + showPrompt({ + title: 'Create Mock Route', + defaultValue: path, + label: 'Name', + onComplete: async name => { + invariant(activeResponse, 'Active response must be defined'); + const body = await fs.readFile(activeResponse.bodyPath); + // TODO: consider setting selected mock server here rather than redirecting + fetcher.submit( + JSON.stringify({ + name: name, + body: body.toString(), + mimeType: activeResponse.contentType, + statusCode: activeResponse.statusCode, + headers: activeResponse.headers, + mockServerName: activeWorkspace.name, + }), + { + encType: 'application/json', + method: 'post', + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`, + } + ); + }, + }); + return; + } + if (!selectedMockRoute) { + showPrompt({ + title: 'Create Mock Route', + defaultValue: path, + label: 'Name', + onComplete: async name => { + invariant(activeResponse, 'Active response must be defined'); + const body = await fs.readFile(activeResponse.bodyPath); - if (activeResponse) { - // TODO: move this out of the renderer, and upsert mock - const body = await fs.readFile(activeResponse.bodyPath); + // setSelectedMockRoute(newRoute._id); - patchMockRoute(selectedMockRoute, { - body: body.toString(), - mimeType: activeResponse.contentType, - statusCode: activeResponse.statusCode, - headers: activeResponse.headers, + fetcher.submit( + JSON.stringify({ + name: name, + parentId: selectedMockServer, + body: body.toString(), + mimeType: activeResponse.contentType, + statusCode: activeResponse.statusCode, + headers: activeResponse.headers, + }), + { + encType: 'application/json', + method: 'post', + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`, + } + ); + }, }); } + }} >
@@ -61,7 +135,7 @@ export const MockResponseExtractor = () => { setSelectedMockServer(selected); }} > - + {mockServerAndRoutes .map(w => ( + {mockServerAndRoutes.find(s => s._id === selectedMockServer)?.routes .map(w => (
diff --git a/packages/insomnia/src/ui/routes/actions.tsx b/packages/insomnia/src/ui/routes/actions.tsx index 94343c784a..1267e33e2a 100644 --- a/packages/insomnia/src/ui/routes/actions.tsx +++ b/packages/insomnia/src/ui/routes/actions.tsx @@ -1211,9 +1211,26 @@ export const createMockRouteAction: ActionFunction = async ({ request, params }) const patch = await request.json(); invariant(typeof patch.name === 'string', 'Name is required'); - invariant(typeof patch.parentId === 'string', 'parentId is required'); + // TODO: remove this hack + if (patch.mockServerName) { + const activeWorkspace = await models.workspace.getById(workspaceId); + invariant(activeWorkspace, 'Active workspace not found'); + const workspace = await models.workspace.create({ + name: activeWorkspace.name, + scope: 'mock-server', + parentId: projectId, + }); + invariant(workspace, 'Workspace not found'); + // create a mock server under the workspace with the same name + const newServer = await models.mockServer.getOrCreateForParentId(workspace._id, { name: activeWorkspace.name }); + // TODO: filterout the mockServerName from the patch, or use an alternate method to create new workspace and server + const mockRoute = await models.mockRoute.create({ ...patch, parentId: newServer._id }); + return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${newServer.parentId}/mock-server/mock-route/${mockRoute._id}`); + } + const mockServer = await models.mockServer.getById(patch.parentId); + invariant(mockServer, 'Mock server not found'); const mockRoute = await models.mockRoute.create(patch); - return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${mockRoute._id}`); + return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${mockServer.parentId}/mock-server/mock-route/${mockRoute._id}`); }; export const updateMockRouteAction: ActionFunction = async ({ request, params }) => { const { mockRouteId } = params; diff --git a/packages/insomnia/src/ui/routes/mock-route.tsx b/packages/insomnia/src/ui/routes/mock-route.tsx index d9e0844f5c..87a6f80174 100644 --- a/packages/insomnia/src/ui/routes/mock-route.tsx +++ b/packages/insomnia/src/ui/routes/mock-route.tsx @@ -17,10 +17,12 @@ import { CodeEditor } from '../components/codemirror/code-editor'; import { MockResponseHeadersEditor } from '../components/editors/mock-response-headers-editor'; import { MockResponsePane } from '../components/mocks/mock-response-pane'; import { MockUrlBar } from '../components/mocks/mock-url-bar'; -import { showAlert } from '../components/modals'; +import { showAlert, showModal } from '../components/modals'; +import { AlertModal } from '../components/modals/alert-modal'; import { EmptyStatePane } from '../components/panes/empty-state-pane'; import { Pane, PaneBody, PaneHeader } from '../components/panes/pane'; import { SvgIcon } from '../components/svg-icon'; +import { MockServerLoaderData } from './mock-server'; import { useRootLoaderData } from './root'; export interface MockRouteLoaderData { @@ -91,6 +93,8 @@ export const useMockRoutePatcher = () => { export const MockRouteRoute = () => { const { mockServer, mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData; + const { mockRoutes } = useRouteLoaderData('mock-server') as MockServerLoaderData; + const { userSession } = useRootLoaderData(); const patchMockRoute = useMockRoutePatcher(); const mockbinUrl = mockServer.useInsomniaCloud ? getMockServiceURL() : mockServer.url; @@ -142,6 +146,15 @@ export const MockRouteRoute = () => { }); const upsertMockbinHar = async (pathInput?: string) => { + const hasRouteInServer = mockRoutes.filter(m => m._id !== mockRoute._id).find(m => m.name === pathInput); + if (hasRouteInServer) { + showModal(AlertModal, { + title: 'Error', + message: `Path "${pathInput}" must be unique. Please enter a different name.`, + }); + + return; + }; const compoundId = mockRoute.parentId + pathInput; const error = await upsertBinOnRemoteFromResponse(compoundId); if (error) { @@ -163,6 +176,15 @@ export const MockRouteRoute = () => { }); }; const onSend = async (pathInput: string) => { + const hasRouteInServer = mockRoutes.filter(m => m._id !== mockRoute._id).find(m => m.name === pathInput); + if (hasRouteInServer) { + showModal(AlertModal, { + title: 'Error', + message: `Path "${pathInput}" must be unique. Please enter a different name.`, + }); + + return; + }; await upsertMockbinHar(pathInput); const compoundId = mockRoute.parentId + pathInput; createandSendPrivateRequest({ diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index 79940413f7..5ab40c5de9 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -11,17 +11,18 @@ import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dr import { EditableInput } from '../components/editable-input'; import { Icon } from '../components/icon'; import { showModal, showPrompt } from '../components/modals'; +import { AlertModal } from '../components/modals/alert-modal'; import { AskModal } from '../components/modals/ask-modal'; import { EmptyStatePane } from '../components/panes/empty-state-pane'; import { SidebarLayout } from '../components/sidebar-layout'; import { SvgIcon } from '../components/svg-icon'; import { formatMethodName } from '../components/tags/method-tag'; import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher } from './mock-route'; -interface LoaderData { +export interface MockServerLoaderData { mockServerId: string; mockRoutes: MockRoute[]; } -export const loader: LoaderFunction = async ({ params }): Promise => { +export const loader: LoaderFunction = async ({ params }): Promise => { const { organizationId, projectId, workspaceId } = params; invariant(organizationId, 'Organization ID is required'); invariant(projectId, 'Project ID is required'); @@ -46,7 +47,7 @@ const MockServerRoute = () => { workspaceId: string; mockRouteId: string; }; - const { mockServerId, mockRoutes } = useLoaderData() as LoaderData; + const { mockServerId, mockRoutes } = useLoaderData() as MockServerLoaderData; const fetcher = useFetcher(); const navigate = useNavigate(); const patchMockRoute = useMockRoutePatcher(); @@ -66,6 +67,14 @@ const MockServerRoute = () => { defaultValue: mockRoutes.find(s => s._id === id)?.name, submitName: 'Rename', onComplete: name => { + const hasRouteInServer = mockRoutes.filter(m => m._id !== id).find(m => m.name === name); + if (hasRouteInServer) { + showModal(AlertModal, { + title: 'Error', + message: `Path "${name}" must be unique. Please enter a different name.`, + }); + return; + }; name && patchMockRoute(id, { name }); }, }); @@ -195,6 +204,14 @@ const MockServerRoute = () => { }); }} onSubmit={name => { + const hasRouteInServer = mockRoutes.filter(m => m._id !== item._id).find(m => m.name === name); + if (hasRouteInServer) { + showModal(AlertModal, { + title: 'Error', + message: `Path "${name}" must be unique. Please enter a different name.`, + }); + return; + }; name && fetcher.submit( { name }, {