mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-30 11:03:00 -04:00
runner refactoring pass (#7975)
* rename iterations to iterationCount * make duration 0 * use local storage * raise orderedJSON * rename * fix cli
This commit is contained in:
@@ -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<Record<string, string>[]> => {
|
||||
const pathToIterationData = async (pathOrUrl: string): Promise<UserUploadEnvironment[]> => {
|
||||
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<string, string>[] => {
|
||||
if (fileType === 'json') {
|
||||
try {
|
||||
const jsonDataContent = JSON.parse(content);
|
||||
@@ -267,6 +273,21 @@ const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise<Record<
|
||||
throw new Error(`Uploaded file is unsupported ${fileType}`);
|
||||
};
|
||||
|
||||
const transformIterationDataToEnvironmentList = (list: Record<string, string>[]): UserUploadEnvironment[] => {
|
||||
return list?.map(data => {
|
||||
const orderedJson = orderedJSON.parse<Record<string, any>>(
|
||||
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++) {
|
||||
|
||||
@@ -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<string, any>;
|
||||
},
|
||||
) {
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>[], 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<Record<string, any>>(
|
||||
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
|
||||
|
||||
@@ -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<Environment, 'data' | 'dataPropertyOrder' | 'name'>;
|
||||
|
||||
export const isEnvironment = (model: Pick<BaseModel, 'type'>): model is Environment => (
|
||||
|
||||
@@ -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<BaseModel, 'type'>): model is Run
|
||||
export function init() {
|
||||
return {
|
||||
source: 'runner',
|
||||
// environmentId: string;
|
||||
iterations: 0,
|
||||
duration: 0,
|
||||
avgRespTime: 0,
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface TransformedExecuteScriptContext {
|
||||
globals?: Environment;
|
||||
cookieJar: CookieJar;
|
||||
requestTestResults?: RequestTestResult[];
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
userUploadEnvironment?: UserUploadEnvironment;
|
||||
}
|
||||
|
||||
interface Task {
|
||||
|
||||
@@ -155,7 +155,7 @@ export const tryToExecutePreRequestScript = async (
|
||||
ancestors,
|
||||
}: Awaited<ReturnType<typeof fetchRequestData>>,
|
||||
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<RequestTestResult>(),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<ModalOverlay
|
||||
|
||||
214
packages/insomnia/src/ui/hooks/use-local-storage.ts
Normal file
214
packages/insomnia/src/ui/hooks/use-local-storage.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
// Taken from https://github.com/astoilkov/use-local-storage-state/blob/main/src/useLocalStorageState.ts
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
||||
|
||||
// in memory fallback used when `localStorage` throws an error
|
||||
export const inMemoryData = new Map<string, unknown>();
|
||||
|
||||
export interface LocalStorageOptions<T> {
|
||||
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> = [
|
||||
T,
|
||||
Dispatch<SetStateAction<T>>,
|
||||
{
|
||||
isPersistent: boolean;
|
||||
removeItem: () => void;
|
||||
},
|
||||
];
|
||||
|
||||
export default function useLocalStorageState(
|
||||
key: string,
|
||||
options?: LocalStorageOptions<undefined>,
|
||||
): LocalStorageState<unknown>;
|
||||
export default function useLocalStorageState<T>(
|
||||
key: string,
|
||||
options?: Omit<LocalStorageOptions<T | undefined>, 'defaultValue'>,
|
||||
): LocalStorageState<T | undefined>;
|
||||
export default function useLocalStorageState<T>(
|
||||
key: string,
|
||||
options?: LocalStorageOptions<T>,
|
||||
): LocalStorageState<T>;
|
||||
export default function useLocalStorageState<T = undefined>(
|
||||
key: string,
|
||||
options?: LocalStorageOptions<T | undefined>,
|
||||
): LocalStorageState<T | undefined> {
|
||||
const serializer = options?.serializer;
|
||||
const [defaultValue] = useState(options?.defaultValue);
|
||||
return useLocalStorage(
|
||||
key,
|
||||
defaultValue,
|
||||
options?.storageSync,
|
||||
serializer?.parse,
|
||||
serializer?.stringify,
|
||||
);
|
||||
}
|
||||
|
||||
function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T | undefined,
|
||||
storageSync: boolean = true,
|
||||
parse: (value: string) => unknown = parseJSON,
|
||||
stringify: (value: unknown) => string = JSON.stringify,
|
||||
): LocalStorageState<T | undefined> {
|
||||
// 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<T | undefined>): 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<T>(tryFn: () => T): T | undefined {
|
||||
try {
|
||||
return tryFn();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<UploadDataType[]>(tempRunnerSettings?.iterationData || []);
|
||||
const [file, setFile] = useState<File | null>(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<number>(localStorageKey + 'iterationCount', { defaultValue: 1 });
|
||||
const [delay, setDelay] = useLocalStorage<number>(localStorageKey + 'delay', { defaultValue: 0 });
|
||||
const [uploadData, setUploadData] = useLocalStorage<UploadDataType[]>(localStorageKey + 'iterationData', { defaultValue: [] });
|
||||
const [file, setFile] = useLocalStorage<File | null>(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<TimingStep[]>([]);
|
||||
@@ -428,22 +410,15 @@ export const Runner: FC<{}> = () => {
|
||||
<div className="h-full min-w-[500px]">
|
||||
<span className="mr-6 text-sm">
|
||||
<input
|
||||
value={iterations}
|
||||
value={iterationCount}
|
||||
name='Iterations'
|
||||
disabled={isRunning}
|
||||
onChange={e => {
|
||||
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<RunnerResultPerRequest[]>(),
|
||||
iterationResults: [],
|
||||
done: false,
|
||||
responsesInfo: new Array<ResponseInfo>(),
|
||||
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<RequestTestResult>(),
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user