From df2bc063b40740f5c9fecea3943206d6615cf112 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Thu, 19 Sep 2024 16:29:11 +0200 Subject: [PATCH] runner refactoring pass (#7975) * rename iterations to iterationCount * make duration 0 * use local storage * raise orderedJSON * rename * fix cli --- packages/insomnia-inso/src/cli.ts | 27 ++- packages/insomnia/src/common/render.ts | 24 +- packages/insomnia/src/common/send-request.ts | 21 +- packages/insomnia/src/models/environment.ts | 1 + .../insomnia/src/models/runner-test-result.ts | 9 +- packages/insomnia/src/network/concurrency.ts | 2 +- packages/insomnia/src/network/network.ts | 32 +-- .../components/modals/cli-preview-modal.tsx | 6 +- .../src/ui/hooks/use-local-storage.ts | 214 ++++++++++++++++++ packages/insomnia/src/ui/routes/request.tsx | 14 +- packages/insomnia/src/ui/routes/runner.tsx | 105 +++------ 11 files changed, 322 insertions(+), 133 deletions(-) create mode 100644 packages/insomnia/src/ui/hooks/use-local-storage.ts diff --git a/packages/insomnia-inso/src/cli.ts b/packages/insomnia-inso/src/cli.ts index d34c16a2d1..f857f84146 100644 --- a/packages/insomnia-inso/src/cli.ts +++ b/packages/insomnia-inso/src/cli.ts @@ -6,9 +6,12 @@ import * as commander from 'commander'; import consola, { BasicReporter, FancyReporter, LogLevel, logType } from 'consola'; import { cosmiconfig } from 'cosmiconfig'; import fs from 'fs'; +import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from 'insomnia/src/common/constants'; import { getSendRequestCallbackMemDb } from 'insomnia/src/common/send-request'; +import { UserUploadEnvironment } from 'insomnia/src/models/environment'; import { type RequestTestResult } from 'insomnia-sdk'; import { generate, runTestsCli } from 'insomnia-testing'; +import orderedJSON from 'json-order'; import { parseArgsStringToArgv } from 'string-argv'; import packageJson from '../package.json'; @@ -236,10 +239,13 @@ const readFileFromPathOrUrl = async (pathOrUrl: string) => { } return readFile(pathOrUrl, 'utf8'); }; - -const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise[]> => { +const pathToIterationData = async (pathOrUrl: string): Promise => { const fileType = pathOrUrl.split('.').pop()?.toLowerCase(); const content = await readFileFromPathOrUrl(pathOrUrl); + const list = getListFromFileOrUrl(content, fileType); + return transformIterationDataToEnvironmentList(list); +}; +const getListFromFileOrUrl = (content: string, fileType?: string): Record[] => { if (fileType === 'json') { try { const jsonDataContent = JSON.parse(content); @@ -267,6 +273,21 @@ const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise[]): UserUploadEnvironment[] => { + return list?.map(data => { + const orderedJson = orderedJSON.parse>( + JSON.stringify(data), + JSON_ORDER_PREFIX, + JSON_ORDER_SEPARATOR, + ); + return { + name: 'User Upload', + data: orderedJson.object, + dataPropertyOrder: orderedJson.map || null, + }; + }); +}; + export const go = (args?: string[]) => { const program = new commander.Command(); @@ -470,7 +491,7 @@ export const go = (args?: string[]) => { try { const iterationCount = parseInt(options.iterationCount, 10); - const iterationData = options.iterationData ? await getIterationDataFromFileOrUrl(options.iterationData) : undefined; + const iterationData = options.iterationData ? await pathToIterationData(options.iterationData) : undefined; const sendRequest = await getSendRequestCallbackMemDb(environment._id, db, { validateSSL: !options.disableCertValidation }, iterationData, iterationCount); let success = true; for (let i = 0; i < iterationCount; i++) { diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts index 31595c4cdb..137f7ae7b0 100644 --- a/packages/insomnia/src/common/render.ts +++ b/packages/insomnia/src/common/render.ts @@ -62,7 +62,7 @@ export async function buildRenderContext( subEnvironment, rootGlobalEnvironment, subGlobalEnvironment, - userUploadEnv, + userUploadEnvironment, baseContext = {}, }: { ancestors?: RenderContextAncestor[]; @@ -70,7 +70,7 @@ export async function buildRenderContext( subEnvironment?: Environment; rootGlobalEnvironment?: Environment | null; subGlobalEnvironment?: Environment | null; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; baseContext?: Record; }, ) { @@ -130,10 +130,10 @@ export async function buildRenderContext( } // user upload env in collection runner has highest priority - if (userUploadEnv) { + if (userUploadEnvironment) { const ordered = orderedJSON.order( - userUploadEnv.data, - userUploadEnv.dataPropertyOrder, + userUploadEnvironment.data, + userUploadEnvironment.dataPropertyOrder, JSON_ORDER_SEPARATOR, ); envObjects.push(ordered); @@ -336,7 +336,7 @@ interface BaseRenderContextOptions { baseEnvironment?: Environment; rootGlobalEnvironment?: Environment; subGlobalEnvironment?: Environment; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; purpose?: RenderPurpose; extraInfo?: ExtraRenderInfo; ignoreUndefinedEnvVariable?: boolean; @@ -350,7 +350,7 @@ export async function getRenderContext( request, environment, baseEnvironment, - userUploadEnv, + userUploadEnvironment, ancestors: _ancestors, purpose, extraInfo, @@ -455,8 +455,8 @@ export async function getRenderContext( } // Get Keys from user upload environment - if (userUploadEnv) { - getKeySource(userUploadEnv.data || {}, inKey, userUploadEnv.name || 'uploadData'); + if (userUploadEnvironment) { + getKeySource(userUploadEnvironment.data || {}, inKey, userUploadEnvironment.name || 'uploadData'); } // Add meta data helper function @@ -490,7 +490,7 @@ export async function getRenderContext( subGlobalEnvironment, rootEnvironment, subEnvironment: subEnvironment || undefined, - userUploadEnv, + userUploadEnvironment, baseContext, }); } @@ -556,7 +556,7 @@ export async function getRenderedRequestAndContext( request, environment, baseEnvironment, - userUploadEnv, + userUploadEnvironment, extraInfo, purpose, ignoreUndefinedEnvVariable, @@ -566,7 +566,7 @@ export async function getRenderedRequestAndContext( const workspace = ancestors.find(isWorkspace); const parentId = workspace ? workspace._id : 'n/a'; const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId); - const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnv }); + const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnvironment }); // HACK: Switch '#}' to '# }' to prevent Nunjucks from barfing // https://github.com/kong/insomnia/issues/895 diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index 4af58c87be..0d2ed40017 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -1,4 +1,3 @@ -import orderedJSON from 'json-order'; import path from 'path'; import { type BaseModel, types as modelTypes } from '../models'; @@ -19,7 +18,6 @@ import { tryToInterpolateRequest, } from '../network/network'; import { invariant } from '../utils/invariant'; -import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from './constants'; import { database } from './database'; import { generateId } from './misc'; @@ -35,7 +33,7 @@ const wrapAroundIterationOverIterationData = (list?: UserUploadEnvironment[], cu }; return list[(currentIteration + 1) % list.length]; }; -export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride, iterationData?: Record[], iterationCount?: number) { +export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride, iterationData?: UserUploadEnvironment[], iterationCount?: number) { // Initialize the DB in-memory and fill it with data if we're given one await database.init( modelTypes(), @@ -95,23 +93,12 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB: return { request, settings, clientCertificates, caCert, environment, activeEnvironmentId, workspace, timelinePath, responseId, ancestors }; }; - const userUploadEnvs = iterationData?.map(data => { - const orderedJson = orderedJSON.parse>( - JSON.stringify(data), - JSON_ORDER_PREFIX, - JSON_ORDER_SEPARATOR, - ); - return { - name: 'User Upload', - data: orderedJson.object, - dataPropertyOrder: orderedJson.map || null, - }; - }); // Return callback helper to send requests return async function sendRequest(requestId: string, iteration?: number) { const requestData = await fetchInsoRequestData(requestId, environmentId); - const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id, wrapAroundIterationOverIterationData(userUploadEnvs, iteration), iteration, iterationCount); + const getCurrentRowOfIterationData = wrapAroundIterationOverIterationData(iterationData, iteration); + const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id, getCurrentRowOfIterationData, iteration, iterationCount); if (mutatedContext === null) { console.error('Time out while executing pre-request script'); return null; @@ -125,7 +112,7 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB: purpose: 'send', extraInfo: undefined, baseEnvironment: mutatedContext.baseEnvironment, - userUploadEnv: mutatedContext.userUploadEnv, + userUploadEnvironment: mutatedContext.userUploadEnvironment, ignoreUndefinedEnvVariable, }); // skip plugins diff --git a/packages/insomnia/src/models/environment.ts b/packages/insomnia/src/models/environment.ts index 2a2f6322e8..7f64f91fcc 100644 --- a/packages/insomnia/src/models/environment.ts +++ b/packages/insomnia/src/models/environment.ts @@ -20,6 +20,7 @@ export interface BaseEnvironment { } export type Environment = BaseModel & BaseEnvironment; +// This is a representation of the data taken from a csv or json file AKA iterationData export type UserUploadEnvironment = Pick; export const isEnvironment = (model: Pick): model is Environment => ( diff --git a/packages/insomnia/src/models/runner-test-result.ts b/packages/insomnia/src/models/runner-test-result.ts index 7419731e6d..a7d682382d 100644 --- a/packages/insomnia/src/models/runner-test-result.ts +++ b/packages/insomnia/src/models/runner-test-result.ts @@ -19,7 +19,6 @@ export interface RunnerResultPerRequest { requestName: string; requestUrl: string; responseCode: number; - // TODO: add request name, url, etc } export interface ResponseInfo { @@ -28,15 +27,16 @@ export interface ResponseInfo { originalRequestId: string; } +export type RunnerResultPerRequestPerIteration = RunnerResultPerRequest[][]; + export interface BaseRunnerTestResult { source: RunnerSource; - // environmentId: string; iterations: number; duration: number; // millisecond avgRespTime: number; // millisecond - iterationResults: RunnerResultPerRequest[][]; + iterationResults: RunnerResultPerRequestPerIteration; responsesInfo: ResponseInfo[]; - version: '1'; + version: '1'; // We might want to add or remove result features in future } export type RunnerTestResult = BaseModel & BaseRunnerTestResult; @@ -48,7 +48,6 @@ export const isRunnerTestResult = (model: Pick): model is Run export function init() { return { source: 'runner', - // environmentId: string; iterations: 0, duration: 0, avgRespTime: 0, diff --git a/packages/insomnia/src/network/concurrency.ts b/packages/insomnia/src/network/concurrency.ts index 60ed6ee672..562a38e877 100644 --- a/packages/insomnia/src/network/concurrency.ts +++ b/packages/insomnia/src/network/concurrency.ts @@ -38,7 +38,7 @@ export interface TransformedExecuteScriptContext { globals?: Environment; cookieJar: CookieJar; requestTestResults?: RequestTestResult[]; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; } interface Task { diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 4536c4aa81..ff7c8e0948 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -155,7 +155,7 @@ export const tryToExecutePreRequestScript = async ( ancestors, }: Awaited>, workspaceId: string, - userUploadEnv?: UserUploadEnvironment, + userUploadEnvironment?: UserUploadEnvironment, iteration?: number, iterationCount?: number, ) => { @@ -185,7 +185,7 @@ export const tryToExecutePreRequestScript = async ( settings, cookieJar, globals: activeGlobalEnvironment, - userUploadEnv, + userUploadEnvironment, requestTestResults: new Array(), }; } @@ -201,7 +201,7 @@ export const tryToExecutePreRequestScript = async ( clientCertificates, cookieJar, globals: activeGlobalEnvironment, - userUploadEnv, + userUploadEnvironment, iteration, iterationCount, ancestors, @@ -231,7 +231,7 @@ export const tryToExecutePreRequestScript = async ( globals: mutatedContext.globals, cookieJar: mutatedContext.cookieJar, requestTestResults: mutatedContext.requestTestResults, - userUploadEnv: mutatedContext.userUploadEnv, + userUploadEnvironment: mutatedContext.userUploadEnvironment, execution: mutatedContext.execution, }; }; @@ -294,7 +294,7 @@ export async function savePatchesMadeByScript( } export const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse) => { - const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals, userUploadEnv, iteration, iterationCount, ancestors, eventName } = context; + const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals, userUploadEnvironment, iteration, iterationCount, ancestors, eventName } = context; invariant(script, 'script must be provided'); const settings = await models.settings.get(); @@ -334,9 +334,9 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe }, response, globals: globals?.data || undefined, - iterationData: userUploadEnv ? { - name: userUploadEnv.name, - data: userUploadEnv.data || {}, + iterationData: userUploadEnvironment ? { + name: userUploadEnvironment.name, + data: userUploadEnvironment.data || {}, } : undefined, execution: { location: requestLocation, @@ -374,14 +374,14 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe globals.dataPropertyOrder = globalEnvPropertyOrder.map; } - if (userUploadEnv) { + if (userUploadEnvironment) { const userUploadEnvPropertyOrder = orderedJSON.parse( JSON.stringify(output?.iterationData?.data || {}), JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR, ); - userUploadEnv.data = output?.iterationData?.data || {}; - userUploadEnv.dataPropertyOrder = userUploadEnvPropertyOrder.map; + userUploadEnvironment.data = output?.iterationData?.data || {}; + userUploadEnvironment.dataPropertyOrder = userUploadEnvPropertyOrder.map; } return { @@ -392,7 +392,7 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe clientCertificates: output.clientCertificates, cookieJar: output.cookieJar, globals, - userUploadEnv, + userUploadEnvironment, requestTestResults: output.requestTestResults, execution: output.execution, }; @@ -440,7 +440,7 @@ type RequestAndContextAndResponse = RequestContextForScript & { type RequestAndContextAndOptionalResponse = RequestContextForScript & { script: string; response?: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; iteration?: number; iterationCount?: number; eventName?: RequestContext['requestInfo']['eventName']; @@ -488,7 +488,7 @@ export const tryToInterpolateRequest = async ({ purpose, extraInfo, baseEnvironment, - userUploadEnv, + userUploadEnvironment, ignoreUndefinedEnvVariable, }: { request: Request; @@ -496,7 +496,7 @@ export const tryToInterpolateRequest = async ({ purpose?: RenderPurpose; extraInfo?: ExtraRenderInfo; baseEnvironment?: Environment; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; ignoreUndefinedEnvVariable?: boolean; } ) => { @@ -505,7 +505,7 @@ export const tryToInterpolateRequest = async ({ request: request, environment, baseEnvironment, - userUploadEnv, + userUploadEnvironment, purpose, extraInfo, ignoreUndefinedEnvVariable, diff --git a/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx b/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx index c4226fb65a..6c0dff1315 100644 --- a/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx @@ -6,14 +6,14 @@ import type { WorkspaceLoaderData } from '../../routes/workspace'; import { CopyButton } from '../base/copy-button'; import { Icon } from '../icon'; -export const CLIPreviewModal = ({ onClose, requestIds, allSelected, iterations, delay, filePath }: { onClose: () => void; requestIds: string[]; allSelected: boolean; iterations: number; delay: number; filePath: string }) => { +export const CLIPreviewModal = ({ onClose, requestIds, allSelected, iterationCount, delay, filePath }: { onClose: () => void; requestIds: string[]; allSelected: boolean; iterationCount: number; delay: number; filePath: string }) => { const { workspaceId } = useParams() as { workspaceId: string }; const { activeEnvironment } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const workspaceIdOrRequestIds = allSelected ? workspaceId.slice(0, 10) : '-i ' + requestIds.join(' -i '); - const iterationsArgument = iterations > 1 ? ` -n ${iterations}` : ''; + const iterationCountArgument = iterationCount > 1 ? ` -n ${iterationCount}` : ''; const delayArgument = delay > 0 ? ` --delay-request ${delay}` : ''; const iterationFilePath = filePath ? ` -d "${filePath}"` : ''; - const cliCommand = `inso run collection ${workspaceIdOrRequestIds} -e ${activeEnvironment._id.slice(0, 10)}${iterationsArgument}${delayArgument}${iterationFilePath}`; + const cliCommand = `inso run collection ${workspaceIdOrRequestIds} -e ${activeEnvironment._id.slice(0, 10)}${iterationCountArgument}${delayArgument}${iterationFilePath}`; return ( (); + +export interface LocalStorageOptions { + defaultValue?: T | (() => T); + storageSync?: boolean; + serializer?: { + stringify: (value: unknown) => string; + parse: (value: string) => unknown; + }; +}; + +// - `useLocalStorageState()` return type +// - first two values are the same as `useState` +export type LocalStorageState = [ + T, + Dispatch>, + { + isPersistent: boolean; + removeItem: () => void; + }, +]; + +export default function useLocalStorageState( + key: string, + options?: LocalStorageOptions, +): LocalStorageState; +export default function useLocalStorageState( + key: string, + options?: Omit, 'defaultValue'>, +): LocalStorageState; +export default function useLocalStorageState( + key: string, + options?: LocalStorageOptions, +): LocalStorageState; +export default function useLocalStorageState( + key: string, + options?: LocalStorageOptions, +): LocalStorageState { + const serializer = options?.serializer; + const [defaultValue] = useState(options?.defaultValue); + return useLocalStorage( + key, + defaultValue, + options?.storageSync, + serializer?.parse, + serializer?.stringify, + ); +} + +function useLocalStorage( + key: string, + defaultValue: T | undefined, + storageSync: boolean = true, + parse: (value: string) => unknown = parseJSON, + stringify: (value: unknown) => string = JSON.stringify, +): LocalStorageState { + // we keep the `parsed` value in a ref because `useSyncExternalStore` requires a cached version + const storageItem = useRef<{ string: string | null; parsed: T | undefined }>({ + string: null, + parsed: undefined, + }); + + const value = useSyncExternalStore( + // useSyncExternalStore.subscribe + useCallback( + onStoreChange => { + const onChange = (localKey: string): void => { + if (key === localKey) { + onStoreChange(); + } + }; + callbacks.add(onChange); + return (): void => { + callbacks.delete(onChange); + }; + }, + [key], + ), + + // useSyncExternalStore.getSnapshot + () => { + const string = goodTry(() => localStorage.getItem(key)) ?? null; + + if (inMemoryData.has(key)) { + storageItem.current.parsed = inMemoryData.get(key) as T | undefined; + } else if (string !== storageItem.current.string) { + let parsed: T | undefined; + + try { + parsed = string === null ? defaultValue : (parse(string) as T); + } catch { + parsed = defaultValue; + } + + storageItem.current.parsed = parsed; + } + + storageItem.current.string = string; + + // store default value in localStorage: + // - initial issue: https://github.com/astoilkov/use-local-storage-state/issues/26 + // issues that were caused by incorrect initial and secondary implementations: + // - https://github.com/astoilkov/use-local-storage-state/issues/30 + // - https://github.com/astoilkov/use-local-storage-state/issues/33 + if (defaultValue !== undefined && string === null) { + // reasons for `localStorage` to throw an error: + // - maximum quota is exceeded + // - under Mobile Safari (since iOS 5) when the user enters private mode + // `localStorage.setItem()` will throw + // - trying to access localStorage object when cookies are disabled in Safari throws + // "SecurityError: The operation is insecure." + // eslint-disable-next-line no-console + goodTry(() => { + const string = stringify(defaultValue); + localStorage.setItem(key, string); + storageItem.current = { string, parsed: defaultValue }; + }); + } + + return storageItem.current.parsed; + }, + + // useSyncExternalStore.getServerSnapshot + () => defaultValue, + ); + const setState = useCallback( + (newValue: SetStateAction): void => { + const value = + newValue instanceof Function ? newValue(storageItem.current.parsed) : newValue; + + // reasons for `localStorage` to throw an error: + // - maximum quota is exceeded + // - under Mobile Safari (since iOS 5) when the user enters private mode + // `localStorage.setItem()` will throw + // - trying to access `localStorage` object when cookies are disabled in Safari throws + // "SecurityError: The operation is insecure." + try { + localStorage.setItem(key, stringify(value)); + + inMemoryData.delete(key); + } catch { + inMemoryData.set(key, value); + } + + triggerCallbacks(key); + }, + [key, stringify], + ); + const removeItem = useCallback(() => { + goodTry(() => localStorage.removeItem(key)); + + inMemoryData.delete(key); + + triggerCallbacks(key); + }, [key]); + + // - syncs change across tabs, windows, iframes + // - the `storage` event is called only in all tabs, windows, iframe's except the one that + // triggered the change + useEffect(() => { + if (!storageSync) { + return undefined; + } + + const onStorage = (e: StorageEvent): void => { + if (e.key === key && e.storageArea === goodTry(() => localStorage)) { + triggerCallbacks(key); + } + }; + + window.addEventListener('storage', onStorage); + + return (): void => window.removeEventListener('storage', onStorage); + }, [key, storageSync]); + + return useMemo( + () => [ + value, + setState, + { + isPersistent: value === defaultValue || !inMemoryData.has(key), + removeItem, + }, + ], + [key, setState, value, defaultValue, removeItem], + ); +} + +// notifies all instances using the same `key` to update +const callbacks = new Set<(key: string) => void>(); +function triggerCallbacks(key: string): void { + for (const callback of [...callbacks]) { + callback(key); + } +} + +// a wrapper for `JSON.parse()` that supports "undefined" value. otherwise, +// `JSON.parse(JSON.stringify(undefined))` returns the string "undefined" not the value `undefined` +function parseJSON(value: string): unknown { + return value === 'undefined' ? undefined : JSON.parse(value); +} + +function goodTry(tryFn: () => T): T | undefined { + try { + return tryFn(); + } catch { + return undefined; + } +} diff --git a/packages/insomnia/src/ui/routes/request.tsx b/packages/insomnia/src/ui/routes/request.tsx index 3cdd776893..b15b2d733e 100644 --- a/packages/insomnia/src/ui/routes/request.tsx +++ b/packages/insomnia/src/ui/routes/request.tsx @@ -26,6 +26,7 @@ import { getPathParametersFromUrl, isEventStreamRequest, isRequest, type Request import { isRequestMeta, type RequestMeta } from '../../models/request-meta'; import type { RequestVersion } from '../../models/request-version'; import type { Response } from '../../models/response'; +import type { ResponseInfo, RunnerResultPerRequestPerIteration } from '../../models/runner-test-result'; import { isWebSocketRequest, isWebSocketRequestId, type WebSocketRequest } from '../../models/websocket-request'; import type { WebSocketResponse } from '../../models/websocket-response'; import { getAuthHeader } from '../../network/authentication'; @@ -405,13 +406,14 @@ export type RunnerSource = 'runner'; export interface CollectionRunnerContext { source: RunnerSource; environmentId: string; - iterations: number; + iterationCount: number; iterationData: object; duration: number; // millisecond testCount: number; avgRespTime: number; // millisecond - results: RequestTestResult[]; + iterationResults: RunnerResultPerRequestPerIteration; done: boolean; + responsesInfo: ResponseInfo[]; } export interface RunnerContextForRequest { @@ -428,7 +430,7 @@ export interface RunnerContextForRequest { export const sendActionImp = async ({ requestId, workspaceId, - userUploadEnv, + userUploadEnvironment, shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable, testResultCollector, @@ -442,12 +444,12 @@ export const sendActionImp = async ({ testResultCollector?: RunnerContextForRequest; iteration?: number; iterationCount?: number; - userUploadEnv?: UserUploadEnvironment; + userUploadEnvironment?: UserUploadEnvironment; }) => { window.main.startExecution({ requestId }); const requestData = await fetchRequestData(requestId); window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' }); - const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId, userUploadEnv, iteration, iterationCount); + const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId, userUploadEnvironment, iteration, iterationCount); if ('error' in mutatedContext) { throw { error: mutatedContext.error, @@ -484,7 +486,7 @@ export const sendActionImp = async ({ purpose: 'send', extraInfo: undefined, baseEnvironment: mutatedContext.baseEnvironment, - userUploadEnv: mutatedContext.userUploadEnv, + userUploadEnvironment: mutatedContext.userUploadEnvironment, ignoreUndefinedEnvVariable, }); const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult); diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 04fdb034f6..fbfc859988 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -1,4 +1,4 @@ -import type { RequestContext, RequestTestResult } from 'insomnia-sdk'; +import type { RequestContext } from 'insomnia-sdk'; import porderedJSON from 'json-order'; import React, { type FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components'; @@ -15,7 +15,7 @@ import * as models from '../../models'; import type { UserUploadEnvironment } from '../../models/environment'; import { isRequest, type Request } from '../../models/request'; import { isRequestGroup } from '../../models/request-group'; -import type { ResponseInfo, RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result'; +import type { RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result'; import { cancelRequestById } from '../../network/cancellation'; import { invariant } from '../../utils/invariant'; import { SegmentEvent } from '../analytics'; @@ -32,8 +32,9 @@ import { RunnerTestResultPane } from '../components/panes/runner-test-result-pan import { ResponseTimer } from '../components/response-timer'; import { getTimeAndUnit } from '../components/tags/time-tag'; import { ResponseTimelineViewer } from '../components/viewers/response-timeline-viewer'; +import useLocalStorage from '../hooks/use-local-storage'; import type { OrganizationLoaderData } from './organization'; -import { type RunnerSource, sendActionImp } from './request'; +import { type CollectionRunnerContext, type RunnerContextForRequest, type RunnerSource, sendActionImp } from './request'; import { useRootLoaderData } from './root'; import type { Child, WorkspaceLoaderData } from './workspace'; @@ -86,21 +87,6 @@ async function aggregateAllTimelines(errorMsg: string | null, testResult: Runner return timelines; } -interface RunnerSettings { - iterations: number; - delay: number; - iterationData: UploadDataType[]; - file: File | null; -} - -// TODO: remove this when the suite management is introduced -let tempRunnerSettings: RunnerSettings = { - iterations: 1, - delay: 0, - iterationData: [], - file: null, -}; - export const Runner: FC<{}> = () => { const [searchParams, setSearchParams] = useSearchParams(); const [shouldRefresh, setShouldRefresh] = useState(false); @@ -133,17 +119,18 @@ export const Runner: FC<{}> = () => { setSearchParams({}); } - const [iterations, setIterations] = useState(tempRunnerSettings?.iterations || 1); - const [delay, setDelay] = useState(tempRunnerSettings?.delay || 0); - const [uploadData, setUploadData] = useState(tempRunnerSettings?.iterationData || []); - const [file, setFile] = useState(tempRunnerSettings?.file || null); - const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; direction: 'vertical' | 'horizontal'; }; + const localStorageKey = workspaceId + 'runnerSettings'; + const [iterationCount, setIterationCount] = useLocalStorage(localStorageKey + 'iterationCount', { defaultValue: 1 }); + const [delay, setDelay] = useLocalStorage(localStorageKey + 'delay', { defaultValue: 0 }); + const [uploadData, setUploadData] = useLocalStorage(localStorageKey + 'iterationData', { defaultValue: [] }); + const [file, setFile] = useLocalStorage(localStorageKey + 'file', { defaultValue: null }); + invariant(iterationCount, 'iterationCount should not be null'); const { settings } = useRootLoaderData(); const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const [showUploadModal, setShowUploadModal] = useState(false); @@ -251,7 +238,7 @@ export const Runner: FC<{}> = () => { } setIsRunning(true); - window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterations } }); + window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterationCount } }); const selected = new Set(reqList.selectedKeys); const requests = Array.from(reqList.items) .filter(item => selected.has(item.id)); @@ -272,7 +259,7 @@ export const Runner: FC<{}> = () => { submit( { requests, - iterations, + iterationCount, userUploadEnvs, delay, }, @@ -309,14 +296,9 @@ export const Runner: FC<{}> = () => { useEffect(() => { if (uploadData.length >= 1) { - // update iteration number from upload data length - setIterations(uploadData.length); // also update the temp settings - tempRunnerSettings = { - ...tempRunnerSettings, - iterations: uploadData.length, - }; + setIterationCount(uploadData.length); } - }, [uploadData]); + }, [setIterationCount, uploadData]); const [isRunning, setIsRunning] = useState(false); const [timingSteps, setTimingSteps] = useState([]); @@ -428,22 +410,15 @@ export const Runner: FC<{}> = () => {
{ try { - const iterCount = parseInt(e.target.value, 10); - if (iterCount > 0) { - setIterations(iterCount); // also update the temp settings - tempRunnerSettings = { - ...tempRunnerSettings, - iterations: iterCount, - }; + if (parseInt(e.target.value, 10) > 0) { + setIterationCount(parseInt(e.target.value, 10)); } - } catch (ex) { - // no op - } + } catch (ex) { } }} type='number' className={iterationInputStyle} @@ -460,10 +435,6 @@ export const Runner: FC<{}> = () => { const delay = parseInt(e.target.value, 10); if (delay >= 0) { setDelay(delay); // also update the temp settings - tempRunnerSettings = { - ...tempRunnerSettings, - delay, - }; } } catch (ex) { // no op @@ -682,7 +653,7 @@ export const Runner: FC<{}> = () => { onClose={() => setShowCLIModal(false)} requestIds={Array.from(reqList.selectedKeys) as string[]} allSelected={Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length} - iterations={iterations} + iterationCount={iterationCount} delay={delay} filePath={file?.path || ''} /> @@ -692,11 +663,6 @@ export const Runner: FC<{}> = () => { onUploadFile={(file, uploadData) => { setFile(file); setUploadData(uploadData); // also update the temp settings - tempRunnerSettings = { - ...tempRunnerSettings, - iterationData: uploadData, - file, - }; }} userUploadData={uploadData} onClose={() => setShowUploadModal(false)} @@ -860,20 +826,20 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = invariant(organizationId, 'Organization id is required'); invariant(projectId, 'Project id is required'); invariant(workspaceId, 'Workspace id is required'); - const { requests, iterations, delay, userUploadEnvs } = await request.json(); + const { requests, iterationCount, delay, userUploadEnvs } = await request.json(); const source: RunnerSource = 'runner'; - let testCtx = { + let testCtx: CollectionRunnerContext = { source, environmentId: '', - iterations, + iterationCount, iterationData: userUploadEnvs, - duration: 1, // TODO: disable this + duration: 0, testCount: 0, avgRespTime: 0, - iterationResults: new Array(), + iterationResults: [], done: false, - responsesInfo: new Array(), + responsesInfo: [], }; window.main.startExecution({ requestId: workspaceId }); @@ -890,11 +856,11 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = }; try { - for (let i = 0; i < iterations; i++) { + for (let i = 0; i < iterationCount; i++) { // nextRequestIdOrName is used to manual set next request in iteration from pre-request script let nextRequestIdOrName = ''; - let iterationResults: RunnerResultPerRequest[] = []; + let testResultsForOneIteration: RunnerResultPerRequest[] = []; for (let j = 0; j < requests.length; j++) { const targetRequest = requests[j] as RequestType; @@ -925,22 +891,22 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = invariant(activeRequestMeta, 'Request meta not found'); await new Promise(resolve => setTimeout(resolve, delay)); - const resultCollector = { + const resultCollector: RunnerContextForRequest = { requestId: targetRequest.id, requestName: targetRequest.name, requestUrl: targetRequest.url, responseReason: '', - duration: 1, + duration: 0, size: 0, - results: new Array(), + results: [], responseId: '', }; const mutatedContext = await sendActionImp({ requestId: targetRequest.id, workspaceId, iteration: i + 1, - iterationCount: iterations, - userUploadEnv: wrapAroundIterationOverIterationData(userUploadEnvs, i), + iterationCount, + userUploadEnvironment: wrapAroundIterationOverIterationData(userUploadEnvs, i), shouldPromptForPathAfterResponse: false, ignoreUndefinedEnvVariable: true, testResultCollector: resultCollector, @@ -956,7 +922,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = results: resultCollector.results, }; - iterationResults = [...iterationResults, requestResults]; + testResultsForOneIteration = [...testResultsForOneIteration, requestResults]; testCtx = { ...testCtx, duration: testCtx.duration + resultCollector.duration, @@ -973,7 +939,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = testCtx = { ...testCtx, - iterationResults: [...testCtx.iterationResults, iterationResults], + iterationResults: [...testCtx.iterationResults, testResultsForOneIteration], }; } @@ -989,8 +955,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = await models.runnerTestResult.create({ parentId: workspaceId, source: testCtx.source, - // environmentId: string; - iterations: testCtx.iterations, + iterations: testCtx.iterationCount, duration: testCtx.duration, avgRespTime: testCtx.avgRespTime, iterationResults: testCtx.iterationResults,