feat: add collection-level pre-request and test scripts (#5745)
Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: “mirarifhasan” <arif.ishan05@gmail.com> Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
00c75b9de0
commit
696ddc336c
75 changed files with 2793 additions and 307 deletions
|
|
@ -2,6 +2,7 @@ export enum PrismaError {
|
|||
DATABASE_UNREACHABLE = 'P1001',
|
||||
TABLE_DOES_NOT_EXIST = 'P2021',
|
||||
UNIQUE_CONSTRAINT_VIOLATION = 'P2002',
|
||||
RECORD_NOT_FOUND = 'P2025',
|
||||
TRANSACTION_TIMEOUT = 'P2028',
|
||||
TRANSACTION_DEADLOCK = 'P2034', // write conflict or a deadlock
|
||||
}
|
||||
|
|
|
|||
|
|
@ -569,23 +569,24 @@ export class TeamCollectionService {
|
|||
collection.parentID,
|
||||
);
|
||||
|
||||
const deletedCollection = await tx.teamCollection.delete({
|
||||
where: { id: collection.id },
|
||||
});
|
||||
|
||||
// if collection is deleted, update siblings orderIndexes
|
||||
// if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if (deletedCollection) {
|
||||
// update siblings orderIndexes
|
||||
await tx.teamCollection.updateMany({
|
||||
where: {
|
||||
teamID: collection.teamID,
|
||||
parentID: collection.parentID,
|
||||
orderIndex: orderIndexCondition,
|
||||
},
|
||||
data: { orderIndex: dataCondition },
|
||||
try {
|
||||
await tx.teamCollection.delete({
|
||||
where: { id: collection.id },
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// P2025: Record not found — already deleted by a concurrent transaction
|
||||
if (deleteError?.code === PrismaError.RECORD_NOT_FOUND) return;
|
||||
throw deleteError;
|
||||
}
|
||||
|
||||
await tx.teamCollection.updateMany({
|
||||
where: {
|
||||
teamID: collection.teamID,
|
||||
parentID: collection.parentID,
|
||||
orderIndex: orderIndexCondition,
|
||||
},
|
||||
data: { orderIndex: dataCondition },
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
@ -749,12 +750,7 @@ export class TeamCollectionService {
|
|||
// Get collection details of collectionID
|
||||
const collection = await this.getCollection(collectionID, tx);
|
||||
if (E.isLeft(collection)) return E.left(collection.left);
|
||||
// lock the rows of the collection and its siblings
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
collection.right.teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
|
||||
// destCollectionID == null i.e move collection to root
|
||||
if (!destCollectionID) {
|
||||
if (!collection.right.parentID) {
|
||||
|
|
@ -763,6 +759,12 @@ export class TeamCollectionService {
|
|||
return E.left(TEAM_COL_ALREADY_ROOT);
|
||||
}
|
||||
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
collection.right.teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
|
||||
// Change parent from child to root i.e child collection becomes a root collection
|
||||
// Move child collection into root and update orderIndexes for root teamCollections
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -806,12 +808,41 @@ export class TeamCollectionService {
|
|||
return E.left(TEAM_COLL_IS_PARENT_COLL);
|
||||
}
|
||||
|
||||
// lock the rows of the destination collection and its siblings
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
destCollection.right.teamID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
// Acquire locks in deterministic order (sorted by parentID) to prevent deadlocks
|
||||
// when two concurrent moves happen in opposite directions
|
||||
const srcParentID = collection.right.parentID ?? '';
|
||||
const destParentID = destCollection.right.parentID ?? '';
|
||||
const teamID = collection.right.teamID;
|
||||
|
||||
if (srcParentID === destParentID) {
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
} else if (srcParentID < destParentID) {
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
} else {
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
}
|
||||
|
||||
// Change parent from null to teamCollection i.e collection becomes a child collection
|
||||
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
TeamRequest as DbTeamRequest,
|
||||
} from 'src/generated/prisma/client';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
import { PrismaError } from 'src/prisma/prisma-error-codes';
|
||||
|
||||
@Injectable()
|
||||
export class TeamRequestService {
|
||||
|
|
@ -124,21 +125,23 @@ export class TeamRequestService {
|
|||
dbTeamReq.collectionID,
|
||||
]);
|
||||
|
||||
const deletedTeamRequest = await tx.teamRequest.delete({
|
||||
where: { id: requestID },
|
||||
});
|
||||
|
||||
// if request is deleted, update orderIndexes of siblings
|
||||
// if request was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if (deletedTeamRequest) {
|
||||
await tx.teamRequest.updateMany({
|
||||
where: {
|
||||
collectionID: dbTeamReq.collectionID,
|
||||
orderIndex: { gte: dbTeamReq.orderIndex },
|
||||
},
|
||||
data: { orderIndex: { decrement: 1 } },
|
||||
try {
|
||||
await tx.teamRequest.delete({
|
||||
where: { id: requestID },
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// P2025: Record not found — already deleted by a concurrent transaction
|
||||
if (deleteError?.code === PrismaError.RECORD_NOT_FOUND) return;
|
||||
throw deleteError;
|
||||
}
|
||||
|
||||
await tx.teamRequest.updateMany({
|
||||
where: {
|
||||
collectionID: dbTeamReq.collectionID,
|
||||
orderIndex: { gte: dbTeamReq.orderIndex },
|
||||
},
|
||||
data: { orderIndex: { decrement: 1 } },
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2380,3 +2380,165 @@ describe('exportUserCollectionToJSONObject', () => {
|
|||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importCollectionsFromJSON — collection-level script fields', () => {
|
||||
// The backend treats `data` as an opaque JSON blob, so the script fields
|
||||
// ride through transparently. The test asserts on both ends: the create
|
||||
// call payload must carry script fields (proving import wrote them), and
|
||||
// the export payload must surface them unchanged. Guards against any
|
||||
// future refactor that destructures `data` and drops scripts on either
|
||||
// side.
|
||||
test('preRequestScript and testScript on root and folder survive import → export round-trip', async () => {
|
||||
const importJSON = JSON.stringify([
|
||||
{
|
||||
name: 'root-with-scripts',
|
||||
folders: [
|
||||
{
|
||||
name: 'child-folder',
|
||||
folders: [],
|
||||
requests: [],
|
||||
data: JSON.stringify({
|
||||
auth: { authType: 'inherit', authActive: true },
|
||||
headers: [],
|
||||
variables: [],
|
||||
preRequestScript: 'pw.env.set("FOLDER_RAN", "yes");',
|
||||
testScript: 'pw.test("folder", () => {});',
|
||||
}),
|
||||
},
|
||||
],
|
||||
requests: [],
|
||||
data: JSON.stringify({
|
||||
auth: { authType: 'none', authActive: false },
|
||||
headers: [],
|
||||
variables: [],
|
||||
preRequestScript: 'pw.env.set("ROOT_RAN", "yes");',
|
||||
testScript: 'pw.test("root", () => {});',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const rootRowId = 'imported-root-id';
|
||||
const folderRowId = 'imported-folder-id';
|
||||
|
||||
// Capture what generatePrismaQueryObj writes into Prisma so the export
|
||||
// path sees the same blob shape the import wrote.
|
||||
const rootDataAtCreate = {
|
||||
auth: { authType: 'none', authActive: false },
|
||||
headers: [],
|
||||
variables: [],
|
||||
preRequestScript: 'pw.env.set("ROOT_RAN", "yes");',
|
||||
testScript: 'pw.test("root", () => {});',
|
||||
};
|
||||
const folderDataAtCreate = {
|
||||
auth: { authType: 'inherit', authActive: true },
|
||||
headers: [],
|
||||
variables: [],
|
||||
preRequestScript: 'pw.env.set("FOLDER_RAN", "yes");',
|
||||
testScript: 'pw.test("folder", () => {});',
|
||||
};
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));
|
||||
mockPrisma.lockUserCollectionByParent.mockResolvedValue(undefined);
|
||||
mockPrisma.userCollection.findFirst.mockResolvedValueOnce(null);
|
||||
mockPrisma.userCollection.create.mockResolvedValueOnce({
|
||||
id: rootRowId,
|
||||
orderIndex: 1,
|
||||
parentID: null,
|
||||
title: 'root-with-scripts',
|
||||
userUid: user.uid,
|
||||
type: ReqType.REST,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
data: rootDataAtCreate,
|
||||
});
|
||||
|
||||
// Export-side mocks: root resolves once, then its child folder resolves.
|
||||
mockPrisma.userCollection.findUniqueOrThrow
|
||||
.mockResolvedValueOnce({
|
||||
id: rootRowId,
|
||||
orderIndex: 1,
|
||||
parentID: null,
|
||||
title: 'root-with-scripts',
|
||||
userUid: user.uid,
|
||||
type: ReqType.REST,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
data: rootDataAtCreate,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: folderRowId,
|
||||
orderIndex: 1,
|
||||
parentID: rootRowId,
|
||||
title: 'child-folder',
|
||||
userUid: user.uid,
|
||||
type: ReqType.REST,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
data: folderDataAtCreate,
|
||||
});
|
||||
|
||||
mockPrisma.userCollection.findMany
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: folderRowId,
|
||||
orderIndex: 1,
|
||||
parentID: rootRowId,
|
||||
title: 'child-folder',
|
||||
userUid: user.uid,
|
||||
type: ReqType.REST,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
data: folderDataAtCreate,
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
mockPrisma.userRequest.findMany
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userCollectionService.importCollectionsFromJSON(
|
||||
importJSON,
|
||||
user.uid,
|
||||
null,
|
||||
ReqType.REST,
|
||||
);
|
||||
|
||||
expect(E.isRight(result)).toBe(true);
|
||||
|
||||
// Import side: `userCollection.create` must receive script fields inside
|
||||
// its `data` payload (root) and inside `children.create[0].data` (folder).
|
||||
// Asserting against the create call args proves import preserved scripts;
|
||||
// export-side mocks alone would only round-trip the values we pre-loaded.
|
||||
const createCallArg = mockPrisma.userCollection.create.mock.calls[0][0]
|
||||
.data as any;
|
||||
expect(createCallArg.data.preRequestScript).toBe(
|
||||
'pw.env.set("ROOT_RAN", "yes");',
|
||||
);
|
||||
expect(createCallArg.data.testScript).toBe(
|
||||
'pw.test("root", () => {});',
|
||||
);
|
||||
const childCreateArg = createCallArg.children.create[0];
|
||||
expect(childCreateArg.data.preRequestScript).toBe(
|
||||
'pw.env.set("FOLDER_RAN", "yes");',
|
||||
);
|
||||
expect(childCreateArg.data.testScript).toBe(
|
||||
'pw.test("folder", () => {});',
|
||||
);
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const exported = JSON.parse(result.right.exportedCollection);
|
||||
// `data` is JSON-stringified by transformCollectionData on export.
|
||||
const rootData = JSON.parse(exported[0].data);
|
||||
const folderData = JSON.parse(exported[0].folders[0].data);
|
||||
expect(rootData.preRequestScript).toBe(
|
||||
'pw.env.set("ROOT_RAN", "yes");',
|
||||
);
|
||||
expect(rootData.testScript).toBe('pw.test("root", () => {});');
|
||||
expect(folderData.preRequestScript).toBe(
|
||||
'pw.env.set("FOLDER_RAN", "yes");',
|
||||
);
|
||||
expect(folderData.testScript).toBe('pw.test("folder", () => {});');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -520,23 +520,24 @@ export class UserCollectionService {
|
|||
collection.parentID,
|
||||
);
|
||||
|
||||
const deletedCollection = await tx.userCollection.delete({
|
||||
where: { id: collection.id },
|
||||
});
|
||||
|
||||
// if collection is deleted, update siblings orderIndexes
|
||||
// if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if (deletedCollection) {
|
||||
// update orderIndexes
|
||||
await tx.userCollection.updateMany({
|
||||
where: {
|
||||
userUid: collection.userUid,
|
||||
parentID: collection.parentID,
|
||||
orderIndex: orderIndexCondition,
|
||||
},
|
||||
data: { orderIndex: dataCondition },
|
||||
try {
|
||||
await tx.userCollection.delete({
|
||||
where: { id: collection.id },
|
||||
});
|
||||
} catch (deleteError) {
|
||||
// P2025: Record not found — already deleted by a concurrent transaction
|
||||
if (deleteError?.code === PrismaError.RECORD_NOT_FOUND) return;
|
||||
throw deleteError;
|
||||
}
|
||||
|
||||
await tx.userCollection.updateMany({
|
||||
where: {
|
||||
userUid: collection.userUid,
|
||||
parentID: collection.parentID,
|
||||
orderIndex: orderIndexCondition,
|
||||
},
|
||||
data: { orderIndex: dataCondition },
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
@ -596,6 +597,12 @@ export class UserCollectionService {
|
|||
return E.left(USER_COLL_ALREADY_ROOT);
|
||||
}
|
||||
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
|
||||
// Change parent from child to root i.e child collection becomes a root collection
|
||||
// Move child collection into root and update orderIndexes for child userCollections
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -643,7 +650,40 @@ export class UserCollectionService {
|
|||
return E.left(USER_COLL_IS_PARENT_COLL);
|
||||
}
|
||||
|
||||
// Change parent from null to teamCollection i.e collection becomes a child collection
|
||||
// Acquire locks in deterministic order (sorted by parentID) to prevent deadlocks
|
||||
const srcParentID = collection.right.parentID ?? '';
|
||||
const destParentID = destCollection.right.parentID ?? '';
|
||||
|
||||
if (srcParentID === destParentID) {
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
} else if (srcParentID < destParentID) {
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
} else {
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
}
|
||||
|
||||
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
tx,
|
||||
|
|
|
|||
|
|
@ -539,6 +539,21 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
|
|||
// Clean up
|
||||
fs.unlinkSync(junitPath);
|
||||
}, 600000); // 600 second (10 minute) timeout
|
||||
|
||||
test("Inherited collection-level scripts run in order across both sandboxes", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"collection-level-scripts-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
|
||||
const defaultResult = await runCLIWithNetworkRetry(args);
|
||||
if (defaultResult === null) return;
|
||||
expect(defaultResult.error).toBeNull();
|
||||
|
||||
const legacyResult = await runCLIWithNetworkRetry(`${args} --legacy-sandbox`);
|
||||
if (legacyResult === null) return;
|
||||
expect(legacyResult.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"v": 12,
|
||||
"name": "collection-level-scripts-coll",
|
||||
"variables": [],
|
||||
"description": null,
|
||||
"folders": [
|
||||
{
|
||||
"v": 12,
|
||||
"name": "target-folder",
|
||||
"variables": [],
|
||||
"description": null,
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "17",
|
||||
"id": "cl-script-req-1",
|
||||
"name": "target-request",
|
||||
"method": "GET",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"REQ_RAN\", \"yes\");\npw.env.set(\"PRE_ORDER\", pw.env.get(\"PRE_ORDER\") + \"->target-req\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", \"target-req\");\npw.env.set(\"ORDER_AT_REQ\", pw.env.get(\"TEST_ORDER\"));\npw.test(\"pre-script cascade ran in root->target-folder->target-req order\", () => {\n pw.expect(pw.env.get(\"PRE_ORDER\")).toBe(\"root->target-folder->target-req\");\n});\npw.test(\"all cascade pre-scripts committed env vars\", () => {\n pw.expect(pw.env.get(\"ROOT_RAN\")).toBe(\"yes\");\n pw.expect(pw.env.get(\"TARGET_FOLDER_RAN\")).toBe(\"yes\");\n pw.expect(pw.env.get(\"REQ_RAN\")).toBe(\"yes\");\n});\npw.test(\"request-level test observed request position in test-cascade\", () => {\n pw.expect(pw.env.get(\"ORDER_AT_REQ\")).toBe(\"target-req\");\n});",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "17",
|
||||
"id": "cl-script-req-2",
|
||||
"name": "sibling-request-in-target-folder",
|
||||
"method": "GET",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"PRE_ORDER\", pw.env.get(\"PRE_ORDER\") + \"->sibling-req-in-target\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", \"sibling-req-in-target\");\npw.test(\"sibling request cascade is root->target-folder->this-request\", () => {\n pw.expect(pw.env.get(\"PRE_ORDER\")).toBe(\"root->target-folder->sibling-req-in-target\");\n});",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"TARGET_FOLDER_RAN\", \"yes\");\npw.env.set(\"TARGET_FOLDER_RUN_COUNT\", String((parseInt(pw.env.get(\"TARGET_FOLDER_RUN_COUNT\") || \"0\", 10)) + 1));\npw.env.set(\"PRE_ORDER\", pw.env.get(\"PRE_ORDER\") + \"->target-folder\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", pw.env.get(\"TEST_ORDER\") + \"->target-folder\");\npw.env.set(\"ORDER_AT_TARGET_FOLDER\", pw.env.get(\"TEST_ORDER\"));"
|
||||
},
|
||||
{
|
||||
"v": 12,
|
||||
"name": "sibling-folder",
|
||||
"variables": [],
|
||||
"description": null,
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "17",
|
||||
"id": "cl-script-req-3",
|
||||
"name": "sibling-request-in-sibling-folder",
|
||||
"method": "GET",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"PRE_ORDER\", pw.env.get(\"PRE_ORDER\") + \"->sibling-req-in-sibling\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", \"sibling-req-in-sibling\");\npw.test(\"sibling-folder cascade is root->sibling-folder->this-request (no target-folder leak)\", () => {\n pw.expect(pw.env.get(\"PRE_ORDER\")).toBe(\"root->sibling-folder->sibling-req-in-sibling\");\n});\npw.test(\"target-folder pre-script ran exactly twice (one per request in target-folder)\", () => {\n pw.expect(pw.env.get(\"TARGET_FOLDER_RUN_COUNT\")).toBe(\"2\");\n});",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"SIBLING_FOLDER_RAN\", \"yes\");\npw.env.set(\"PRE_ORDER\", pw.env.get(\"PRE_ORDER\") + \"->sibling-folder\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", pw.env.get(\"TEST_ORDER\") + \"->sibling-folder\");"
|
||||
}
|
||||
],
|
||||
"requests": [],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": [],
|
||||
"preRequestScript": "pw.env.set(\"ROOT_RAN\", \"yes\");\npw.env.set(\"PRE_ORDER\", \"root\");",
|
||||
"testScript": "pw.env.set(\"TEST_ORDER\", pw.env.get(\"TEST_ORDER\") + \"->root\");\npw.test(\"test-script cascade ran in request->folder->root order for every request\", () => {\n pw.expect([\"target-req->target-folder->root\", \"sibling-req-in-target->target-folder->root\", \"sibling-req-in-sibling->sibling-folder->root\"].includes(pw.env.get(\"TEST_ORDER\"))).toBe(true);\n});"
|
||||
}
|
||||
|
|
@ -144,6 +144,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -192,6 +194,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -224,6 +228,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -273,6 +279,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -513,12 +521,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
name: "Multiple child collections with authorization, headers and variables set at each level",
|
||||
folders: [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fjgah000110f8a5bs68gd",
|
||||
name: "folder-1",
|
||||
folders: [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fjwmm000410f8l1gkkr1a",
|
||||
name: "folder-11",
|
||||
folders: [],
|
||||
|
|
@ -567,9 +575,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fjyxm000510f8pv90dt43",
|
||||
name: "folder-12",
|
||||
folders: [],
|
||||
|
|
@ -634,9 +644,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fk1cv000610f88kc3aupy",
|
||||
name: "folder-13",
|
||||
folders: [],
|
||||
|
|
@ -719,6 +731,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -764,14 +778,16 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fjk9o000210f8j0573pls",
|
||||
name: "folder-2",
|
||||
folders: [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fk516000710f87sfpw6bo",
|
||||
name: "folder-21",
|
||||
folders: [],
|
||||
|
|
@ -818,9 +834,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fk72t000810f8gfwkpi5y",
|
||||
name: "folder-22",
|
||||
folders: [],
|
||||
|
|
@ -885,9 +903,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fk95g000910f8bunhaoo8",
|
||||
name: "folder-23",
|
||||
folders: [],
|
||||
|
|
@ -957,6 +977,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -1008,15 +1030,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1fjmlq000310f86o4d3w2o",
|
||||
name: "folder-3",
|
||||
folders: [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1iwq0p003e10f8u8zg0p85",
|
||||
name: "folder-31",
|
||||
folders: [],
|
||||
|
|
@ -1063,9 +1087,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1izut7003m10f894ip59zg",
|
||||
name: "folder-32",
|
||||
folders: [],
|
||||
|
|
@ -1130,9 +1156,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
id: "clx1j2ka9003q10f8cdbzpgpg",
|
||||
name: "folder-33",
|
||||
folders: [],
|
||||
|
|
@ -1202,6 +1230,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -1266,6 +1296,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [
|
||||
|
|
@ -1321,6 +1353,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -1428,6 +1462,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: CollectionSchemaVersion,
|
||||
|
|
@ -1476,6 +1512,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: CollectionSchemaVersion,
|
||||
|
|
@ -1490,6 +1528,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
{
|
||||
v: CollectionSchemaVersion,
|
||||
|
|
@ -1518,6 +1558,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
|||
},
|
||||
],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
],
|
||||
requests: [],
|
||||
|
|
@ -1528,6 +1570,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { makeRESTRequest } from "@hoppscotch/data";
|
||||
import * as E from "fp-ts/Either";
|
||||
|
||||
import { preRequestScriptRunner } from "../../utils/pre-request";
|
||||
import { HoppEnvs } from "../../types/request";
|
||||
|
||||
const SAMPLE_ENVS: HoppEnvs = {
|
||||
global: [],
|
||||
selected: [],
|
||||
};
|
||||
|
||||
const SAMPLE_REQUEST = makeRESTRequest({
|
||||
name: "request",
|
||||
method: "GET",
|
||||
endpoint: "https://example.com",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
auth: { authActive: false, authType: "none" },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
requestVariables: [],
|
||||
description: null,
|
||||
responses: {},
|
||||
});
|
||||
|
||||
describe("preRequestScriptRunner - inheritance", () => {
|
||||
test("Inherited scripts execute in root → parent → request order", async () => {
|
||||
const rootScript = `pw.env.set("ORDER", "root");`;
|
||||
const parentScript = `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",parent");
|
||||
`;
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
preRequestScript: `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",request");
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
false,
|
||||
undefined,
|
||||
[rootScript, parentScript]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const orderVar = result.right.updatedEnvs.selected.find(
|
||||
(v) => v.key === "ORDER"
|
||||
);
|
||||
expect(orderVar?.currentValue).toBe("root,parent,request");
|
||||
}
|
||||
});
|
||||
|
||||
test("Inherited scripts set ENVs used in request endpoint resolution", async () => {
|
||||
const rootScript = `pw.env.set("ENDPOINT", "https://example.com");`;
|
||||
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
endpoint: "<<ENDPOINT>>",
|
||||
preRequestScript: "",
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
false,
|
||||
undefined,
|
||||
[rootScript]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
expect(result.right.effectiveRequest.effectiveFinalURL).toBe(
|
||||
"https://example.com"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Scripts with same local variable names do not collide (IIFE isolation)", async () => {
|
||||
const rootScript = `const x = "root"; pw.env.set("ROOT_VAR", x);`;
|
||||
const parentScript = `const x = "parent"; pw.env.set("PARENT_VAR", x);`;
|
||||
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
preRequestScript: `const x = "request"; pw.env.set("REQUEST_VAR", x);`,
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
false,
|
||||
undefined,
|
||||
[rootScript, parentScript]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const envVars = result.right.updatedEnvs.selected;
|
||||
expect(envVars.find((v) => v.key === "ROOT_VAR")?.currentValue).toBe(
|
||||
"root"
|
||||
);
|
||||
expect(envVars.find((v) => v.key === "PARENT_VAR")?.currentValue).toBe(
|
||||
"parent"
|
||||
);
|
||||
expect(envVars.find((v) => v.key === "REQUEST_VAR")?.currentValue).toBe(
|
||||
"request"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Empty inherited scripts are filtered out gracefully", async () => {
|
||||
const validScript = `pw.env.set("ENDPOINT", "https://example.com");`;
|
||||
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
endpoint: "<<ENDPOINT>>",
|
||||
preRequestScript: "",
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
false,
|
||||
undefined,
|
||||
["", " ", validScript, "\n"]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
expect(result.right.effectiveRequest.effectiveFinalURL).toBe(
|
||||
"https://example.com"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Works correctly with no inherited scripts (backward compatibility)", async () => {
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
endpoint: "<<ENDPOINT>>",
|
||||
preRequestScript: `pw.env.set("ENDPOINT", "https://example.com");`,
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
false,
|
||||
undefined,
|
||||
[]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
expect(result.right.effectiveRequest.effectiveFinalURL).toBe(
|
||||
"https://example.com"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Regression: in the legacy (isolated-vm) sandbox, inherited scripts must
|
||||
// execute sequentially in order. Pre-fix combineScriptsWithIIFE emitted an
|
||||
// outer detached Promise that script.run did not await; sequential ordering
|
||||
// was undefined.
|
||||
//
|
||||
// Note: the unit test exercises the sequential-ordering path. The
|
||||
// post-`await` drop only surfaces with macrotask awaits (setTimeout) or
|
||||
// cross-isolate Reference calls returning Promises — neither is exposed
|
||||
// through the synchronous `pw.*` surface in node/legacy.ts, so it is not
|
||||
// currently user-reachable in the CLI. The web worker path is where the
|
||||
// post-`await` drop is user-reachable; covered by the smoke fixture in
|
||||
// packages/hoppscotch-cli/src/__tests__/e2e/.
|
||||
test("Legacy sandbox executes inherited scripts in order", async () => {
|
||||
const rootScript = `pw.env.set("ORDER", "root");`;
|
||||
const parentScript = `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",parent");
|
||||
`;
|
||||
const request = makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
preRequestScript: `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",request");
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await preRequestScriptRunner(
|
||||
request,
|
||||
SAMPLE_ENVS,
|
||||
true,
|
||||
undefined,
|
||||
[rootScript, parentScript]
|
||||
)();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const orderVar = result.right.updatedEnvs.selected.find(
|
||||
(v) => v.key === "ORDER"
|
||||
);
|
||||
expect(orderVar?.currentValue).toBe("root,parent,request");
|
||||
}
|
||||
});
|
||||
});
|
||||
168
packages/hoppscotch-cli/src/__tests__/unit/scripting.spec.ts
Normal file
168
packages/hoppscotch-cli/src/__tests__/unit/scripting.spec.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
combineScriptsWithIIFE,
|
||||
stripModulePrefix,
|
||||
MODULE_PREFIX,
|
||||
} from "../../utils/scripting";
|
||||
|
||||
describe("scripting", () => {
|
||||
describe("stripModulePrefix", () => {
|
||||
test("strips 'export {};\\n' prefix", () => {
|
||||
expect(stripModulePrefix("export {};\nconst x = 1;")).toBe(
|
||||
"const x = 1;"
|
||||
);
|
||||
});
|
||||
|
||||
test("strips 'export {};' prefix without newline", () => {
|
||||
expect(stripModulePrefix("export {};const x = 1;")).toBe("const x = 1;");
|
||||
});
|
||||
|
||||
test("returns script unchanged if no prefix", () => {
|
||||
expect(stripModulePrefix("const x = 1;")).toBe("const x = 1;");
|
||||
});
|
||||
|
||||
test("returns empty string unchanged", () => {
|
||||
expect(stripModulePrefix("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("combineScriptsWithIIFE", () => {
|
||||
test("returns empty string for empty array", () => {
|
||||
expect(combineScriptsWithIIFE([])).toBe("");
|
||||
});
|
||||
|
||||
test("returns empty string when all scripts are empty", () => {
|
||||
expect(combineScriptsWithIIFE(["", " ", "\n"])).toBe("");
|
||||
});
|
||||
|
||||
test("wraps a single script in a sequential async IIFE", () => {
|
||||
const result = combineScriptsWithIIFE(["const x = 1;"]);
|
||||
|
||||
expect(result).toContain("async");
|
||||
expect(result).toContain("await");
|
||||
expect(result).toContain("const x = 1;");
|
||||
});
|
||||
|
||||
test("preserves script order (root → parent → child → request) for pre-request scripts", () => {
|
||||
const rootScript = 'pw.env.set("token", "root");';
|
||||
const parentScript = 'pw.env.set("parent", "true");';
|
||||
const requestScript = 'pw.env.set("request", "true");';
|
||||
|
||||
const result = combineScriptsWithIIFE([
|
||||
rootScript,
|
||||
parentScript,
|
||||
requestScript,
|
||||
]);
|
||||
|
||||
const rootIndex = result.indexOf(rootScript);
|
||||
const parentIndex = result.indexOf(parentScript);
|
||||
const requestIndex = result.indexOf(requestScript);
|
||||
|
||||
expect(rootIndex).toBeLessThan(parentIndex);
|
||||
expect(parentIndex).toBeLessThan(requestIndex);
|
||||
});
|
||||
|
||||
test("preserves script order (request → child → parent → root) for test scripts", () => {
|
||||
const requestScript = 'pw.test("request test", () => {});';
|
||||
const childScript = 'pw.test("child test", () => {});';
|
||||
const rootScript = 'pw.test("root test", () => {});';
|
||||
|
||||
// Simulates the reversal pattern used in test runner:
|
||||
// combineScriptsWithIIFE([requestScript, ...inheritedTestScripts.slice().reverse()])
|
||||
const inheritedTestScripts = [rootScript, childScript];
|
||||
const result = combineScriptsWithIIFE([
|
||||
requestScript,
|
||||
...inheritedTestScripts.slice().reverse(),
|
||||
]);
|
||||
|
||||
const requestIndex = result.indexOf(requestScript);
|
||||
const childIndex = result.indexOf(childScript);
|
||||
const rootIndex = result.indexOf(rootScript);
|
||||
|
||||
expect(requestIndex).toBeLessThan(childIndex);
|
||||
expect(childIndex).toBeLessThan(rootIndex);
|
||||
});
|
||||
|
||||
test("filters out empty scripts while preserving non-empty ones", () => {
|
||||
const script1 = "const a = 1;";
|
||||
const script2 = "const b = 2;";
|
||||
|
||||
const result = combineScriptsWithIIFE([script1, "", " ", script2]);
|
||||
|
||||
expect(result).toContain(script1);
|
||||
expect(result).toContain(script2);
|
||||
|
||||
// Should only have 2 await statements (not 4)
|
||||
const awaitCount = (result.match(/await/g) || []).length;
|
||||
expect(awaitCount).toBe(2);
|
||||
});
|
||||
|
||||
test("isolates variable scope between scripts (each wrapped in its own function)", () => {
|
||||
const script1 = "const x = 1;";
|
||||
const script2 = "const x = 2;";
|
||||
|
||||
const result = combineScriptsWithIIFE([script1, script2]);
|
||||
|
||||
// Both scripts should appear in separate async functions
|
||||
const fnCount = (result.match(/async function\(\)/g) || []).length;
|
||||
expect(fnCount).toBe(2);
|
||||
});
|
||||
|
||||
test("strips module prefix from scripts before wrapping", () => {
|
||||
const script = `${MODULE_PREFIX}const x = 1;`;
|
||||
|
||||
const result = combineScriptsWithIIFE([script]);
|
||||
|
||||
// The module prefix should be stripped
|
||||
expect(result).not.toContain("export {};");
|
||||
expect(result).toContain("const x = 1;");
|
||||
});
|
||||
|
||||
test("experimental target generates sequential await chain wrapped in try/catch", () => {
|
||||
const result = combineScriptsWithIIFE(
|
||||
["const a = 1;", "const b = 2;", "const c = 3;"],
|
||||
"experimental"
|
||||
);
|
||||
|
||||
// Outer wrapper captures the reporter lexically so user code that
|
||||
// deletes the globalThis property cannot suppress error reporting.
|
||||
expect(result).toMatch(
|
||||
/^const __hoppReporter = globalThis\.__hoppReportScriptExecutionError;\s*try \{/
|
||||
);
|
||||
expect(result).toContain("await (async function() {");
|
||||
// Each script contributes one `await` in the body.
|
||||
const awaitCount = (result.match(/\bawait\b/g) || []).length;
|
||||
expect(awaitCount).toBe(3);
|
||||
// Catch hands the error to the lexically captured reporter.
|
||||
expect(result).toContain(
|
||||
"} catch (__hoppScriptExecutionError) {"
|
||||
);
|
||||
expect(result).toContain("__hoppReporter(__hoppScriptExecutionError);");
|
||||
});
|
||||
|
||||
test("legacy target generates sync IIFE chain with no await", () => {
|
||||
const result = combineScriptsWithIIFE(
|
||||
["const a = 1;", "const b = 2;", "const c = 3;"],
|
||||
"legacy"
|
||||
);
|
||||
|
||||
// No `async` keyword, no `await` — legacy sandbox is sync-only.
|
||||
expect(result).not.toContain("async");
|
||||
expect(result).not.toContain("await");
|
||||
// Leading `;` guards against ASI on the host script.
|
||||
expect(result).toMatch(/^;\(function\(\) \{/);
|
||||
// Each script wrapped in its own IIFE
|
||||
const iifeCount = (result.match(/\.call\(this\);/g) || []).length;
|
||||
expect(iifeCount).toBe(3);
|
||||
});
|
||||
|
||||
test("default target is experimental (wrapped in try/catch)", () => {
|
||||
const result = combineScriptsWithIIFE(["const x = 1;"]);
|
||||
expect(result).toMatch(
|
||||
/^const __hoppReporter = globalThis\.__hoppReportScriptExecutionError;\s*try \{/
|
||||
);
|
||||
expect(result).toContain("await (async function() {");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { makeRESTRequest } from "@hoppscotch/data";
|
||||
import * as E from "fp-ts/Either";
|
||||
|
||||
import { testRunner } from "../../utils/test";
|
||||
import { HoppEnvs } from "../../types/request";
|
||||
|
||||
const SAMPLE_ENVS: HoppEnvs = {
|
||||
global: [],
|
||||
selected: [],
|
||||
};
|
||||
|
||||
const SAMPLE_RESPONSE = {
|
||||
status: 200,
|
||||
headers: [],
|
||||
body: {},
|
||||
statusText: "OK",
|
||||
responseTime: 100,
|
||||
};
|
||||
|
||||
const SAMPLE_REQUEST = makeRESTRequest({
|
||||
name: "request",
|
||||
method: "GET",
|
||||
endpoint: "https://example.com",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
auth: { authActive: false, authType: "none" },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
requestVariables: [],
|
||||
description: null,
|
||||
responses: {},
|
||||
});
|
||||
|
||||
describe("testRunner - inheritance", () => {
|
||||
test("Inherited test scripts are executed and register test cases", async () => {
|
||||
const rootTestScript = `
|
||||
pw.test("Root collection test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`;
|
||||
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `
|
||||
pw.test("Request test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: false,
|
||||
inheritedTestScripts: [rootTestScript],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const { testsReport } = result.right;
|
||||
const descriptors = testsReport.map((r) => r.descriptor);
|
||||
|
||||
expect(descriptors).toContain("Request test");
|
||||
expect(descriptors).toContain("Root collection test");
|
||||
}
|
||||
});
|
||||
|
||||
test("Inherited test scripts execute in request → child → parent → root order", async () => {
|
||||
const rootTestScript = `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",root");
|
||||
`;
|
||||
const parentTestScript = `
|
||||
const prev = pw.env.get("ORDER");
|
||||
pw.env.set("ORDER", prev + ",parent");
|
||||
`;
|
||||
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `pw.env.set("ORDER", "request");`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: false,
|
||||
// Stored as root → parent, reversed to parent → root during execution
|
||||
inheritedTestScripts: [rootTestScript, parentTestScript],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const orderVar = result.right.envs.selected.find(
|
||||
(v) => v.key === "ORDER"
|
||||
);
|
||||
expect(orderVar?.currentValue).toBe("request,parent,root");
|
||||
}
|
||||
});
|
||||
|
||||
test("Scripts with same local variable names do not collide (IIFE isolation)", async () => {
|
||||
const rootTestScript = `const x = "root"; pw.env.set("ROOT_VAR", x);`;
|
||||
const parentTestScript = `const x = "parent"; pw.env.set("PARENT_VAR", x);`;
|
||||
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `const x = "request"; pw.env.set("REQUEST_VAR", x);`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: false,
|
||||
inheritedTestScripts: [rootTestScript, parentTestScript],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const envVars = result.right.envs.selected;
|
||||
expect(envVars.find((v) => v.key === "ROOT_VAR")?.currentValue).toBe(
|
||||
"root"
|
||||
);
|
||||
expect(envVars.find((v) => v.key === "PARENT_VAR")?.currentValue).toBe(
|
||||
"parent"
|
||||
);
|
||||
expect(envVars.find((v) => v.key === "REQUEST_VAR")?.currentValue).toBe(
|
||||
"request"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Empty inherited test scripts are filtered out gracefully", async () => {
|
||||
const validScript = `
|
||||
pw.test("Valid inherited test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`;
|
||||
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `
|
||||
pw.test("Request test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: false,
|
||||
inheritedTestScripts: ["", " ", validScript, "\n"],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const { testsReport } = result.right;
|
||||
const descriptors = testsReport.map((r) => r.descriptor);
|
||||
|
||||
expect(descriptors).toContain("Request test");
|
||||
expect(descriptors).toContain("Valid inherited test");
|
||||
}
|
||||
});
|
||||
|
||||
test("Works correctly with no inherited test scripts (backward compatibility)", async () => {
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `
|
||||
pw.test("Solo request test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: false,
|
||||
inheritedTestScripts: [],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const { testsReport } = result.right;
|
||||
expect(testsReport.map((r) => r.descriptor)).toContain(
|
||||
"Solo request test"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Regression: in the legacy (isolated-vm) sandbox, all `pw.test` blocks
|
||||
// declared in inherited test scripts must register before the runner
|
||||
// captures results. Pre-fix combineScriptsWithIIFE emitted an outer
|
||||
// detached Promise so the registration order was undefined when multiple
|
||||
// inherited scripts were present.
|
||||
//
|
||||
// Note: the post-`await` drop is not currently user-reachable here either
|
||||
// (see pre-request-inheritance.spec.ts for context). User-facing coverage
|
||||
// for the web worker async path is in packages/hoppscotch-cli/src/__tests__/e2e/.
|
||||
test("Legacy sandbox registers inherited test scripts", async () => {
|
||||
const rootTestScript = `
|
||||
pw.test("Root collection test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`;
|
||||
|
||||
const result = await testRunner({
|
||||
request: makeRESTRequest({
|
||||
...SAMPLE_REQUEST,
|
||||
testScript: `
|
||||
pw.test("Request test", () => {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});
|
||||
`,
|
||||
}),
|
||||
envs: SAMPLE_ENVS,
|
||||
response: SAMPLE_RESPONSE,
|
||||
legacySandbox: true,
|
||||
inheritedTestScripts: [rootTestScript],
|
||||
})();
|
||||
|
||||
expect(result).toBeRight();
|
||||
|
||||
if (E.isRight(result)) {
|
||||
const descriptors = result.right.testsReport.map((r) => r.descriptor);
|
||||
expect(descriptors).toContain("Request test");
|
||||
expect(descriptors).toContain("Root collection test");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -39,12 +39,14 @@ export interface RequestRunnerResponse extends TestResponse {
|
|||
* @property {TestResponse} response Response structure for test script runner.
|
||||
* @property {HoppEnvs} envs Environment variables for test script runner.
|
||||
* @property {boolean} legacySandbox Whether to use the legacy sandbox.
|
||||
* @property {string[]} inheritedTestScripts Test scripts inherited from parent collections.
|
||||
*/
|
||||
export interface TestScriptParams {
|
||||
request: HoppRESTRequest;
|
||||
response: TestResponse;
|
||||
envs: HoppEnvs;
|
||||
legacySandbox: boolean;
|
||||
inheritedTestScripts?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -44,4 +44,6 @@ export type ProcessRequestParams = {
|
|||
delay: number;
|
||||
legacySandbox?: boolean;
|
||||
collectionVariables?: HoppCollectionVariable[];
|
||||
inheritedPreRequestScripts?: string[];
|
||||
inheritedTestScripts?: string[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
processRequest,
|
||||
} from "./request";
|
||||
import { getTestMetrics } from "./test";
|
||||
import { filterValidScripts } from "./scripting";
|
||||
|
||||
const { WARN, FAIL, INFO } = exceptionColors;
|
||||
|
||||
|
|
@ -109,8 +110,21 @@ const processCollection = async (
|
|||
envs: HoppEnvs,
|
||||
delay: number,
|
||||
requestsReport: RequestReport[],
|
||||
legacySandbox?: boolean
|
||||
legacySandbox?: boolean,
|
||||
ancestorPreRequestScripts: string[] = [],
|
||||
ancestorTestScripts: string[] = []
|
||||
) => {
|
||||
// Accumulate scripts from root -> current collection for inheritance
|
||||
// filterValidScripts strips empty, whitespace-only, and module-prefix-only scripts
|
||||
const inheritedPreRequestScripts = filterValidScripts([
|
||||
...ancestorPreRequestScripts,
|
||||
collection.preRequestScript,
|
||||
]);
|
||||
const inheritedTestScripts = filterValidScripts([
|
||||
...ancestorTestScripts,
|
||||
collection.testScript,
|
||||
]);
|
||||
|
||||
// Process each request in the collection
|
||||
for (const request of collection.requests) {
|
||||
const _request = preProcessRequest(request as HoppRESTRequest, collection);
|
||||
|
|
@ -127,6 +141,8 @@ const processCollection = async (
|
|||
delay,
|
||||
legacySandbox,
|
||||
collectionVariables,
|
||||
inheritedPreRequestScripts,
|
||||
inheritedTestScripts,
|
||||
};
|
||||
|
||||
// Request processing initiated message.
|
||||
|
|
@ -188,7 +204,9 @@ const processCollection = async (
|
|||
envs,
|
||||
delay,
|
||||
requestsReport,
|
||||
legacySandbox
|
||||
legacySandbox,
|
||||
inheritedPreRequestScripts,
|
||||
inheritedTestScripts
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { FormDataEntry } from "../types/request";
|
|||
import { isHoppErrnoException } from "./checks";
|
||||
import { getResourceContents } from "./getters";
|
||||
|
||||
// Re-export from the canonical implementation in scripting.ts
|
||||
export { stripModulePrefix } from "./scripting";
|
||||
|
||||
const getValidRequests = (
|
||||
collections: HoppCollection[],
|
||||
collectionFilePath: string
|
||||
|
|
@ -158,19 +161,3 @@ export async function parseCollectionData(
|
|||
|
||||
return getValidRequests(collectionSchemaParsedResult.data, pathOrId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Module prefix added by Monaco editor for TypeScript module mode.
|
||||
*/
|
||||
const MODULE_PREFIX = "export {};\n" as const;
|
||||
|
||||
/**
|
||||
* Strips `export {};\n` prefix from scripts before sandbox execution.
|
||||
* The prefix is added by the web app's Monaco editor for IntelliSense
|
||||
* and must be removed before execution.
|
||||
*/
|
||||
export const stripModulePrefix = (script: string): string => {
|
||||
return script.startsWith(MODULE_PREFIX)
|
||||
? script.slice(MODULE_PREFIX.length)
|
||||
: script;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,13 +35,17 @@ import { isHoppCLIError } from "./checks";
|
|||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||
import { getEffectiveFinalMetaData, getResolvedVariables } from "./getters";
|
||||
import { stripComments } from "./jsonc";
|
||||
import { stripModulePrefix, toFormData } from "./mutators";
|
||||
import { toFormData } from "./mutators";
|
||||
import { combineScriptsWithIIFE, filterValidScripts } from "./scripting";
|
||||
|
||||
/**
|
||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||
* applies them on current request to generate updated request.
|
||||
* @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest.
|
||||
* @param envs Environment variables related to request.
|
||||
* @param legacySandbox Whether to use the legacy sandbox.
|
||||
* @param collectionVariables Collection variables to use.
|
||||
* @param inheritedPreRequestScripts Pre-request scripts inherited from parent collections.
|
||||
* @returns EffectiveHoppRESTRequest that includes parsed ENV variables with in
|
||||
* request OR HoppCLIError with error code and related information.
|
||||
*/
|
||||
|
|
@ -49,7 +53,8 @@ export const preRequestScriptRunner = (
|
|||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs,
|
||||
legacySandbox: boolean,
|
||||
collectionVariables?: HoppCollectionVariable[]
|
||||
collectionVariables?: HoppCollectionVariable[],
|
||||
inheritedPreRequestScripts: string[] = []
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
|
|
@ -57,10 +62,19 @@ export const preRequestScriptRunner = (
|
|||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
const hoppFetchHook = createHoppFetchHook();
|
||||
|
||||
// Pre-request order: root → request.
|
||||
const combinedScript = combineScriptsWithIIFE(
|
||||
filterValidScripts([
|
||||
...inheritedPreRequestScripts,
|
||||
request.preRequestScript,
|
||||
]),
|
||||
experimentalScriptingSandbox ? "experimental" : "legacy"
|
||||
);
|
||||
|
||||
return pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
runPreRequestScript(stripModulePrefix(preRequestScript), {
|
||||
TE.chain(() =>
|
||||
runPreRequestScript(combinedScript, {
|
||||
envs,
|
||||
experimentalScriptingSandbox,
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -232,8 +232,16 @@ export const processRequest =
|
|||
params: ProcessRequestParams
|
||||
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
|
||||
async () => {
|
||||
const { envs, path, request, delay, legacySandbox, collectionVariables } =
|
||||
params;
|
||||
const {
|
||||
envs,
|
||||
path,
|
||||
request,
|
||||
delay,
|
||||
legacySandbox,
|
||||
collectionVariables,
|
||||
inheritedPreRequestScripts = [],
|
||||
inheritedTestScripts = [],
|
||||
} = params;
|
||||
|
||||
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
|
||||
const result = {
|
||||
|
|
@ -258,17 +266,21 @@ export const processRequest =
|
|||
effectiveFinalParams: [],
|
||||
effectiveFinalURL: "",
|
||||
};
|
||||
let updatedEnvs = <HoppEnvs>{};
|
||||
|
||||
// Fetch values for secret environment variables from system environment
|
||||
const processedEnvs = processEnvs(envs);
|
||||
|
||||
// Executing pre-request-script
|
||||
// Default envs to the pre-script state so downstream consumers
|
||||
// (test-runner, effectiveRequest builder) receive a well-shaped
|
||||
// HoppEnvs even if the pre-request script fails.
|
||||
let updatedEnvs: HoppEnvs = processedEnvs;
|
||||
|
||||
const preRequestRes = await preRequestScriptRunner(
|
||||
request,
|
||||
processedEnvs,
|
||||
legacySandbox ?? false,
|
||||
collectionVariables
|
||||
collectionVariables,
|
||||
inheritedPreRequestScripts
|
||||
)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
|
@ -317,12 +329,12 @@ export const processRequest =
|
|||
printRequestRunner.success(_requestRunnerRes);
|
||||
}
|
||||
|
||||
// Extracting test-script-runner parameters.
|
||||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
effectiveRequest,
|
||||
updatedEnvs,
|
||||
legacySandbox ?? false
|
||||
legacySandbox ?? false,
|
||||
inheritedTestScripts
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
|
|
|
|||
71
packages/hoppscotch-cli/src/utils/scripting.ts
Normal file
71
packages/hoppscotch-cli/src/utils/scripting.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Module prefix added by Monaco editor for TypeScript module mode.
|
||||
* Enables IntelliSense and isolates variables across editor instances.
|
||||
*/
|
||||
export const MODULE_PREFIX = "export {};\n" as const;
|
||||
|
||||
/**
|
||||
* Strips `export {};` prefix (with or without newline) from scripts before execution
|
||||
* (non-module context) or when exporting collections.
|
||||
*/
|
||||
export const stripModulePrefix = (script: string): string => {
|
||||
if (script.startsWith(MODULE_PREFIX)) {
|
||||
return script.slice(MODULE_PREFIX.length);
|
||||
}
|
||||
if (script.startsWith("export {};")) {
|
||||
return script.slice("export {};".length);
|
||||
}
|
||||
return script;
|
||||
};
|
||||
|
||||
export type CombineScriptsTarget = "experimental" | "legacy";
|
||||
|
||||
const wrapScript = (script: string, target: CombineScriptsTarget): string => {
|
||||
const stripped = stripModulePrefix(script.trim());
|
||||
if (!stripped) return "";
|
||||
const asyncKeyword = target === "experimental" ? "async " : "";
|
||||
return `${asyncKeyword}function() {\n${stripped}\n}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines inherited scripts into a sequential chain. Each script runs in
|
||||
* its own function for scope isolation.
|
||||
*
|
||||
* - `experimental`: `await (async function(){...})();` lines, evaluated in
|
||||
* an async host context so each `await` settles before the next runs.
|
||||
* - `legacy`: sync `(function(){...}).call(this);` lines. Top-level `await`
|
||||
* is rejected at parse time.
|
||||
*/
|
||||
export const combineScriptsWithIIFE = (
|
||||
scripts: string[],
|
||||
target: CombineScriptsTarget = "experimental"
|
||||
): string => {
|
||||
const fns = scripts.map((s) => wrapScript(s, target)).filter((s) => s);
|
||||
if (fns.length === 0) return "";
|
||||
if (target === "experimental") {
|
||||
// Wrap the awaited chain in try/catch so top-level throws / rejected
|
||||
// awaits reach the host reporter; faraday-cage otherwise swallows
|
||||
// async-boundary errors via its keepAlive loop.
|
||||
const body = fns.map((fn) => `await (${fn})();`).join("\n");
|
||||
return [
|
||||
"const __hoppReporter = globalThis.__hoppReportScriptExecutionError;",
|
||||
"try {",
|
||||
body,
|
||||
"} catch (__hoppScriptExecutionError) {",
|
||||
" __hoppReporter(__hoppScriptExecutionError);",
|
||||
"}",
|
||||
].join("\n");
|
||||
}
|
||||
// Leading `;` guards against ASI: a prior `})` on the host line would
|
||||
// otherwise be read as a call against our IIFE expression.
|
||||
return fns.map((fn) => `;(${fn}).call(this);`).join("\n");
|
||||
};
|
||||
|
||||
export const filterValidScripts = (
|
||||
scripts: (string | undefined | null)[]
|
||||
): string[] =>
|
||||
scripts.filter(
|
||||
(script): script is string =>
|
||||
typeof script === "string" &&
|
||||
stripModulePrefix(script).trim().length > 0
|
||||
);
|
||||
|
|
@ -18,7 +18,7 @@ import { HoppEnvs } from "../types/request";
|
|||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||
import { getDurationInSeconds } from "./getters";
|
||||
import { createHoppFetchHook } from "./hopp-fetch";
|
||||
import { stripModulePrefix } from "./mutators";
|
||||
import { combineScriptsWithIIFE, filterValidScripts } from "./scripting";
|
||||
|
||||
/**
|
||||
* Executes test script and runs testDescriptorParser to generate test-report using
|
||||
|
|
@ -39,28 +39,46 @@ export const testRunner = (
|
|||
TE.bind("test_response", () =>
|
||||
pipe(
|
||||
TE.of(testScriptData),
|
||||
TE.chain(({ request, response, envs, legacySandbox }) => {
|
||||
const { status, statusText, headers, responseTime, body } = response;
|
||||
|
||||
const effectiveResponse = {
|
||||
status,
|
||||
statusText,
|
||||
headers,
|
||||
responseTime,
|
||||
body,
|
||||
};
|
||||
|
||||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
const hoppFetchHook = createHoppFetchHook();
|
||||
|
||||
return runTestScript(stripModulePrefix(request.testScript), {
|
||||
envs,
|
||||
TE.chain(
|
||||
({
|
||||
request,
|
||||
response: effectiveResponse,
|
||||
experimentalScriptingSandbox,
|
||||
hoppFetchHook,
|
||||
});
|
||||
})
|
||||
response,
|
||||
envs,
|
||||
legacySandbox,
|
||||
inheritedTestScripts = [],
|
||||
}) => {
|
||||
const { status, statusText, headers, responseTime, body } =
|
||||
response;
|
||||
|
||||
const effectiveResponse = {
|
||||
status,
|
||||
statusText,
|
||||
headers,
|
||||
responseTime,
|
||||
body,
|
||||
};
|
||||
|
||||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
const hoppFetchHook = createHoppFetchHook();
|
||||
|
||||
// Test order: request → root (reverse of pre-request).
|
||||
const combinedScript = combineScriptsWithIIFE(
|
||||
filterValidScripts([
|
||||
request.testScript,
|
||||
...inheritedTestScripts.slice().reverse(),
|
||||
]),
|
||||
experimentalScriptingSandbox ? "experimental" : "legacy"
|
||||
);
|
||||
|
||||
return runTestScript(combinedScript, {
|
||||
envs,
|
||||
request,
|
||||
response: effectiveResponse,
|
||||
experimentalScriptingSandbox,
|
||||
hoppFetchHook,
|
||||
});
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -160,7 +178,8 @@ export const getTestScriptParams = (
|
|||
reqRunnerRes: RequestRunnerResponse,
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs,
|
||||
legacySandbox: boolean
|
||||
legacySandbox: boolean,
|
||||
inheritedTestScripts: string[] = []
|
||||
) => {
|
||||
const testScriptParams: TestScriptParams = {
|
||||
request,
|
||||
|
|
@ -173,6 +192,7 @@ export const getTestScriptParams = (
|
|||
},
|
||||
envs,
|
||||
legacySandbox,
|
||||
inheritedTestScripts,
|
||||
};
|
||||
return testScriptParams;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ export const transformWorkspaceCollections = (
|
|||
headers?: HoppRESTHeaders;
|
||||
variables: HoppCollectionVariable[];
|
||||
description: string | null;
|
||||
preRequestScript?: string;
|
||||
testScript?: string;
|
||||
} = data ? JSON.parse(data) : {};
|
||||
|
||||
const {
|
||||
|
|
@ -186,6 +188,8 @@ export const transformWorkspaceCollections = (
|
|||
headers = [],
|
||||
variables = [],
|
||||
description = null,
|
||||
preRequestScript = "",
|
||||
testScript = "",
|
||||
} = parsedData;
|
||||
|
||||
const transformedAuth = transformAuth(auth);
|
||||
|
|
@ -211,6 +215,8 @@ export const transformWorkspaceCollections = (
|
|||
headers: transformedHeaders,
|
||||
variables: filteredCollectionVariables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -841,6 +841,7 @@
|
|||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
|
||||
"collection_properties_header": "This header will be set for every request in this collection.",
|
||||
"collection_properties_scripts": "These scripts will run for every request in this collection. Pre-request scripts run before the request, test scripts run after the response.",
|
||||
"generate_documentation_first": "Generate documentation first",
|
||||
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
||||
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
||||
|
|
@ -1256,6 +1257,12 @@
|
|||
"saved": "Response saved",
|
||||
"invalid_name": "Please provide a name for the response"
|
||||
},
|
||||
"script": {
|
||||
"inheriting": "Inheriting scripts from",
|
||||
"inheriting_from_count": "Inheriting from {count} collection | Inheriting from {count} collections",
|
||||
"inherited_scripts": "Inherited Scripts",
|
||||
"view_inherited": "View inherited scripts"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Accent color",
|
||||
"account": "Account",
|
||||
|
|
@ -1781,6 +1788,7 @@
|
|||
"parameters": "Parameters",
|
||||
"post_request_script": "Post-request Script",
|
||||
"pre_request_script": "Pre-request Script",
|
||||
"scripts": "Scripts",
|
||||
"queries": "Queries",
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ declare module 'vue' {
|
|||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
|
||||
IconLucideFileQuestion: typeof import('~icons/lucide/file-question')['default']
|
||||
IconLucideFileSymlink: typeof import('~icons/lucide/file-symlink')['default']
|
||||
IconLucideFileText: typeof import('~icons/lucide/file-text')['default']
|
||||
IconLucideFileX: typeof import('~icons/lucide/file-x')['default']
|
||||
IconLucideFolder: typeof import('~icons/lucide/folder')['default']
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
modelValue: string
|
||||
type: "pre-request" | "post-request"
|
||||
readOnly?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
readOnly: false,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -40,12 +42,15 @@ const emit = defineEmits<{
|
|||
|
||||
const editorModel = ref<monaco.editor.ITextModel | null>(null)
|
||||
|
||||
const MONACO_EDITOR_OPTIONS: Readonly<monaco.editor.IStandaloneEditorConstructionOptions> =
|
||||
{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true,
|
||||
}
|
||||
const MONACO_EDITOR_OPTIONS = computed<
|
||||
Readonly<monaco.editor.IStandaloneEditorConstructionOptions>
|
||||
>(() => ({
|
||||
automaticLayout: true,
|
||||
formatOnType: !props.readOnly,
|
||||
formatOnPaste: !props.readOnly,
|
||||
readOnly: props.readOnly,
|
||||
domReadOnly: props.readOnly,
|
||||
}))
|
||||
|
||||
// Static imports: import X from "URL"
|
||||
const staticImportRegex =
|
||||
|
|
@ -237,3 +242,27 @@ const monacoEditorTheme = computed(() =>
|
|||
["dark", "black"].includes(theme.value) ? "vs-dark" : "vs"
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* Override Monaco editor colors with Hoppscotch CSS variables
|
||||
to keep visual consistency with the CodeMirror editors.
|
||||
:deep() penetrates into Monaco's rendered DOM within this component. */
|
||||
:deep(.monaco-editor),
|
||||
:deep(.monaco-editor .overflow-guard),
|
||||
:deep(.monaco-editor-background),
|
||||
:deep(.monaco-editor .margin) {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
:deep(.monaco-editor .line-numbers) {
|
||||
color: var(--secondary-light-color) !important;
|
||||
}
|
||||
|
||||
:deep(.monaco-editor .cursor) {
|
||||
border-color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
:deep(.monaco-editor .selected-text) {
|
||||
background-color: var(--accent-dark-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,83 @@
|
|||
/>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
v-if="source === 'REST'"
|
||||
id="scripts"
|
||||
:label="`${t('tab.scripts')}`"
|
||||
>
|
||||
<div class="flex flex-col flex-1">
|
||||
<HoppSmartTabs
|
||||
v-model="activeScriptsTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
id="pre-request"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
hasActualScript(editableCollection.preRequestScript)
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="h-64 overflow-hidden relative">
|
||||
<MonacoScriptEditor
|
||||
v-if="
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX &&
|
||||
activeTab === 'scripts' &&
|
||||
activeScriptsTab === 'pre-request'
|
||||
"
|
||||
v-model="editableCollection.preRequestScript"
|
||||
type="pre-request"
|
||||
:read-only="!hasTeamWriteAccess"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
ref="preRequestEditor"
|
||||
class="h-full absolute inset-0"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
id="test-script"
|
||||
:label="`${t('tab.post_request_script')}`"
|
||||
:indicator="hasActualScript(editableCollection.testScript)"
|
||||
>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="h-64 border-b border-dividerLight overflow-hidden relative"
|
||||
>
|
||||
<MonacoScriptEditor
|
||||
v-if="
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX &&
|
||||
activeTab === 'scripts' &&
|
||||
activeScriptsTab === 'test-script'
|
||||
"
|
||||
v-model="editableCollection.testScript"
|
||||
type="post-request"
|
||||
:read-only="!hasTeamWriteAccess"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
ref="testScriptEditor"
|
||||
class="h-full absolute inset-0"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
|
||||
<div
|
||||
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
|
||||
>
|
||||
<icon-lucide-info class="svg-icons mr-2" />
|
||||
{{ t("helpers.collection_properties_scripts") }}
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
v-if="showDetails"
|
||||
:id="'details'"
|
||||
|
|
@ -138,11 +215,17 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import { clone } from "lodash-es"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "~/composables/settings"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import preRequestCompleter from "~/helpers/editor/completion/preRequest"
|
||||
import testScriptCompleter from "~/helpers/editor/completion/testScript"
|
||||
import preRequestLinter from "~/helpers/editor/linting/preRequest"
|
||||
import testScriptLinter from "~/helpers/editor/linting/testScript"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
|
|
@ -154,6 +237,7 @@ import {
|
|||
HoppRESTHeaders,
|
||||
GQLHeader,
|
||||
} from "@hoppscotch/data"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { PersistenceService } from "~/services/persistence"
|
||||
|
||||
|
|
@ -206,10 +290,14 @@ const editableCollection = ref<{
|
|||
headers: HoppCollectionHeaders
|
||||
auth: HoppCollectionAuth
|
||||
variables: HoppCollectionVariable[]
|
||||
preRequestScript: string
|
||||
testScript: string
|
||||
}>({
|
||||
headers: [],
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
variables: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
|
|
@ -217,9 +305,65 @@ const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||
1000
|
||||
)
|
||||
const activeTab = useVModel(props, "modelValue", emit)
|
||||
const activeScriptsTab = ref<"pre-request" | "test-script">("pre-request")
|
||||
|
||||
const activeTabIsDetails = computed(() => activeTab.value === "details")
|
||||
|
||||
const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting(
|
||||
"EXPERIMENTAL_SCRIPTING_SANDBOX"
|
||||
)
|
||||
|
||||
const preRequestEditor = ref<any | null>(null)
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
|
||||
const preRequestScriptModel = computed({
|
||||
get: () => editableCollection.value.preRequestScript,
|
||||
set: (val: string) => {
|
||||
editableCollection.value.preRequestScript = val
|
||||
},
|
||||
})
|
||||
|
||||
const testScriptModel = computed({
|
||||
get: () => editableCollection.value.testScript,
|
||||
set: (val: string) => {
|
||||
editableCollection.value.testScript = val
|
||||
},
|
||||
})
|
||||
|
||||
useCodemirror(
|
||||
preRequestEditor,
|
||||
preRequestScriptModel,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: true,
|
||||
placeholder: `${t("preRequest.javascript_code")}`,
|
||||
readOnly: !props.hasTeamWriteAccess,
|
||||
},
|
||||
linter: preRequestLinter,
|
||||
completer: preRequestCompleter,
|
||||
environmentHighlights: false,
|
||||
contextMenuEnabled: false,
|
||||
})
|
||||
)
|
||||
|
||||
useCodemirror(
|
||||
testScriptEditor,
|
||||
testScriptModel,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: true,
|
||||
placeholder: `${t("test.javascript_code")}`,
|
||||
readOnly: !props.hasTeamWriteAccess,
|
||||
},
|
||||
linter: testScriptLinter,
|
||||
completer: testScriptCompleter,
|
||||
environmentHighlights: false,
|
||||
contextMenuEnabled: false,
|
||||
})
|
||||
)
|
||||
|
||||
const persistUnsavedChanges = async (
|
||||
updated: typeof editableCollection.value
|
||||
) => {
|
||||
|
|
@ -258,23 +402,34 @@ const enforceTabAccessRules = () => {
|
|||
["headers", "authorization"].includes(activeTab.value)
|
||||
)
|
||||
activeTab.value = "variables"
|
||||
// `Scripts` tab only exists for REST collections
|
||||
// Switch to `Variables` tab if scripts tab becomes unavailable
|
||||
if (activeTab.value === "scripts" && props.source !== "REST")
|
||||
activeTab.value = "variables"
|
||||
}
|
||||
|
||||
const loadEditableCollection = () => {
|
||||
activeScriptsTab.value = "pre-request"
|
||||
editableCollection.value = {
|
||||
auth: clone(props.editingProperties.collection!.auth as HoppCollectionAuth),
|
||||
headers: clone(
|
||||
props.editingProperties.collection!.headers as HoppCollectionHeaders
|
||||
),
|
||||
variables: clone(props.editingProperties.collection!.variables || []),
|
||||
preRequestScript:
|
||||
props.editingProperties.collection!.preRequestScript || "",
|
||||
testScript: props.editingProperties.collection!.testScript || "",
|
||||
}
|
||||
}
|
||||
|
||||
const resetEditableCollection = () => {
|
||||
activeScriptsTab.value = "pre-request"
|
||||
editableCollection.value = {
|
||||
headers: [],
|
||||
auth: { authType: "inherit", authActive: false },
|
||||
variables: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -737,6 +737,8 @@ const saveCollectionDocumentation = async () => {
|
|||
headers: collection.headers || [],
|
||||
variables: collection.variables || [],
|
||||
description: documentationDescription.value,
|
||||
preRequestScript: collection.preRequestScript || "",
|
||||
testScript: collection.testScript || "",
|
||||
}
|
||||
|
||||
pipe(
|
||||
|
|
@ -831,6 +833,8 @@ const saveCollectionDocumentationById = async (
|
|||
headers: collectionData.headers || [],
|
||||
variables: collectionData.variables || [],
|
||||
description: documentation,
|
||||
preRequestScript: collectionData.preRequestScript || "",
|
||||
testScript: collectionData.testScript || "",
|
||||
}
|
||||
|
||||
const result = await pipe(
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ const addNewCollection = () => {
|
|||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
description: "",
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -906,6 +906,8 @@ const addNewRootCollection = async (name: string) => {
|
|||
},
|
||||
variables: [],
|
||||
description: "",
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -3254,6 +3256,7 @@ const editProperties = async (payload: {
|
|||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
scripts: [],
|
||||
}
|
||||
|
||||
if (parentIndex) {
|
||||
|
|
@ -3307,16 +3310,19 @@ const editProperties = async (payload: {
|
|||
description: null as string | null,
|
||||
folders: null,
|
||||
requests: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
if (parentIndex) {
|
||||
const { auth, headers, variables } =
|
||||
const { auth, headers, variables, scripts } =
|
||||
teamCollectionService.cascadeParentCollectionForProperties(parentIndex)
|
||||
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
scripts,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3337,6 +3343,8 @@ const editProperties = async (payload: {
|
|||
headers: data.headers,
|
||||
variables: collectionVariables,
|
||||
description: data.description,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
}
|
||||
|
||||
coll = {
|
||||
|
|
@ -3448,6 +3456,8 @@ const setCollectionProperties = (newCollection: {
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
// Mark as loading BEFORE triggering async update to avoid race conditions and push the collectionId to the loading array
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('script.inherited_scripts')"
|
||||
styles="sm:max-w-3xl"
|
||||
full-width-body
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex h-[24rem]">
|
||||
<div
|
||||
class="flex w-52 flex-shrink-0 flex-col border-r border-dividerLight overflow-auto bg-primary"
|
||||
>
|
||||
<button
|
||||
v-for="script in scripts"
|
||||
:key="script.parentID"
|
||||
class="group flex items-center gap-2 px-3 py-2.5 text-left text-tiny transition"
|
||||
:class="
|
||||
isSelected(script.parentID)
|
||||
? 'bg-primaryLight text-secondaryDark'
|
||||
: 'text-secondaryLight hover:bg-primaryLight/50 hover:text-secondary'
|
||||
"
|
||||
@click="selectedScriptID = script.parentID"
|
||||
>
|
||||
<icon-lucide-folder
|
||||
class="svg-icons !w-3.5 !h-3.5 flex-shrink-0"
|
||||
:class="
|
||||
isSelected(script.parentID)
|
||||
? 'text-secondaryDark'
|
||||
: 'text-secondaryLight opacity-50 group-hover:opacity-75'
|
||||
"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="flex-1 truncate font-bold">
|
||||
{{ script.parentName }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative flex flex-1 flex-col overflow-hidden">
|
||||
<HoppButtonSecondary
|
||||
v-if="selectedScript"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
class="!absolute right-2 top-2 z-10"
|
||||
@click="copyScriptContent(displayedScript)"
|
||||
/>
|
||||
<div
|
||||
ref="scriptEditor"
|
||||
class="flex-1 overflow-auto"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { stripModulePrefix } from "~/helpers/scripting"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
|
||||
type InheritedScript = {
|
||||
parentID: string
|
||||
parentName: string
|
||||
preRequestScript: string
|
||||
testScript: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
scripts: InheritedScript[]
|
||||
scriptType: "preRequestScript" | "testScript"
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const selectedScriptID = ref<string | null>(null)
|
||||
|
||||
// Reset selection when modal reopens to avoid stale state
|
||||
watch(
|
||||
() => props.show,
|
||||
(isVisible) => {
|
||||
if (isVisible) {
|
||||
selectedScriptID.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const isSelected = (id: string) =>
|
||||
selectedScriptID.value === id ||
|
||||
(selectedScriptID.value === null && props.scripts[0]?.parentID === id)
|
||||
|
||||
const selectedScript = computed(
|
||||
() =>
|
||||
props.scripts.find((s) => s.parentID === selectedScriptID.value) ??
|
||||
props.scripts[0] ??
|
||||
null
|
||||
)
|
||||
|
||||
const displayedScript = computed(() =>
|
||||
selectedScript.value
|
||||
? stripModulePrefix(selectedScript.value[props.scriptType])
|
||||
: ""
|
||||
)
|
||||
|
||||
const scriptEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting(
|
||||
"WRAP_LINES",
|
||||
props.scriptType === "preRequestScript" ? "httpPreRequest" : "httpTest"
|
||||
)
|
||||
|
||||
useCodemirror(
|
||||
scriptEditor,
|
||||
displayedScript,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const copyScriptContent = (script: string) => {
|
||||
copyToClipboard(script)
|
||||
copyIcon.value = IconCheck
|
||||
}
|
||||
</script>
|
||||
|
|
@ -3,9 +3,26 @@
|
|||
<div
|
||||
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
|
||||
>
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
{{ t("preRequest.javascript_code") }}
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
{{ t("preRequest.javascript_code") }}
|
||||
</label>
|
||||
<HoppButtonSecondary
|
||||
v-if="inheritedScripts.length > 0"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('script.view_inherited')"
|
||||
:label="
|
||||
t('script.inheriting_from_count', {
|
||||
count: inheritedScripts.length,
|
||||
})
|
||||
"
|
||||
:icon="IconFileSymlink"
|
||||
class="!px-1 !py-0.5 text-yellow-500 hover:text-yellow-500"
|
||||
filled
|
||||
outline
|
||||
@click="showInheritedModal = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
|
|
@ -76,6 +93,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HttpInheritedScriptsModal
|
||||
:show="showInheritedModal"
|
||||
:scripts="inheritedScripts"
|
||||
script-type="preRequestScript"
|
||||
@close="showInheritedModal = false"
|
||||
/>
|
||||
<AiexperimentsModifyPreRequestModal
|
||||
v-if="isModifyPreRequestModalOpen && currentRequest"
|
||||
:current-script="preRequestScript"
|
||||
|
|
@ -101,9 +124,12 @@ import { useReadonlyStream } from "~/composables/stream"
|
|||
import { invokeAction } from "~/helpers/actions"
|
||||
import completer from "~/helpers/editor/completion/preRequest"
|
||||
import linter from "~/helpers/editor/linting/preRequest"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconFileSymlink from "~icons/lucide/file-symlink"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconSparkles from "~icons/lucide/sparkles"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
|
|
@ -114,6 +140,7 @@ const t = useI18n()
|
|||
const props = defineProps<{
|
||||
modelValue: string
|
||||
isActive?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
|
|
@ -121,6 +148,16 @@ const emit = defineEmits<{
|
|||
|
||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
||||
|
||||
const showInheritedModal = ref(false)
|
||||
|
||||
const inheritedScripts = computed(() => {
|
||||
return (
|
||||
props.inheritedProperties?.scripts?.filter((script) =>
|
||||
hasActualScript(script.preRequestScript)
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const preRequestEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
||||
|
||||
|
|
|
|||
|
|
@ -54,17 +54,16 @@
|
|||
:id="'preRequestScript'"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
'preRequestScript' in request &&
|
||||
request.preRequestScript &&
|
||||
request.preRequestScript.length > 0
|
||||
? true
|
||||
: false
|
||||
('preRequestScript' in request &&
|
||||
hasActualScript(request.preRequestScript)) ||
|
||||
hasInheritedPreRequestScripts
|
||||
"
|
||||
>
|
||||
<HttpPreRequestScript
|
||||
v-if="'preRequestScript' in request"
|
||||
v-model="request.preRequestScript"
|
||||
:is-active="selectedOptionTab === 'preRequestScript'"
|
||||
:inherited-properties="inheritedProperties"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
|
|
@ -72,17 +71,15 @@
|
|||
:id="'tests'"
|
||||
:label="`${t('tab.post_request_script')}`"
|
||||
:indicator="
|
||||
'testScript' in request &&
|
||||
request.testScript &&
|
||||
request.testScript.length > 0
|
||||
? true
|
||||
: false
|
||||
('testScript' in request && hasActualScript(request.testScript)) ||
|
||||
hasInheritedTestScripts
|
||||
"
|
||||
>
|
||||
<HttpTests
|
||||
v-if="'testScript' in request"
|
||||
v-model="request.testScript"
|
||||
:is-active="selectedOptionTab === 'tests'"
|
||||
:inherited-properties="inheritedProperties"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
|
|
@ -107,6 +104,7 @@ import { useVModel } from "@vueuse/core"
|
|||
import { computed } from "vue"
|
||||
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { AggregateEnvironment } from "~/newstore/environments"
|
||||
|
||||
|
|
@ -188,6 +186,22 @@ const isBodyFilled = computed(() => {
|
|||
return Boolean(request.value.body.body && request.value.body.body.length > 0)
|
||||
})
|
||||
|
||||
const hasInheritedPreRequestScripts = computed(() => {
|
||||
return (
|
||||
props.inheritedProperties?.scripts?.some((script) =>
|
||||
hasActualScript(script.preRequestScript)
|
||||
) ?? false
|
||||
)
|
||||
})
|
||||
|
||||
const hasInheritedTestScripts = computed(() => {
|
||||
return (
|
||||
props.inheritedProperties?.scripts?.some((script) =>
|
||||
hasActualScript(script.testScript)
|
||||
) ?? false
|
||||
)
|
||||
})
|
||||
|
||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||
selectedOptionTab.value = tab as RESTOptionTabs
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,9 +3,26 @@
|
|||
<div
|
||||
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
|
||||
>
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
{{ t("test.javascript_code") }}
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
{{ t("test.javascript_code") }}
|
||||
</label>
|
||||
<HoppButtonSecondary
|
||||
v-if="inheritedScripts.length > 0"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('script.view_inherited')"
|
||||
:label="
|
||||
t('script.inheriting_from_count', {
|
||||
count: inheritedScripts.length,
|
||||
})
|
||||
"
|
||||
:icon="IconFileSymlink"
|
||||
class="!px-1 !py-0.5 text-yellow-500 hover:text-yellow-500"
|
||||
filled
|
||||
outline
|
||||
@click="showInheritedModal = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
|
|
@ -76,6 +93,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HttpInheritedScriptsModal
|
||||
:show="showInheritedModal"
|
||||
:scripts="inheritedScripts"
|
||||
script-type="testScript"
|
||||
@close="showInheritedModal = false"
|
||||
/>
|
||||
<AiexperimentsModifyTestScriptModal
|
||||
v-if="isModifyTestScriptModalOpen && currentRequest"
|
||||
:current-script="testScript"
|
||||
|
|
@ -99,10 +122,13 @@ import { useReadonlyStream } from "~/composables/stream"
|
|||
import { invokeAction } from "~/helpers/actions"
|
||||
import completer from "~/helpers/editor/completion/testScript"
|
||||
import linter from "~/helpers/editor/linting/testScript"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import testSnippets from "~/helpers/testSnippets"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconFileSymlink from "~icons/lucide/file-symlink"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconSparkles from "~icons/lucide/sparkles"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
|
|
@ -113,9 +139,22 @@ const t = useI18n()
|
|||
const props = defineProps<{
|
||||
modelValue: string
|
||||
isActive?: boolean
|
||||
inheritedProperties?: HoppInheritedProperty
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const testScript = useVModel(props, "modelValue", emit)
|
||||
|
||||
const showInheritedModal = ref(false)
|
||||
|
||||
const inheritedScripts = computed(() => {
|
||||
return (
|
||||
props.inheritedProperties?.scripts?.filter((script) =>
|
||||
hasActualScript(script.testScript)
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
||||
|
||||
|
|
|
|||
|
|
@ -249,6 +249,11 @@ const runTests = async () => {
|
|||
)
|
||||
|
||||
let resolvedCollection: HoppCollection = collection.value
|
||||
// Scripts declared on ancestors above the selected run-start node — must be
|
||||
// seeded into the runner so partial-scope runs still honor the documented
|
||||
// Root → Parent → Child → Request inheritance chain.
|
||||
let ancestorPreRequestScripts: string[] = []
|
||||
let ancestorTestScripts: string[] = []
|
||||
|
||||
if (!isPersonalWorkspace) {
|
||||
const requestAuth = tab.value.document.inheritedProperties?.auth
|
||||
|
|
@ -270,6 +275,19 @@ const runTests = async () => {
|
|||
tab.value.document.inheritedProperties?.variables ?? []
|
||||
)
|
||||
|
||||
// Team cascade includes the selected node itself in its scripts array;
|
||||
// drop it here because runTestCollection will cascade that node's scripts
|
||||
// as part of the normal tree walk, and we must not double-run them.
|
||||
const inheritedScripts = (
|
||||
tab.value.document.inheritedProperties?.scripts ?? []
|
||||
).filter((s) => s.parentID !== collectionID)
|
||||
ancestorPreRequestScripts = inheritedScripts
|
||||
.map((s) => s.preRequestScript)
|
||||
.filter((s) => s && s.trim().length > 0)
|
||||
ancestorTestScripts = inheritedScripts
|
||||
.map((s) => s.testScript)
|
||||
.filter((s) => s && s.trim().length > 0)
|
||||
|
||||
resolvedCollection = {
|
||||
...collection.value,
|
||||
auth: requestAuth,
|
||||
|
|
@ -277,12 +295,23 @@ const runTests = async () => {
|
|||
variables: parentVariables,
|
||||
}
|
||||
} else {
|
||||
const { auth, headers, variables } = collectionInheritedProps ?? {
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
ancestorPreRequestScripts: preAncestors,
|
||||
ancestorTestScripts: testAncestors,
|
||||
} = collectionInheritedProps ?? {
|
||||
auth: { authActive: true, authType: "none" },
|
||||
headers: [],
|
||||
variables: [],
|
||||
ancestorPreRequestScripts: [],
|
||||
ancestorTestScripts: [],
|
||||
}
|
||||
|
||||
ancestorPreRequestScripts = preAncestors
|
||||
ancestorTestScripts = testAncestors
|
||||
|
||||
resolvedCollection = {
|
||||
...collection.value,
|
||||
auth,
|
||||
|
|
@ -292,10 +321,16 @@ const runTests = async () => {
|
|||
}
|
||||
|
||||
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
|
||||
testRunnerService.runTests(tab, resolvedCollection, {
|
||||
...testRunnerConfig.value,
|
||||
stopRef: testRunnerStopRef,
|
||||
})
|
||||
testRunnerService.runTests(
|
||||
tab,
|
||||
resolvedCollection,
|
||||
{
|
||||
...testRunnerConfig.value,
|
||||
stopRef: testRunnerStopRef,
|
||||
},
|
||||
ancestorPreRequestScripts,
|
||||
ancestorTestScripts
|
||||
)
|
||||
}
|
||||
|
||||
const stopTests = () => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ import { map } from "fp-ts/Either"
|
|||
import { runPreRequestScript, runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import { useSetting } from "~/composables/settings"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { stripModulePrefix } from "~/helpers/scripting"
|
||||
import {
|
||||
combineScriptsWithIIFE,
|
||||
hasActualScript,
|
||||
} from "~/helpers/scripting"
|
||||
import { createHoppFetchHook } from "~/helpers/hopp-fetch"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import {
|
||||
|
|
@ -364,14 +367,21 @@ const delegatePreRequestScriptRunner = (
|
|||
selected: Environment["variables"]
|
||||
temp: Environment["variables"]
|
||||
},
|
||||
cookies: Cookie[] | null
|
||||
cookies: Cookie[] | null,
|
||||
inheritedPreRequestScripts: string[] = []
|
||||
): Promise<E.Either<string, SandboxPreRequestResult>> => {
|
||||
const { preRequestScript } = request
|
||||
const experimentalScriptingSandbox = EXPERIMENTAL_SCRIPTING_SANDBOX.value
|
||||
const target = experimentalScriptingSandbox ? "experimental" : "legacy"
|
||||
|
||||
const cleanScript = stripModulePrefix(preRequestScript)
|
||||
// Pre-request order: root → request.
|
||||
const combinedScript = combineScriptsWithIIFE(
|
||||
[...inheritedPreRequestScripts, preRequestScript],
|
||||
target
|
||||
)
|
||||
|
||||
// Short-circuit empty scripts to avoid unnecessary WASM initialization
|
||||
if (cleanScript.trim().length === 0) {
|
||||
if (combinedScript.length === 0) {
|
||||
return Promise.resolve(
|
||||
E.right({
|
||||
updatedEnvs: envs,
|
||||
|
|
@ -380,19 +390,16 @@ const delegatePreRequestScriptRunner = (
|
|||
)
|
||||
}
|
||||
|
||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
||||
// Strip `export {};\n` before executing in legacy sandbox to prevent syntax errors
|
||||
|
||||
return runPreRequestScript(cleanScript, {
|
||||
if (!experimentalScriptingSandbox) {
|
||||
return runPreRequestScript(combinedScript, {
|
||||
envs,
|
||||
experimentalScriptingSandbox: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Experimental sandbox enabled - use faraday-cage with hook
|
||||
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
||||
|
||||
return runPreRequestScript(cleanScript, {
|
||||
return runPreRequestScript(combinedScript, {
|
||||
envs,
|
||||
request,
|
||||
cookies,
|
||||
|
|
@ -405,14 +412,21 @@ const runPostRequestScript = (
|
|||
envs: TestResult["envs"],
|
||||
request: HoppRESTRequest,
|
||||
response: HoppRESTResponse,
|
||||
cookies: Cookie[] | null
|
||||
cookies: Cookie[] | null,
|
||||
inheritedTestScripts: string[] = []
|
||||
): Promise<E.Either<string, SandboxTestResult>> => {
|
||||
const { testScript } = request
|
||||
const experimentalScriptingSandbox = EXPERIMENTAL_SCRIPTING_SANDBOX.value
|
||||
const target = experimentalScriptingSandbox ? "experimental" : "legacy"
|
||||
|
||||
const cleanScript = stripModulePrefix(testScript)
|
||||
// Test order: request → root (reverse of pre-request).
|
||||
const combinedScript = combineScriptsWithIIFE(
|
||||
[testScript, ...inheritedTestScripts.slice().reverse()],
|
||||
target
|
||||
)
|
||||
|
||||
// Short-circuit empty scripts to avoid unnecessary WASM initialization
|
||||
if (cleanScript.trim().length === 0) {
|
||||
if (combinedScript.length === 0) {
|
||||
return Promise.resolve(
|
||||
E.right({
|
||||
tests: { descriptor: "root", expectResults: [], children: [] },
|
||||
|
|
@ -423,20 +437,17 @@ const runPostRequestScript = (
|
|||
)
|
||||
}
|
||||
|
||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
||||
// Strip `export {};\n` before executing in legacy sandbox to prevent syntax errors
|
||||
|
||||
return runTestScript(cleanScript, {
|
||||
if (!experimentalScriptingSandbox) {
|
||||
return runTestScript(combinedScript, {
|
||||
envs,
|
||||
response,
|
||||
experimentalScriptingSandbox: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Experimental sandbox enabled - use faraday-cage with hook
|
||||
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
||||
|
||||
return runTestScript(cleanScript, {
|
||||
return runTestScript(combinedScript, {
|
||||
envs,
|
||||
request,
|
||||
response,
|
||||
|
|
@ -497,10 +508,20 @@ export function runRESTRequest$(
|
|||
initialEnvsForComparison,
|
||||
} = captureInitialEnvironmentState()
|
||||
|
||||
// Extract inherited scripts from collection hierarchy, filtering out empty/module-prefix-only scripts
|
||||
const inheritedScripts = inheritedProperties?.scripts ?? []
|
||||
const inheritedPreRequestScripts = inheritedScripts
|
||||
.map((s) => s.preRequestScript)
|
||||
.filter(hasActualScript)
|
||||
const inheritedTestScripts = inheritedScripts
|
||||
.map((s) => s.testScript)
|
||||
.filter(hasActualScript)
|
||||
|
||||
const res = delegatePreRequestScriptRunner(
|
||||
resolvedRequest,
|
||||
initialEnvs,
|
||||
cookieJarEntries
|
||||
cookieJarEntries,
|
||||
inheritedPreRequestScripts
|
||||
).then(async (preRequestScriptResult) => {
|
||||
if (cancelCalled) return E.left("cancellation" as const)
|
||||
|
||||
|
|
@ -579,7 +600,8 @@ export function runRESTRequest$(
|
|||
statusText: res.statusText,
|
||||
responseTime: res.meta.responseDuration,
|
||||
},
|
||||
preRequestScriptResult.right.updatedCookies ?? null
|
||||
preRequestScriptResult.right.updatedCookies ?? null,
|
||||
inheritedTestScripts
|
||||
)
|
||||
|
||||
if (E.isRight(postRequestScriptResult)) {
|
||||
|
|
@ -795,7 +817,9 @@ export async function runTestRunnerRequest(
|
|||
request: HoppRESTRequest,
|
||||
persistEnv = true,
|
||||
inheritedVariables: HoppCollectionVariable[] = [],
|
||||
initialEnvironmentState: InitialEnvironmentState
|
||||
initialEnvironmentState: InitialEnvironmentState,
|
||||
inheritedPreRequestScripts: string[] = [],
|
||||
inheritedTestScripts: string[] = []
|
||||
): Promise<
|
||||
| E.Left<"script_fail">
|
||||
| E.Right<{
|
||||
|
|
@ -824,7 +848,8 @@ export async function runTestRunnerRequest(
|
|||
return delegatePreRequestScriptRunner(
|
||||
request,
|
||||
initialEnvs,
|
||||
cookieJarEntries
|
||||
cookieJarEntries,
|
||||
inheritedPreRequestScripts
|
||||
).then(async (preRequestScriptResult) => {
|
||||
if (E.isLeft(preRequestScriptResult)) {
|
||||
console.error("[Pre-Request Script Error]", preRequestScriptResult.left)
|
||||
|
|
@ -883,7 +908,8 @@ export async function runTestRunnerRequest(
|
|||
statusText: res.statusText,
|
||||
responseTime: res.meta.responseDuration,
|
||||
},
|
||||
preRequestScriptResult.right.updatedCookies ?? null
|
||||
preRequestScriptResult.right.updatedCookies ?? null,
|
||||
inheritedTestScripts
|
||||
)
|
||||
|
||||
if (E.isRight(postRequestScriptResult)) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ export type CollectionDataProps = {
|
|||
headers: HoppRESTHeaders
|
||||
variables: HoppCollectionVariable[]
|
||||
description: string | null
|
||||
preRequestScript: string
|
||||
testScript: string
|
||||
}
|
||||
|
||||
export const BACKEND_PAGE_SIZE = 10
|
||||
|
|
@ -121,6 +123,8 @@ const parseCollectionData = (
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
|
|
@ -159,11 +163,23 @@ const parseCollectionData = (
|
|||
? parsedData.description
|
||||
: defaultDataProps.description
|
||||
|
||||
const preRequestScript =
|
||||
typeof parsedData?.preRequestScript === "string"
|
||||
? parsedData.preRequestScript
|
||||
: defaultDataProps.preRequestScript
|
||||
|
||||
const testScript =
|
||||
typeof parsedData?.testScript === "string"
|
||||
? parsedData.testScript
|
||||
: defaultDataProps.testScript
|
||||
|
||||
return {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,9 +187,14 @@ const parseCollectionData = (
|
|||
export const teamCollectionJSONToHoppRESTColl = (
|
||||
coll: TeamCollectionJSON
|
||||
): HoppCollection => {
|
||||
const { auth, headers, variables, description } = parseCollectionData(
|
||||
coll.data
|
||||
)
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} = parseCollectionData(coll.data)
|
||||
|
||||
return makeCollection({
|
||||
id: coll.id,
|
||||
|
|
@ -184,6 +205,8 @@ export const teamCollectionJSONToHoppRESTColl = (
|
|||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +270,14 @@ export const teamCollToHoppRESTColl = (
|
|||
description: null,
|
||||
}
|
||||
|
||||
const { auth, headers, variables, description } = parseCollectionData(data)
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} = parseCollectionData(data)
|
||||
|
||||
return makeCollection({
|
||||
id: coll.id,
|
||||
|
|
@ -258,6 +288,8 @@ export const teamCollToHoppRESTColl = (
|
|||
headers: headers ?? [],
|
||||
variables: variables ?? [],
|
||||
description: description ?? null,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ function parseCollectionDataFromString(data?: string): CollectionDataProps {
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
|
|
@ -111,6 +113,9 @@ function parseCollectionDataFromString(data?: string): CollectionDataProps {
|
|||
headers: parsed.headers || defaultDataProps.headers,
|
||||
variables: parsed.variables || defaultDataProps.variables,
|
||||
description: parsed.description || defaultDataProps.description,
|
||||
preRequestScript:
|
||||
parsed.preRequestScript || defaultDataProps.preRequestScript,
|
||||
testScript: parsed.testScript || defaultDataProps.testScript,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse collection data:", error)
|
||||
|
|
@ -127,8 +132,14 @@ export function collectionFolderToHoppCollection(
|
|||
folder: CollectionFolder
|
||||
): HoppCollection {
|
||||
// Parse the data field to extract auth, headers, variables, and description
|
||||
const { auth, headers, variables, description } =
|
||||
parseCollectionDataFromString(folder.data)
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} = parseCollectionDataFromString(folder.data)
|
||||
|
||||
return makeCollection({
|
||||
name: folder.name,
|
||||
|
|
@ -139,6 +150,8 @@ export function collectionFolderToHoppCollection(
|
|||
variables,
|
||||
description,
|
||||
id: folder.id,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -311,6 +311,8 @@ export function transformCollectionForImport(
|
|||
headers: collection.headers,
|
||||
variables: collection.variables,
|
||||
description: collection.description,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
const obj: CollectionFolder = {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export const harImporter = (
|
|||
headers: [],
|
||||
description: null,
|
||||
variables: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
return E.right([collection])
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@ const getHoppFolder = (
|
|||
headers: [],
|
||||
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
|
||||
description: folderRes.meta?.description ?? null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
const getHoppCollections = (docs: InsomniaDoc[]) => {
|
||||
|
|
@ -283,6 +285,8 @@ const getParsedHoppFolder = (
|
|||
headers: [],
|
||||
variables: getCollectionVariables(collection.environment),
|
||||
description: collection.meta.description ?? null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -323,6 +327,8 @@ const getParsedHoppCollections = (docs: InsomniaDocV5[]): HoppCollection[] =>
|
|||
headers: [],
|
||||
variables: getCollectionVariables(doc.environments?.data),
|
||||
description: doc.meta.description ?? null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1186,6 +1186,8 @@ const convertOpenApiDocsToHopp = (
|
|||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
variables: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
),
|
||||
requests: requestsWithoutTags,
|
||||
|
|
@ -1193,6 +1195,8 @@ const convertOpenApiDocsToHopp = (
|
|||
headers: [],
|
||||
variables: [],
|
||||
description,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -550,6 +550,36 @@ const getHoppScripts = (
|
|||
return { preRequestScript, testScript }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts pre-request and test scripts from a Postman ItemGroup (collection/folder)
|
||||
* Postman collections and folders can have their own scripts that run before/after all requests
|
||||
*/
|
||||
const getHoppCollectionScripts = (
|
||||
ig: ItemGroup<Item>,
|
||||
importScripts: boolean
|
||||
): { preRequestScript: string; testScript: string } => {
|
||||
if (!importScripts) {
|
||||
return { preRequestScript: "", testScript: "" }
|
||||
}
|
||||
|
||||
let preRequestScript = ""
|
||||
let testScript = ""
|
||||
|
||||
// ItemGroup (collection/folder) stores scripts in the events property
|
||||
if (ig.events) {
|
||||
const events = ig.events.all()
|
||||
events.forEach((event: any) => {
|
||||
if (event.listen === "prerequest") {
|
||||
preRequestScript = extractScriptFromEvent(event)
|
||||
} else if (event.listen === "test") {
|
||||
testScript = extractScriptFromEvent(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { preRequestScript, testScript }
|
||||
}
|
||||
|
||||
const getCollectionDescription = (
|
||||
docField?: string | DescriptionDefinition
|
||||
): string | null => {
|
||||
|
|
@ -609,8 +639,13 @@ const getHoppRequest = (
|
|||
const getHoppFolder = (
|
||||
ig: ItemGroup<Item>,
|
||||
importScripts: boolean
|
||||
): HoppCollection =>
|
||||
makeCollection({
|
||||
): HoppCollection => {
|
||||
const { preRequestScript, testScript } = getHoppCollectionScripts(
|
||||
ig,
|
||||
importScripts
|
||||
)
|
||||
|
||||
return makeCollection({
|
||||
name: ig.name,
|
||||
folders: pipe(
|
||||
ig.items.all(),
|
||||
|
|
@ -626,7 +661,10 @@ const getHoppFolder = (
|
|||
headers: [],
|
||||
variables: getHoppCollVariables(ig),
|
||||
description: getCollectionDescription(ig.description),
|
||||
preRequestScript,
|
||||
testScript,
|
||||
})
|
||||
}
|
||||
|
||||
export const getHoppCollections = (
|
||||
collections: PMCollection[],
|
||||
|
|
|
|||
|
|
@ -323,5 +323,9 @@ export function createExamplePetStoreCollection(
|
|||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,6 +328,9 @@ export async function createMockCollectionForPersonal(
|
|||
auth: data.auth,
|
||||
headers: data.headers,
|
||||
variables: data.variables,
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
// Add the backend ID to the collection
|
||||
|
|
|
|||
|
|
@ -9,14 +9,77 @@ export const MODULE_PREFIX = "export {};\n" as const
|
|||
* (non-module context) or when exporting collections.
|
||||
*/
|
||||
export const stripModulePrefix = (script: string): string => {
|
||||
return script.startsWith(MODULE_PREFIX)
|
||||
? script.slice(MODULE_PREFIX.length)
|
||||
: script
|
||||
if (script.startsWith(MODULE_PREFIX)) {
|
||||
return script.slice(MODULE_PREFIX.length)
|
||||
}
|
||||
if (script.startsWith("export {};")) {
|
||||
return script.slice("export {};".length)
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex for stripping the JSON-serialized module prefix (`export {};\\n`)
|
||||
* from scripts during collection exports.
|
||||
* Note: This matches the literal backslash-n (`\\n`), not an actual newline character.
|
||||
* Anchored to JSON value-opening delimiters so it only matches inside JSON
|
||||
* string values during collection export, not inside script source. Matches
|
||||
* both `export {};\\n` and `export {};` (`\\n` is the literal backslash-n
|
||||
* pair, not a newline).
|
||||
*/
|
||||
export const MODULE_PREFIX_REGEX_JSON_SERIALIZED = /export \{\};\\n/g
|
||||
export const MODULE_PREFIX_REGEX_JSON_SERIALIZED =
|
||||
/(?<=:\s*")export \{\};(?:\\n)?/g
|
||||
|
||||
export type CombineScriptsTarget = "experimental" | "legacy"
|
||||
|
||||
const wrapScript = (script: string, target: CombineScriptsTarget): string => {
|
||||
const stripped = stripModulePrefix(script.trim())
|
||||
if (!stripped) return ""
|
||||
const asyncKeyword = target === "experimental" ? "async " : ""
|
||||
return `${asyncKeyword}function() {\n${stripped}\n}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines inherited scripts into a sequential chain. Each script runs in
|
||||
* its own function for scope isolation.
|
||||
*
|
||||
* - `experimental`: `await (async function(){...})();` lines, evaluated in
|
||||
* an async host context so each `await` settles before the next runs.
|
||||
* - `legacy`: sync `(function(){...}).call(this);` lines. Top-level `await`
|
||||
* is rejected at parse time.
|
||||
*/
|
||||
export const combineScriptsWithIIFE = (
|
||||
scripts: string[],
|
||||
target: CombineScriptsTarget = "experimental"
|
||||
): string => {
|
||||
const fns = scripts.map((s) => wrapScript(s, target)).filter((s) => s)
|
||||
if (fns.length === 0) return ""
|
||||
if (target === "experimental") {
|
||||
// Wrap the entire awaited chain in try/catch so a top-level throw (or a
|
||||
// rejected await) surfaces synchronously via the host reporter.
|
||||
// faraday-cage swallows rejected keepAlive promises and does not await
|
||||
// afterScriptExecutionHooks, so this is the only reliable channel for
|
||||
// async-boundary errors to reach the host caller.
|
||||
//
|
||||
// The reporter is captured in a const before the try so a user script
|
||||
// that tampers with `globalThis.__hoppReportScriptExecutionError`
|
||||
// inside the try body cannot suppress the report. Bootstrap installs
|
||||
// the property as non-writable and non-configurable for defense in
|
||||
// depth; the lexical capture makes that redundant but explicit.
|
||||
const body = fns.map((fn) => `await (${fn})();`).join("\n")
|
||||
return [
|
||||
"const __hoppReporter = globalThis.__hoppReportScriptExecutionError;",
|
||||
"try {",
|
||||
body,
|
||||
"} catch (__hoppScriptExecutionError) {",
|
||||
" __hoppReporter(__hoppScriptExecutionError);",
|
||||
"}",
|
||||
].join("\n")
|
||||
}
|
||||
// Leading `;` guards against ASI: a prior `})` on the host line would
|
||||
// otherwise be read as a call against our IIFE expression.
|
||||
return fns.map((fn) => `;(${fn}).call(this);`).join("\n")
|
||||
}
|
||||
|
||||
// Monaco prepends "export {};\n" to empty scripts — strip before checking.
|
||||
export const hasActualScript = (script: string | undefined | null): boolean => {
|
||||
if (!script) return false
|
||||
return stripModulePrefix(script.trim()).length > 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
import { BehaviorSubject, Subscription } from "rxjs"
|
||||
import {
|
||||
HoppCollectionVariable,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
translateToNewRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { HoppCollectionVariable, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { pull, remove } from "lodash-es"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { CollectionDataProps } from "~/helpers/backend/helpers"
|
||||
import { Subscription as WSubscription } from "wonka"
|
||||
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
|
||||
import { TeamCollection } from "./TeamCollection"
|
||||
|
|
@ -1093,14 +1090,16 @@ export default class NewTeamCollectionAdapter {
|
|||
|
||||
const variables: HoppInheritedProperty["variables"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables }
|
||||
const scripts: HoppInheritedProperty["scripts"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables, scripts }
|
||||
|
||||
const path = folderPath.split("/")
|
||||
|
||||
// Check if the path is empty or invalid
|
||||
if (!path || path.length === 0) {
|
||||
console.error("Invalid path:", folderPath)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
// Loop through the path and get the last parent folder with authType other than 'inherit'
|
||||
|
|
@ -1110,20 +1109,12 @@ export default class NewTeamCollectionAdapter {
|
|||
// Check if parentFolder is undefined or null
|
||||
if (!parentFolder) {
|
||||
console.error("Parent folder not found for path:", path)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
const data: {
|
||||
auth: HoppRESTAuth
|
||||
headers: HoppRESTHeader[]
|
||||
variables: HoppCollectionVariable[]
|
||||
} = parentFolder.data
|
||||
const data: Partial<CollectionDataProps> = parentFolder.data
|
||||
? JSON.parse(parentFolder.data)
|
||||
: {
|
||||
auth: null,
|
||||
headers: null,
|
||||
variables: null,
|
||||
}
|
||||
: {}
|
||||
|
||||
if (!data.auth) {
|
||||
data.auth = {
|
||||
|
|
@ -1141,6 +1132,8 @@ export default class NewTeamCollectionAdapter {
|
|||
const parentFolderAuth = data.auth
|
||||
const parentFolderHeaders = data.headers
|
||||
const parentFolderVariables = data.variables
|
||||
const parentFolderPreRequestScript = data.preRequestScript ?? ""
|
||||
const parentFolderTestScript = data.testScript ?? ""
|
||||
|
||||
if (
|
||||
parentFolderAuth?.authType === "inherit" &&
|
||||
|
|
@ -1200,8 +1193,23 @@ export default class NewTeamCollectionAdapter {
|
|||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Update scripts
|
||||
if (
|
||||
hasActualScript(parentFolderPreRequestScript) ||
|
||||
hasActualScript(parentFolderTestScript)
|
||||
) {
|
||||
const currentPath = [...path.slice(0, i + 1)].join("/")
|
||||
|
||||
scripts.push({
|
||||
parentID: parentFolder.id ?? currentPath,
|
||||
parentName: parentFolder.title,
|
||||
preRequestScript: parentFolderPreRequestScript,
|
||||
testScript: parentFolderTestScript,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import {
|
||||
HoppCollectionVariable,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
HoppRESTRequest,
|
||||
getDefaultRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
|
|
@ -10,6 +8,7 @@ import { Service } from "dioc"
|
|||
import * as E from "fp-ts/Either"
|
||||
import { Ref, ref } from "vue"
|
||||
import { getSingleCollection, TeamCollection } from "./TeamCollection"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
|
||||
|
|
@ -19,6 +18,24 @@ import {
|
|||
TeamRequest,
|
||||
getCollectionChildCollections,
|
||||
} from "./TeamRequest"
|
||||
import { CollectionDataProps } from "../backend/helpers"
|
||||
|
||||
/**
|
||||
* Parses collection data that may be double-encoded JSON
|
||||
* Handles both single and double JSON stringification
|
||||
*/
|
||||
const parseCollectionData = (data: string): CollectionDataProps | null => {
|
||||
try {
|
||||
let parsed = JSON.parse(data)
|
||||
// Handle double-encoded JSON (string containing JSON string)
|
||||
if (typeof parsed === "string") {
|
||||
parsed = JSON.parse(parsed)
|
||||
}
|
||||
return parsed as CollectionDataProps
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
type CollectionSearchMeta = {
|
||||
isSearchResult?: boolean
|
||||
|
|
@ -354,6 +371,8 @@ export class TeamSearchService extends Service {
|
|||
|
||||
const defaultInheritedVariables: HoppInheritedProperty["variables"] = []
|
||||
|
||||
const defaultInheritedScripts: HoppInheritedProperty["scripts"] = []
|
||||
|
||||
const collection = Object.values(this.searchResultsCollections).find(
|
||||
(col) => col.id === collectionID
|
||||
)
|
||||
|
|
@ -363,12 +382,14 @@ export class TeamSearchService extends Service {
|
|||
auth: defaultInheritedAuth,
|
||||
headers: defaultInheritedHeaders,
|
||||
variables: defaultInheritedVariables,
|
||||
scripts: defaultInheritedScripts,
|
||||
}
|
||||
|
||||
const inheritedAuthData = this.findInheritableParentAuth(collectionID)
|
||||
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
|
||||
const inheritedVariablesData =
|
||||
this.findInheritableParentVariables(collectionID)
|
||||
const inheritedScriptsData = this.findInheritableParentScripts(collectionID)
|
||||
|
||||
return {
|
||||
auth: E.isRight(inheritedAuthData)
|
||||
|
|
@ -380,6 +401,9 @@ export class TeamSearchService extends Service {
|
|||
variables: E.isRight(inheritedVariablesData)
|
||||
? Object.values(inheritedVariablesData.right)
|
||||
: defaultInheritedVariables,
|
||||
scripts: E.isRight(inheritedScriptsData)
|
||||
? Object.values(inheritedScriptsData.right)
|
||||
: defaultInheritedScripts,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,13 +427,9 @@ export class TeamSearchService extends Service {
|
|||
|
||||
// has inherited data
|
||||
if (collection.data) {
|
||||
const parentInheritedData = JSON.parse(collection.data) as {
|
||||
auth?: HoppRESTAuth
|
||||
headers?: HoppRESTHeader[]
|
||||
variables?: HoppCollectionVariable[]
|
||||
}
|
||||
const parentInheritedData = parseCollectionData(collection.data)
|
||||
|
||||
const inheritedAuth = parentInheritedData.auth
|
||||
const inheritedAuth = parentInheritedData?.auth
|
||||
|
||||
if (inheritedAuth && inheritedAuth.authType !== "inherit") {
|
||||
return E.right({
|
||||
|
|
@ -447,13 +467,9 @@ export class TeamSearchService extends Service {
|
|||
|
||||
// see if it has headers to inherit, if yes, add it to the existing headers
|
||||
if (collection.data) {
|
||||
const parentInheritedData = JSON.parse(collection.data) as {
|
||||
auth?: HoppRESTAuth
|
||||
headers?: HoppRESTHeader[]
|
||||
variables?: HoppCollectionVariable[]
|
||||
}
|
||||
const parentInheritedData = parseCollectionData(collection.data)
|
||||
|
||||
const inheritedHeaders = parentInheritedData.headers
|
||||
const inheritedHeaders = parentInheritedData?.headers
|
||||
|
||||
if (inheritedHeaders) {
|
||||
inheritedHeaders.forEach((header) => {
|
||||
|
|
@ -493,13 +509,9 @@ export class TeamSearchService extends Service {
|
|||
}
|
||||
|
||||
if (collection.data) {
|
||||
const parentData = JSON.parse(collection.data) as {
|
||||
auth?: HoppRESTAuth
|
||||
headers?: HoppRESTHeader[]
|
||||
variables?: HoppCollectionVariable[]
|
||||
}
|
||||
const parentData = parseCollectionData(collection.data)
|
||||
|
||||
const variables = parentData.variables
|
||||
const variables = parentData?.variables
|
||||
|
||||
if (variables) {
|
||||
vars.push({
|
||||
|
|
@ -517,6 +529,51 @@ export class TeamSearchService extends Service {
|
|||
return E.right(vars)
|
||||
}
|
||||
|
||||
findInheritableParentScripts = (
|
||||
collectionID: string,
|
||||
existingScripts: HoppInheritedProperty["scripts"] = []
|
||||
): E.Either<string, HoppInheritedProperty["scripts"]> => {
|
||||
const collection = Object.values(this.searchResultsCollections).find(
|
||||
(col) => col.id === collectionID
|
||||
)
|
||||
|
||||
if (!collection) {
|
||||
return E.left("PARENT_NOT_FOUND" as const)
|
||||
}
|
||||
|
||||
// Recurse to parent first to build root→parent→child order
|
||||
let scripts = [...existingScripts]
|
||||
if (collection.parentID) {
|
||||
const parentResult = this.findInheritableParentScripts(
|
||||
collection.parentID,
|
||||
scripts
|
||||
)
|
||||
if (E.isLeft(parentResult)) {
|
||||
return parentResult
|
||||
}
|
||||
scripts = parentResult.right
|
||||
}
|
||||
|
||||
// Then add current collection's scripts
|
||||
if (collection.data) {
|
||||
const parentData = parseCollectionData(collection.data)
|
||||
|
||||
const preRequestScript = parentData?.preRequestScript ?? ""
|
||||
const testScript = parentData?.testScript ?? ""
|
||||
|
||||
if (hasActualScript(preRequestScript) || hasActualScript(testScript)) {
|
||||
scripts.push({
|
||||
parentID: collection.id,
|
||||
parentName: collection.title,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return E.right(scripts)
|
||||
}
|
||||
|
||||
expandCollection = async (collectionID: string) => {
|
||||
if (this.expandingCollections.value.includes(collectionID)) return
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,10 @@ export type HoppInheritedProperty = {
|
|||
parentName: string
|
||||
inheritedVariables: HoppCollectionVariable[]
|
||||
}[]
|
||||
scripts: {
|
||||
parentID: string
|
||||
parentName: string
|
||||
preRequestScript: string
|
||||
testScript: string
|
||||
}[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
GQLHeader,
|
||||
} from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { pluck } from "rxjs/operators"
|
||||
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
|
@ -38,6 +39,8 @@ const defaultRESTCollectionState = {
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
|
@ -55,6 +58,8 @@ const defaultGraphqlCollectionState = {
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
|
@ -142,14 +147,16 @@ export function cascadeParentCollectionForProperties(
|
|||
|
||||
const variables: HoppInheritedProperty["variables"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables }
|
||||
const scripts: HoppInheritedProperty["scripts"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables, scripts }
|
||||
|
||||
const path = folderPath.split("/").map((i) => parseInt(i))
|
||||
|
||||
// Check if the path is empty or invalid
|
||||
if (!path || path.length === 0) {
|
||||
console.error("Invalid path:", folderPath)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
// Loop through the path and get the last parent folder with authType other than 'inherit'
|
||||
|
|
@ -162,7 +169,7 @@ export function cascadeParentCollectionForProperties(
|
|||
// Check if parentFolder is undefined or null
|
||||
if (!parentFolder) {
|
||||
console.error("Parent folder not found for path:", path)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth
|
||||
|
|
@ -223,9 +230,27 @@ export function cascadeParentCollectionForProperties(
|
|||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Collect scripts from the collection hierarchy (root to child order)
|
||||
const parentPreRequestScript = parentFolder.preRequestScript ?? ""
|
||||
const parentTestScript = parentFolder.testScript ?? ""
|
||||
|
||||
if (
|
||||
hasActualScript(parentPreRequestScript) ||
|
||||
hasActualScript(parentTestScript)
|
||||
) {
|
||||
const currentPath = [...path.slice(0, i + 1)].join("/")
|
||||
|
||||
scripts.push({
|
||||
parentID: parentFolder._ref_id ?? parentFolder.id ?? currentPath,
|
||||
parentName: parentFolder.name,
|
||||
preRequestScript: parentPreRequestScript,
|
||||
testScript: parentTestScript,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
function reorderItems(array: unknown[], from: number, to: number) {
|
||||
|
|
@ -365,6 +390,8 @@ const restCollectionDispatchers = defineDispatchers({
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
const newState = state
|
||||
|
|
@ -1026,6 +1053,8 @@ const gqlCollectionDispatchers = defineDispatchers({
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
const newState = state
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
|
@ -1367,18 +1396,26 @@ export function getRESTCollection(collectionIndex: number) {
|
|||
return restCollectionStore.value.state[collectionIndex]
|
||||
}
|
||||
|
||||
export type RESTCollectionInheritedProps = {
|
||||
auth: HoppRESTAuth
|
||||
headers: HoppRESTHeaders
|
||||
variables: HoppCollectionVariable[]
|
||||
// Ancestor scripts for partial-scope runs (root → target's parent).
|
||||
// Empty when running from the topmost collection.
|
||||
ancestorPreRequestScripts: string[]
|
||||
ancestorTestScripts: string[]
|
||||
}
|
||||
|
||||
function computeCollectionInheritedProps(
|
||||
collection: HoppCollection,
|
||||
ref_id: string,
|
||||
type: "my-collections" | "team-collections" = "my-collections",
|
||||
parentAuth: HoppRESTAuth | null = null,
|
||||
parentHeaders: HoppRESTHeaders | null = null,
|
||||
parentVariables: HoppCollectionVariable[] | null = null
|
||||
): {
|
||||
auth: HoppRESTAuth
|
||||
headers: HoppRESTHeaders
|
||||
variables: HoppCollectionVariable[]
|
||||
} | null {
|
||||
parentVariables: HoppCollectionVariable[] | null = null,
|
||||
parentPreRequestScripts: string[] = [],
|
||||
parentTestScripts: string[] = []
|
||||
): RESTCollectionInheritedProps | null {
|
||||
// Determine the inherited authentication and headers
|
||||
const inheritedAuth =
|
||||
collection.auth?.authType === "inherit" && collection.auth.authActive
|
||||
|
|
@ -1406,9 +1443,19 @@ function computeCollectionInheritedProps(
|
|||
auth: inheritedAuth,
|
||||
headers: inheritedHeaders,
|
||||
variables: inheritedVariables,
|
||||
ancestorPreRequestScripts: parentPreRequestScripts,
|
||||
ancestorTestScripts: parentTestScripts,
|
||||
}
|
||||
}
|
||||
|
||||
// Append this collection's scripts before descending so children see them.
|
||||
const nextPreRequestScripts = hasActualScript(collection.preRequestScript)
|
||||
? [...parentPreRequestScripts, collection.preRequestScript]
|
||||
: parentPreRequestScripts
|
||||
const nextTestScripts = hasActualScript(collection.testScript)
|
||||
? [...parentTestScripts, collection.testScript]
|
||||
: parentTestScripts
|
||||
|
||||
// Recursively search in folders
|
||||
for (const folder of collection.folders) {
|
||||
const result = computeCollectionInheritedProps(
|
||||
|
|
@ -1417,7 +1464,9 @@ function computeCollectionInheritedProps(
|
|||
type,
|
||||
inheritedAuth,
|
||||
inheritedHeaders,
|
||||
inheritedVariables
|
||||
inheritedVariables,
|
||||
nextPreRequestScripts,
|
||||
nextTestScripts
|
||||
)
|
||||
if (result) return result // Return as soon as a match is found
|
||||
}
|
||||
|
|
@ -1429,11 +1478,7 @@ export function getRESTCollectionInheritedProps(
|
|||
collectionID: string,
|
||||
collections: HoppCollection[] = restCollectionStore.value.state,
|
||||
type: "my-collections" | "team-collections" = "my-collections"
|
||||
): {
|
||||
auth: HoppRESTAuth
|
||||
headers: HoppRESTHeaders
|
||||
variables: HoppCollectionVariable[]
|
||||
} | null {
|
||||
): RESTCollectionInheritedProps | null {
|
||||
for (const collection of collections) {
|
||||
const result = computeCollectionInheritedProps(
|
||||
collection,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import {
|
|||
translateToNewEnvironmentVariables,
|
||||
} from "@hoppscotch/data"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import {
|
||||
PublishedDocREST,
|
||||
PublishedDocsVersion,
|
||||
|
|
@ -151,6 +152,23 @@ const flattenCollection = (
|
|||
})),
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
...(inheritedProperties?.scripts || []),
|
||||
...(hasActualScript(collection.preRequestScript) ||
|
||||
hasActualScript(collection.testScript)
|
||||
? [
|
||||
{
|
||||
parentID:
|
||||
collection.id ||
|
||||
collection._ref_id ||
|
||||
`collection-${collection.name}`,
|
||||
parentName: collection.name,
|
||||
preRequestScript: collection.preRequestScript || "",
|
||||
testScript: collection.testScript || "",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
|
||||
if (collection.folders && collection.folders.length > 0) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ describe("DocumentationService", () => {
|
|||
variables: [],
|
||||
id: "collection-123",
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
})
|
||||
|
||||
const mockRequest: HoppRESTRequest = makeRESTRequest({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
CollectionSchemaVersion,
|
||||
Environment,
|
||||
GlobalEnvironment,
|
||||
HoppCollection,
|
||||
|
|
@ -25,7 +26,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
|||
|
||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
name: "Echo",
|
||||
requests: [
|
||||
{
|
||||
|
|
@ -54,13 +55,15 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
folders: [],
|
||||
},
|
||||
]
|
||||
|
||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 11,
|
||||
v: CollectionSchemaVersion,
|
||||
name: "Echo",
|
||||
requests: [
|
||||
{
|
||||
|
|
@ -80,6 +83,8 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
folders: [],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -325,6 +325,16 @@ const HoppInheritedPropertySchema = z
|
|||
})
|
||||
)
|
||||
.catch([]),
|
||||
scripts: z
|
||||
.array(
|
||||
z.object({
|
||||
parentID: z.string(),
|
||||
parentName: z.string(),
|
||||
preRequestScript: z.string(),
|
||||
testScript: z.string(),
|
||||
})
|
||||
)
|
||||
.catch([]),
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
import { Subscription } from "rxjs"
|
||||
import {
|
||||
HoppCollectionVariable,
|
||||
HoppRESTAuth,
|
||||
HoppRESTHeader,
|
||||
translateToNewRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { HoppCollectionVariable, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { pull, remove } from "lodash-es"
|
||||
import { Subscription as WSubscription } from "wonka"
|
||||
import {
|
||||
|
|
@ -34,6 +29,8 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
|||
import { ref, watch } from "vue"
|
||||
import { Service } from "dioc"
|
||||
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import { CollectionDataProps } from "~/helpers/backend/helpers"
|
||||
|
||||
export const TEAMS_BACKEND_PAGE_SIZE = 10
|
||||
|
||||
|
|
@ -1123,14 +1120,16 @@ export class TeamCollectionsService extends Service<void> {
|
|||
|
||||
const variables: HoppInheritedProperty["variables"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables }
|
||||
const scripts: HoppInheritedProperty["scripts"] = []
|
||||
|
||||
if (!folderPath) return { auth, headers, variables, scripts }
|
||||
|
||||
const path = folderPath.split("/")
|
||||
|
||||
// Check if the path is empty or invalid
|
||||
if (!path || path.length === 0) {
|
||||
console.error("Invalid path:", folderPath)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
// Loop through the path and get the last parent folder with authType other than 'inherit'
|
||||
|
|
@ -1140,20 +1139,12 @@ export class TeamCollectionsService extends Service<void> {
|
|||
// Check if parentFolder is undefined or null
|
||||
if (!parentFolder) {
|
||||
console.error("Parent folder not found for path:", path)
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
const data: {
|
||||
auth?: HoppRESTAuth
|
||||
headers?: HoppRESTHeader[]
|
||||
variables?: HoppCollectionVariable[]
|
||||
} = parentFolder.data
|
||||
const data: Partial<CollectionDataProps> = parentFolder.data
|
||||
? JSON.parse(parentFolder.data)
|
||||
: {
|
||||
auth: null,
|
||||
headers: null,
|
||||
variables: null,
|
||||
}
|
||||
: {}
|
||||
|
||||
if (!data.auth) {
|
||||
data.auth = {
|
||||
|
|
@ -1230,9 +1221,27 @@ export class TeamCollectionsService extends Service<void> {
|
|||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Collect scripts from the collection hierarchy (root to child order)
|
||||
const parentPreRequestScript = data.preRequestScript ?? ""
|
||||
const parentTestScript = data.testScript ?? ""
|
||||
|
||||
if (
|
||||
hasActualScript(parentPreRequestScript) ||
|
||||
hasActualScript(parentTestScript)
|
||||
) {
|
||||
const currentPath = path.slice(0, i + 1).join("/")
|
||||
|
||||
scripts.push({
|
||||
parentID: parentFolder.id ?? currentPath,
|
||||
parentName: parentFolder.title,
|
||||
preRequestScript: parentPreRequestScript,
|
||||
testScript: parentTestScript,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { auth, headers, variables }
|
||||
return { auth, headers, variables, scripts }
|
||||
}
|
||||
|
||||
private async waitForCollectionLoading(collectionID: string) {
|
||||
|
|
@ -1260,6 +1269,7 @@ export class TeamCollectionsService extends Service<void> {
|
|||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
scripts: [],
|
||||
}
|
||||
|
||||
const path = folderPath.split("/")
|
||||
|
|
@ -1278,6 +1288,7 @@ export class TeamCollectionsService extends Service<void> {
|
|||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
scripts: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { Service } from "dioc"
|
||||
import { hasActualScript } from "~/helpers/scripting"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { nextTick, Ref } from "vue"
|
||||
|
|
@ -52,7 +53,9 @@ export class TestRunnerService extends Service {
|
|||
public runTests(
|
||||
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
||||
collection: HoppCollection,
|
||||
options: TestRunnerOptions
|
||||
options: TestRunnerOptions,
|
||||
ancestorPreRequestScripts: string[] = [],
|
||||
ancestorTestScripts: string[] = []
|
||||
) {
|
||||
// Reset the result collection
|
||||
tab.value.document.status = "running"
|
||||
|
|
@ -66,9 +69,22 @@ export class TestRunnerService extends Service {
|
|||
requests: [],
|
||||
variables: [],
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
this.runTestCollection(tab, collection, options)
|
||||
this.runTestCollection(
|
||||
tab,
|
||||
collection,
|
||||
options,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
ancestorPreRequestScripts,
|
||||
ancestorTestScripts
|
||||
)
|
||||
.then(() => {
|
||||
tab.value.document.status = "stopped"
|
||||
})
|
||||
|
|
@ -96,7 +112,9 @@ export class TestRunnerService extends Service {
|
|||
parentHeaders?: HoppRESTHeaders,
|
||||
parentAuth?: HoppRESTRequest["auth"],
|
||||
parentVariables: HoppCollection["variables"] = [],
|
||||
parentID?: string
|
||||
parentID?: string,
|
||||
parentPreRequestScripts: string[] = [],
|
||||
parentTestScripts: string[] = []
|
||||
) {
|
||||
try {
|
||||
// Compute inherited auth and headers for this collection
|
||||
|
|
@ -121,6 +139,19 @@ export class TestRunnerService extends Service {
|
|||
) || []),
|
||||
]
|
||||
|
||||
const inheritedPreRequestScripts = [
|
||||
...parentPreRequestScripts,
|
||||
...(hasActualScript(collection.preRequestScript)
|
||||
? [collection.preRequestScript]
|
||||
: []),
|
||||
]
|
||||
const inheritedTestScripts = [
|
||||
...parentTestScripts,
|
||||
...(hasActualScript(collection.testScript)
|
||||
? [collection.testScript]
|
||||
: []),
|
||||
]
|
||||
|
||||
// Process folders progressively
|
||||
for (let i = 0; i < collection.folders.length; i++) {
|
||||
if (options.stopRef?.value) {
|
||||
|
|
@ -142,7 +173,6 @@ export class TestRunnerService extends Service {
|
|||
}
|
||||
)
|
||||
|
||||
// Pass inherited headers and auth to the folder
|
||||
await this.runTestCollection(
|
||||
tab,
|
||||
folder,
|
||||
|
|
@ -151,7 +181,9 @@ export class TestRunnerService extends Service {
|
|||
inheritedHeaders,
|
||||
inheritedAuth,
|
||||
inheritedVariables,
|
||||
collection._ref_id || collection.id
|
||||
collection._ref_id || collection.id,
|
||||
inheritedPreRequestScripts,
|
||||
inheritedTestScripts
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +220,9 @@ export class TestRunnerService extends Service {
|
|||
collection,
|
||||
options,
|
||||
currentPath,
|
||||
inheritedVariables
|
||||
inheritedVariables,
|
||||
inheritedPreRequestScripts,
|
||||
inheritedTestScripts
|
||||
)
|
||||
|
||||
if (options.delay && options.delay > 0) {
|
||||
|
|
@ -279,7 +313,9 @@ export class TestRunnerService extends Service {
|
|||
collection: HoppCollection,
|
||||
options: TestRunnerOptions,
|
||||
path: number[],
|
||||
inheritedVariables: HoppCollectionVariable[] = []
|
||||
inheritedVariables: HoppCollectionVariable[] = [],
|
||||
inheritedPreRequestScripts: string[] = [],
|
||||
inheritedTestScripts: string[] = []
|
||||
) {
|
||||
if (options.stopRef?.value) {
|
||||
throw new Error("Test execution stopped")
|
||||
|
|
@ -305,7 +341,9 @@ export class TestRunnerService extends Service {
|
|||
request,
|
||||
options.keepVariableValues,
|
||||
inheritedVariables,
|
||||
initialEnvironmentState
|
||||
initialEnvironmentState,
|
||||
inheritedPreRequestScripts,
|
||||
inheritedTestScripts
|
||||
)
|
||||
|
||||
if (options.stopRef?.value) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import V8_VERSION from "./v/8"
|
|||
import V9_VERSION from "./v/9"
|
||||
import V10_VERSION from "./v/10"
|
||||
import V11_VERSION from "./v/11"
|
||||
import V12_VERSION from "./v/12"
|
||||
|
||||
export { CollectionVariable } from "./v/10"
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ const versionedObject = z.object({
|
|||
})
|
||||
|
||||
export const HoppCollection = createVersionedEntity({
|
||||
latestVersion: 11,
|
||||
latestVersion: 12,
|
||||
versionMap: {
|
||||
1: V1_VERSION,
|
||||
2: V2_VERSION,
|
||||
|
|
@ -37,6 +38,7 @@ export const HoppCollection = createVersionedEntity({
|
|||
9: V9_VERSION,
|
||||
10: V10_VERSION,
|
||||
11: V11_VERSION,
|
||||
12: V12_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
const versionCheck = versionedObject.safeParse(data)
|
||||
|
|
@ -56,7 +58,7 @@ export type HoppCollectionVariable = InferredEntity<
|
|||
typeof HoppCollection
|
||||
>["variables"][number]
|
||||
|
||||
export const CollectionSchemaVersion = 11
|
||||
export const CollectionSchemaVersion = 12
|
||||
|
||||
/**
|
||||
* Generates a Collection object. This ignores the version number object
|
||||
|
|
@ -88,6 +90,9 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
|
|||
|
||||
const description = x.description ?? null
|
||||
|
||||
const preRequestScript = x.preRequestScript ?? ""
|
||||
const testScript = x.testScript ?? ""
|
||||
|
||||
const obj = makeCollection({
|
||||
name,
|
||||
folders,
|
||||
|
|
@ -96,6 +101,8 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
|
|||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
|
@ -123,6 +130,9 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
|
|||
|
||||
const description = x.description ?? null
|
||||
|
||||
const preRequestScript = x.preRequestScript ?? ""
|
||||
const testScript = x.testScript ?? ""
|
||||
|
||||
const obj = makeCollection({
|
||||
name,
|
||||
folders,
|
||||
|
|
@ -131,6 +141,8 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
|
|||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineVersion, entityRefUptoVersion } from "verzod"
|
|||
import { z } from "zod"
|
||||
|
||||
import { HoppCollection } from ".."
|
||||
import { v9_baseCollectionSchema } from "./9"
|
||||
import { v9_baseCollectionSchema, V9_SCHEMA } from "./9"
|
||||
|
||||
export const CollectionVariable = z.object({
|
||||
key: z.string(),
|
||||
|
|
@ -33,7 +33,7 @@ export const V10_SCHEMA = v10_baseCollectionSchema.extend({
|
|||
export default defineVersion({
|
||||
initial: false,
|
||||
schema: V10_SCHEMA,
|
||||
up(old: z.infer<typeof V10_SCHEMA>) {
|
||||
up(old: z.infer<typeof V9_SCHEMA>) {
|
||||
const result: z.infer<typeof V10_SCHEMA> = {
|
||||
...old,
|
||||
v: 10 as const,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineVersion, entityRefUptoVersion } from "verzod"
|
|||
import { z } from "zod"
|
||||
|
||||
import { HoppCollection } from ".."
|
||||
import { v10_baseCollectionSchema } from "./10"
|
||||
import { v10_baseCollectionSchema, V10_SCHEMA } from "./10"
|
||||
|
||||
export const v11_baseCollectionSchema = v10_baseCollectionSchema.extend({
|
||||
v: z.literal(11),
|
||||
|
|
@ -24,11 +24,11 @@ export const V11_SCHEMA = v11_baseCollectionSchema.extend({
|
|||
export default defineVersion({
|
||||
initial: false,
|
||||
schema: V11_SCHEMA,
|
||||
up(old: z.infer<typeof V11_SCHEMA>) {
|
||||
up(old: z.infer<typeof V10_SCHEMA>) {
|
||||
const result: z.infer<typeof V11_SCHEMA> = {
|
||||
...old,
|
||||
v: 11 as const,
|
||||
description: old.description ?? null,
|
||||
description: null,
|
||||
folders: old.folders.map((folder) => {
|
||||
const result = HoppCollection.safeParseUpToVersion(folder, 11)
|
||||
|
||||
|
|
|
|||
47
packages/hoppscotch-data/src/collection/v/12.ts
Normal file
47
packages/hoppscotch-data/src/collection/v/12.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { defineVersion, entityRefUptoVersion } from "verzod"
|
||||
import { z } from "zod"
|
||||
|
||||
import { HoppCollection } from ".."
|
||||
import { v11_baseCollectionSchema, V11_SCHEMA } from "./11"
|
||||
|
||||
export const v12_baseCollectionSchema = v11_baseCollectionSchema.extend({
|
||||
v: z.literal(12),
|
||||
preRequestScript: z.string().catch(""),
|
||||
testScript: z.string().catch(""),
|
||||
})
|
||||
|
||||
type Input = z.input<typeof v12_baseCollectionSchema> & {
|
||||
folders: Input[]
|
||||
}
|
||||
|
||||
type Output = z.output<typeof v12_baseCollectionSchema> & {
|
||||
folders: Output[]
|
||||
}
|
||||
|
||||
export const V12_SCHEMA = v12_baseCollectionSchema.extend({
|
||||
folders: z.lazy(() => z.array(entityRefUptoVersion(HoppCollection, 12))),
|
||||
}) as z.ZodType<Output, z.ZodTypeDef, Input>
|
||||
|
||||
export default defineVersion({
|
||||
initial: false,
|
||||
schema: V12_SCHEMA,
|
||||
up(old: z.infer<typeof V11_SCHEMA>) {
|
||||
const result: z.infer<typeof V12_SCHEMA> = {
|
||||
...old,
|
||||
v: 12 as const,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
folders: old.folders.map((folder) => {
|
||||
const result = HoppCollection.safeParseUpToVersion(folder, 12)
|
||||
|
||||
if (result.type !== "ok") {
|
||||
throw new Error("Failed to migrate child collections")
|
||||
}
|
||||
|
||||
return result.value
|
||||
}),
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
|
@ -53,7 +53,7 @@ export const V6_SCHEMA = V5_SCHEMA.extend({
|
|||
export default defineVersion({
|
||||
schema: V6_SCHEMA,
|
||||
initial: false,
|
||||
up(old: z.infer<typeof V6_SCHEMA>) {
|
||||
up(old: z.infer<typeof V5_SCHEMA>) {
|
||||
const headers = old.headers.map((header) => {
|
||||
return {
|
||||
...header,
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ export const V16_SCHEMA = V15_SCHEMA.extend({
|
|||
const V16_VERSION = defineVersion({
|
||||
schema: V16_SCHEMA,
|
||||
initial: false,
|
||||
up(old: z.infer<typeof V16_SCHEMA>) {
|
||||
up(old: z.infer<typeof V15_SCHEMA>) {
|
||||
return {
|
||||
...old,
|
||||
v: "16" as const,
|
||||
_ref_id: old._ref_id ?? generateUniqueRefId("req"),
|
||||
_ref_id: generateUniqueRefId("req"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ export const V17_SCHEMA = V16_SCHEMA.extend({
|
|||
const V17_VERSION = defineVersion({
|
||||
schema: V17_SCHEMA,
|
||||
initial: false,
|
||||
up(old: z.infer<typeof V17_SCHEMA>) {
|
||||
up(old: z.infer<typeof V16_SCHEMA>) {
|
||||
return {
|
||||
...old,
|
||||
v: "17" as const,
|
||||
description: old.description ?? null,
|
||||
description: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, expect, test } from "vitest"
|
||||
import { runPreRequest, runTest } from "~/utils/test-helpers"
|
||||
|
||||
/**
|
||||
* The experimental sandbox silently swallowed top-level throws inside the
|
||||
* generated `await (async function(){...})()` wrapper. The fix routes the
|
||||
* error through a lexically-captured host reporter installed by bootstrap,
|
||||
* read back on the host side as the Left branch of the TaskEither pipeline.
|
||||
*
|
||||
* These tests exercise the sandbox contract directly by reproducing the
|
||||
* generated wrapper shape that `combineScriptsWithIIFE` emits from
|
||||
* `@hoppscotch/cli` and `@hoppscotch/common`.
|
||||
*/
|
||||
describe("script execution error propagation", () => {
|
||||
const envs = { global: [], selected: [] }
|
||||
|
||||
// Mirrors `combineScriptsWithIIFE("experimental")` in the CLI and common
|
||||
// packages. Kept local to avoid a cross-package import.
|
||||
const wrapExperimental = (body: string): string =>
|
||||
[
|
||||
"const __hoppReporter = globalThis.__hoppReportScriptExecutionError;",
|
||||
"try {",
|
||||
`await (async function() {\n${body}\n})();`,
|
||||
"} catch (__hoppScriptExecutionError) {",
|
||||
" __hoppReporter(__hoppScriptExecutionError);",
|
||||
"}",
|
||||
].join("\n")
|
||||
|
||||
test("experimental pre-request: synchronous top-level throw returns Left", async () => {
|
||||
const script = wrapExperimental(
|
||||
`throw new Error("pre-request top-level throw");`
|
||||
)
|
||||
|
||||
const result = await runPreRequest(script, envs)()
|
||||
|
||||
expect(result).toBeLeft()
|
||||
if (result._tag === "Left") {
|
||||
expect(result.left).toContain("pre-request top-level throw")
|
||||
}
|
||||
})
|
||||
|
||||
test("experimental pre-request: rejected await inside IIFE returns Left", async () => {
|
||||
const script = wrapExperimental(
|
||||
`await Promise.reject(new Error("pre-request rejected await"));`
|
||||
)
|
||||
|
||||
const result = await runPreRequest(script, envs)()
|
||||
|
||||
expect(result).toBeLeft()
|
||||
if (result._tag === "Left") {
|
||||
expect(result.left).toContain("pre-request rejected await")
|
||||
}
|
||||
})
|
||||
|
||||
test("experimental pre-request: valid script still returns Right", async () => {
|
||||
const script = wrapExperimental(`pw.env.set("FLAG", "ok");`)
|
||||
|
||||
const result = await runPreRequest(script, envs)()
|
||||
|
||||
expect(result).toBeRight()
|
||||
})
|
||||
|
||||
test("experimental test-runner: synchronous top-level throw returns Left", async () => {
|
||||
const script = wrapExperimental(
|
||||
`throw new Error("test-runner top-level throw");`
|
||||
)
|
||||
|
||||
const result = await runTest(script, envs)()
|
||||
|
||||
expect(result).toBeLeft()
|
||||
if (result._tag === "Left") {
|
||||
expect(result.left).toContain("test-runner top-level throw")
|
||||
}
|
||||
})
|
||||
|
||||
test("user script cannot tamper with the reporter to suppress error reporting", async () => {
|
||||
// User script attempts to delete + overwrite the globalThis reporter
|
||||
// before throwing; both defenses (immutable property + lexical capture
|
||||
// in the wrapper) keep the report path intact.
|
||||
const script = wrapExperimental(
|
||||
`
|
||||
try {
|
||||
delete globalThis.__hoppReportScriptExecutionError;
|
||||
globalThis.__hoppReportScriptExecutionError = () => {};
|
||||
} catch (_e) {
|
||||
// strict mode may throw on immutable-property tamper; ignore
|
||||
}
|
||||
throw new Error("tamper-attempt throw");
|
||||
`
|
||||
)
|
||||
|
||||
const result = await runPreRequest(script, envs)()
|
||||
|
||||
expect(result).toBeLeft()
|
||||
if (result._tag === "Left") {
|
||||
expect(result.left).toContain("tamper-attempt throw")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -3,6 +3,27 @@
|
|||
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
|
||||
"use strict"
|
||||
|
||||
// Exposes a host reporter for the generated experimental `try/catch`
|
||||
// wrapper in combineScriptsWithIIFE. Locked down (non-writable,
|
||||
// non-configurable) so user scripts cannot delete or overwrite it to
|
||||
// suppress error reporting. faraday-cage's `runCode` creates a fresh
|
||||
// QuickJS runtime per call, so re-definition across calls is not a
|
||||
// concern — the property lives only for the current run's VM.
|
||||
Object.defineProperty(globalThis, "__hoppReportScriptExecutionError", {
|
||||
value: (error) => {
|
||||
const safe = error && typeof error === "object" ? error : {}
|
||||
inputs.setScriptExecutionError({
|
||||
name: typeof safe.name === "string" ? safe.name : "",
|
||||
message:
|
||||
typeof safe.message === "string" ? safe.message : String(error),
|
||||
stack: typeof safe.stack === "string" ? safe.stack : "",
|
||||
})
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
|
||||
// Sequential test execution promise chain
|
||||
// Initialize with a resolved promise to start the chain
|
||||
// Store on globalThis so pm.sendRequest() and test() can both access and modify it
|
||||
|
|
|
|||
|
|
@ -2,6 +2,28 @@
|
|||
;(inputs) => {
|
||||
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
|
||||
"use strict"
|
||||
|
||||
// Exposes a host reporter for the generated experimental `try/catch`
|
||||
// wrapper in combineScriptsWithIIFE. Locked down (non-writable,
|
||||
// non-configurable) so user scripts cannot delete or overwrite it to
|
||||
// suppress error reporting. faraday-cage's `runCode` creates a fresh
|
||||
// QuickJS runtime per call, so re-definition across calls is not a
|
||||
// concern — the property lives only for the current run's VM.
|
||||
Object.defineProperty(globalThis, "__hoppReportScriptExecutionError", {
|
||||
value: (error) => {
|
||||
const safe = error && typeof error === "object" ? error : {}
|
||||
inputs.setScriptExecutionError({
|
||||
name: typeof safe.name === "string" ? safe.name : "",
|
||||
message:
|
||||
typeof safe.message === "string" ? safe.message : String(error),
|
||||
stack: typeof safe.stack === "string" ? safe.stack : "",
|
||||
})
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
|
||||
globalThis.pw = {
|
||||
env: {
|
||||
get: (key) => convertMarkerToValue(inputs.envGet(key)),
|
||||
|
|
|
|||
|
|
@ -386,7 +386,11 @@ const createScriptingModule = (
|
|||
type: ModuleType,
|
||||
bootstrapCode: string,
|
||||
config: ModuleConfig,
|
||||
captureHook?: { capture?: () => void; bootstrapError?: unknown }
|
||||
captureHook?: {
|
||||
capture?: () => void
|
||||
bootstrapError?: unknown
|
||||
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||
}
|
||||
) => {
|
||||
return defineCageModule((ctx) => {
|
||||
// Track test promises for keepAlive (only for post-request scripts)
|
||||
|
|
@ -471,6 +475,31 @@ const createScriptingModule = (
|
|||
}
|
||||
}
|
||||
|
||||
// Reporter invoked from the generated `catch` block that wraps the
|
||||
// experimental IIFE chain. This is a synchronous host callback — unlike
|
||||
// rejected keepAlive promises or async afterScriptExecutionHooks, these
|
||||
// calls cross the QuickJS boundary before the script returns, so the
|
||||
// host sees the error without relying on faraday-cage's loop behaviour.
|
||||
;(inputsObj as Record<string, unknown>).setScriptExecutionError =
|
||||
defineSandboxFn(
|
||||
ctx,
|
||||
"setScriptExecutionError",
|
||||
(error: unknown) => {
|
||||
if (!captureHook || captureHook.scriptExecutionError) return
|
||||
const err = (error ?? {}) as {
|
||||
name?: unknown
|
||||
message?: unknown
|
||||
stack?: unknown
|
||||
}
|
||||
captureHook.scriptExecutionError = {
|
||||
name: typeof err.name === "string" ? err.name : "",
|
||||
message:
|
||||
typeof err.message === "string" ? err.message : String(error),
|
||||
stack: typeof err.stack === "string" ? err.stack : "",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const sandboxInputsObj = defineSandboxObject(ctx, inputsObj)
|
||||
|
||||
const bootstrapResult = ctx.vm.callFunction(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ const executePreRequestOnCage = async (
|
|||
let finalRequest = request
|
||||
let finalCookies = cookies
|
||||
|
||||
const captureHook: { capture?: () => void; bootstrapError?: unknown } = {}
|
||||
const captureHook: {
|
||||
capture?: () => void
|
||||
bootstrapError?: unknown
|
||||
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||
} = {}
|
||||
|
||||
const result = await cage.runCode(preRequestScript, [
|
||||
...defaultModules({
|
||||
|
|
@ -72,6 +76,16 @@ const executePreRequestOnCage = async (
|
|||
return E.left(`Script execution failed: ${String(result.err)}`)
|
||||
}
|
||||
|
||||
// Check for errors reported via the generated try/catch wrapper.
|
||||
// faraday-cage's keepAlive loop swallows rejected promises and does not
|
||||
// await afterScriptExecutionHooks, so async-boundary errors reach us
|
||||
// only via this synchronous host reporter.
|
||||
if (captureHook.scriptExecutionError) {
|
||||
const { name, message } = captureHook.scriptExecutionError
|
||||
const prefix = name ? `${name}: ` : ""
|
||||
return E.left(`Script execution failed: ${prefix}${message}`)
|
||||
}
|
||||
|
||||
if (captureHook.capture) {
|
||||
captureHook.capture()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ const executeTestOnCage = async (
|
|||
let finalTestResults = testRunStack
|
||||
const testPromises: Promise<void>[] = []
|
||||
|
||||
const captureHook: { capture?: () => void; bootstrapError?: unknown } = {}
|
||||
const captureHook: {
|
||||
capture?: () => void
|
||||
bootstrapError?: unknown
|
||||
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||
} = {}
|
||||
|
||||
const result = await cage.runCode(testScript, [
|
||||
...defaultModules({
|
||||
|
|
@ -92,6 +96,16 @@ const executeTestOnCage = async (
|
|||
}
|
||||
}
|
||||
|
||||
// Check for errors reported via the generated try/catch wrapper.
|
||||
// faraday-cage's keepAlive loop swallows rejected promises and does not
|
||||
// await afterScriptExecutionHooks, so async-boundary errors reach us
|
||||
// only via this synchronous host reporter.
|
||||
if (captureHook.scriptExecutionError) {
|
||||
const { name, message } = captureHook.scriptExecutionError
|
||||
const prefix = name ? `${name}: ` : ""
|
||||
return E.left(`Script execution failed: ${prefix}${message}`)
|
||||
}
|
||||
|
||||
if (captureHook.capture) {
|
||||
captureHook.capture()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ const executePreRequestOnCage = async (
|
|||
let finalRequest = request
|
||||
let finalCookies = cookies
|
||||
|
||||
const captureHook: { capture?: () => void; bootstrapError?: unknown } = {}
|
||||
const captureHook: {
|
||||
capture?: () => void
|
||||
bootstrapError?: unknown
|
||||
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||
} = {}
|
||||
|
||||
const result = await cage.runCode(preRequestScript, [
|
||||
...defaultModules({
|
||||
|
|
@ -103,6 +107,16 @@ const executePreRequestOnCage = async (
|
|||
return E.left(`Script execution failed: ${String(result.err)}`)
|
||||
}
|
||||
|
||||
// Check for errors reported via the generated try/catch wrapper.
|
||||
// faraday-cage's keepAlive loop swallows rejected promises and does not
|
||||
// await afterScriptExecutionHooks, so async-boundary errors reach us
|
||||
// only via this synchronous host reporter.
|
||||
if (captureHook.scriptExecutionError) {
|
||||
const { name, message } = captureHook.scriptExecutionError
|
||||
const prefix = name ? `${name}: ` : ""
|
||||
return E.left(`Script execution failed: ${prefix}${message}`)
|
||||
}
|
||||
|
||||
if (captureHook.capture) {
|
||||
captureHook.capture()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,11 @@ const executeTestOnCage = async (
|
|||
let finalCookies = cookies
|
||||
const testPromises: Promise<void>[] = []
|
||||
|
||||
const captureHook: { capture?: () => void; bootstrapError?: unknown } = {}
|
||||
const captureHook: {
|
||||
capture?: () => void
|
||||
bootstrapError?: unknown
|
||||
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||
} = {}
|
||||
|
||||
const result = await cage.runCode(testScript, [
|
||||
...defaultModules({
|
||||
|
|
@ -122,6 +126,16 @@ const executeTestOnCage = async (
|
|||
await Promise.all(testPromises)
|
||||
}
|
||||
|
||||
// Check for errors reported via the generated try/catch wrapper.
|
||||
// faraday-cage's keepAlive loop swallows rejected promises and does not
|
||||
// await afterScriptExecutionHooks, so async-boundary errors reach us
|
||||
// only via this synchronous host reporter.
|
||||
if (captureHook.scriptExecutionError) {
|
||||
const { name, message } = captureHook.scriptExecutionError
|
||||
const prefix = name ? `${name}: ` : ""
|
||||
return E.left(`Script execution failed: ${prefix}${message}`)
|
||||
}
|
||||
|
||||
if (captureHook.capture) {
|
||||
captureHook.capture()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -79,6 +82,9 @@ const recursivelySyncCollections = async (
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
const res = await createGQLRootUserCollection(
|
||||
collection.name,
|
||||
|
|
@ -98,6 +104,9 @@ const recursivelySyncCollections = async (
|
|||
headers: [],
|
||||
variables: [],
|
||||
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = parentCollectionID
|
||||
|
|
@ -105,6 +114,9 @@ const recursivelySyncCollections = async (
|
|||
collection.headers = returnedData.headers
|
||||
collection.variables = returnedData.variables
|
||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateGraphqlCollectionOrFolder(
|
||||
parentCollectionID,
|
||||
|
|
@ -124,6 +136,9 @@ const recursivelySyncCollections = async (
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
const res = await createGQLChildUserCollection(
|
||||
|
|
@ -145,6 +160,9 @@ const recursivelySyncCollections = async (
|
|||
headers: [],
|
||||
variables: [],
|
||||
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = childCollectionId
|
||||
|
|
@ -153,6 +171,9 @@ const recursivelySyncCollections = async (
|
|||
parentCollectionID = childCollectionId
|
||||
collection.variables = returnedData.variables
|
||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateGraphqlCollectionOrFolder(
|
||||
childCollectionId,
|
||||
|
|
@ -268,6 +289,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
headers: collection.headers,
|
||||
variables: collection.variables,
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
if (collectionID) {
|
||||
|
|
@ -314,6 +338,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
headers: folder.headers,
|
||||
variables: folder.variables,
|
||||
_ref_id: folder._ref_id,
|
||||
description: folder.description ?? null,
|
||||
preRequestScript: folder.preRequestScript ?? "",
|
||||
testScript: folder.testScript ?? "",
|
||||
}
|
||||
|
||||
if (folderBackendId) {
|
||||
|
|
|
|||
|
|
@ -141,12 +141,14 @@ export function exportedCollectionToHoppCollection(
|
|||
_ref_id: generateUniqueRefId("coll"),
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
return {
|
||||
id: restCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 11,
|
||||
v: 12,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -200,6 +202,8 @@ export function exportedCollectionToHoppCollection(
|
|||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
}
|
||||
} else {
|
||||
const gqlCollection = collection as ExportedUserCollectionGQL
|
||||
|
|
@ -213,12 +217,14 @@ export function exportedCollectionToHoppCollection(
|
|||
_ref_id: generateUniqueRefId("coll"),
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
return {
|
||||
id: gqlCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 11,
|
||||
v: 12,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -248,6 +254,8 @@ export function exportedCollectionToHoppCollection(
|
|||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -398,6 +406,9 @@ function setupUserCollectionCreatedSubscription() {
|
|||
headers: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
runDispatchWithOutSyncing(() => {
|
||||
|
|
@ -406,23 +417,27 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
})
|
||||
: addRESTCollection({
|
||||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
})
|
||||
|
||||
const localIndex = collectionStore.value.state.length - 1
|
||||
|
|
@ -625,7 +640,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
)
|
||||
|
||||
// Incoming data transformed to the respective internal representations
|
||||
const { auth, headers, variables, description } =
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} =
|
||||
data && data != "null"
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
|
|
@ -633,6 +655,8 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
// Duplicated collection will have a unique ref id
|
||||
const _ref_id = generateUniqueRefId("coll")
|
||||
|
|
@ -649,12 +673,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
variables: variables ?? [],
|
||||
description: description ?? null,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
}
|
||||
|
||||
// only folders will have parent collection id
|
||||
|
|
@ -1122,7 +1148,14 @@ function transformDuplicatedCollections(
|
|||
requests: userRequests,
|
||||
title: name,
|
||||
}) => {
|
||||
const { auth, headers, variables, description } =
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} =
|
||||
data && data !== "null"
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
|
|
@ -1130,6 +1163,8 @@ function transformDuplicatedCollections(
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
const _ref_id = generateUniqueRefId("coll")
|
||||
|
|
@ -1144,11 +1179,13 @@ function transformDuplicatedCollections(
|
|||
folders,
|
||||
requests,
|
||||
_ref_id,
|
||||
v: 11,
|
||||
v: 12,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
variables: variables ?? [],
|
||||
description: description ?? null,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -83,6 +85,8 @@ const recursivelySyncCollections = async (
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
const res = await createRESTRootUserCollection(
|
||||
collection.name,
|
||||
|
|
@ -102,6 +106,8 @@ const recursivelySyncCollections = async (
|
|||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = parentCollectionID
|
||||
|
|
@ -110,6 +116,9 @@ const recursivelySyncCollections = async (
|
|||
collection.headers = returnedData.headers
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
|
||||
} else {
|
||||
parentCollectionID = undefined
|
||||
|
|
@ -125,6 +134,8 @@ const recursivelySyncCollections = async (
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
const res = await createRESTChildUserCollection(
|
||||
|
|
@ -147,6 +158,8 @@ const recursivelySyncCollections = async (
|
|||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = childCollectionId
|
||||
|
|
@ -156,6 +169,8 @@ const recursivelySyncCollections = async (
|
|||
parentCollectionID = childCollectionId
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateRESTCollectionOrFolder(
|
||||
childCollectionId,
|
||||
|
|
@ -268,6 +283,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
variables: collection.variables,
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
if (collectionID) {
|
||||
|
|
@ -351,6 +368,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
variables: folder.variables,
|
||||
_ref_id: folder._ref_id,
|
||||
description: folder.description ?? null,
|
||||
preRequestScript: folder.preRequestScript ?? "",
|
||||
testScript: folder.testScript ?? "",
|
||||
}
|
||||
if (folderID) {
|
||||
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -79,6 +82,9 @@ const recursivelySyncCollections = async (
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
const res = await createGQLRootUserCollection(
|
||||
collection.name,
|
||||
|
|
@ -98,6 +104,9 @@ const recursivelySyncCollections = async (
|
|||
headers: [],
|
||||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = parentCollectionID
|
||||
|
|
@ -105,6 +114,9 @@ const recursivelySyncCollections = async (
|
|||
collection.auth = returnedData.auth
|
||||
collection.headers = returnedData.headers
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateGraphqlCollectionOrFolder(
|
||||
parentCollectionID,
|
||||
|
|
@ -124,6 +136,9 @@ const recursivelySyncCollections = async (
|
|||
headers: collection.headers ?? [],
|
||||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
const res = await createGQLChildUserCollection(
|
||||
|
|
@ -145,6 +160,9 @@ const recursivelySyncCollections = async (
|
|||
headers: [],
|
||||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = childCollectionId
|
||||
|
|
@ -153,6 +171,9 @@ const recursivelySyncCollections = async (
|
|||
collection.headers = returnedData.headers
|
||||
parentCollectionID = childCollectionId
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateGraphqlCollectionOrFolder(
|
||||
childCollectionId,
|
||||
|
|
@ -268,6 +289,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
headers: collection.headers,
|
||||
variables: collection.variables,
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
if (collectionID) {
|
||||
|
|
@ -314,6 +338,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
headers: folder.headers,
|
||||
variables: folder.variables,
|
||||
_ref_id: folder._ref_id,
|
||||
description: folder.description ?? null,
|
||||
preRequestScript: folder.preRequestScript ?? "",
|
||||
testScript: folder.testScript ?? "",
|
||||
}
|
||||
|
||||
if (folderBackendId) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export function translateToPersonalCollectionFormat(x: HoppCollection) {
|
|||
headers: x.headers,
|
||||
variables: x.variables,
|
||||
description: x.description,
|
||||
preRequestScript: x.preRequestScript ?? "",
|
||||
testScript: x.testScript ?? "",
|
||||
}
|
||||
|
||||
const obj = {
|
||||
|
|
|
|||
|
|
@ -142,12 +142,14 @@ export function exportedCollectionToHoppCollection(
|
|||
_ref_id: generateUniqueRefId("coll"),
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
return {
|
||||
id: restCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 11,
|
||||
v: 12,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -201,6 +203,8 @@ export function exportedCollectionToHoppCollection(
|
|||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
}
|
||||
} else {
|
||||
const gqlCollection = collection as ExportedUserCollectionGQL
|
||||
|
|
@ -219,7 +223,7 @@ export function exportedCollectionToHoppCollection(
|
|||
return {
|
||||
id: gqlCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 11,
|
||||
v: 12,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -249,6 +253,8 @@ export function exportedCollectionToHoppCollection(
|
|||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -400,6 +406,8 @@ function setupUserCollectionCreatedSubscription() {
|
|||
_ref_id: generateUniqueRefId("coll"),
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
runDispatchWithOutSyncing(() => {
|
||||
|
|
@ -408,23 +416,27 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
})
|
||||
: addRESTCollection({
|
||||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
variables: data.variables ?? [],
|
||||
description: data.description ?? null,
|
||||
preRequestScript: data.preRequestScript ?? "",
|
||||
testScript: data.testScript ?? "",
|
||||
})
|
||||
|
||||
const localIndex = collectionStore.value.state.length - 1
|
||||
|
|
@ -627,7 +639,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
)
|
||||
|
||||
// Incoming data transformed to the respective internal representations
|
||||
const { auth, headers, variables, description } =
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} =
|
||||
data && data != "null"
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
|
|
@ -635,6 +654,8 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
// Duplicated collection will have a unique ref id
|
||||
const _ref_id = generateUniqueRefId("coll")
|
||||
|
|
@ -651,12 +672,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 11,
|
||||
v: 12,
|
||||
_ref_id,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
variables: variables ?? [],
|
||||
description: description ?? null,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
}
|
||||
|
||||
// only folders will have parent collection id
|
||||
|
|
@ -1122,7 +1145,14 @@ function transformDuplicatedCollections(
|
|||
requests: userRequests,
|
||||
title: name,
|
||||
}) => {
|
||||
const { auth, headers, variables, description } =
|
||||
const {
|
||||
auth,
|
||||
headers,
|
||||
variables,
|
||||
description,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
} =
|
||||
data && data !== "null"
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
|
|
@ -1130,6 +1160,8 @@ function transformDuplicatedCollections(
|
|||
headers: [],
|
||||
variables: [],
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
const _ref_id = generateUniqueRefId("coll")
|
||||
|
|
@ -1144,11 +1176,13 @@ function transformDuplicatedCollections(
|
|||
folders,
|
||||
requests,
|
||||
_ref_id,
|
||||
v: 11,
|
||||
v: 12,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
variables: variables ?? [],
|
||||
description: description ?? null,
|
||||
preRequestScript: preRequestScript ?? "",
|
||||
testScript: testScript ?? "",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -83,6 +85,8 @@ const recursivelySyncCollections = async (
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
const res = await createRESTRootUserCollection(
|
||||
collection.name,
|
||||
|
|
@ -102,6 +106,8 @@ const recursivelySyncCollections = async (
|
|||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = parentCollectionID
|
||||
|
|
@ -110,6 +116,8 @@ const recursivelySyncCollections = async (
|
|||
collection.headers = returnedData.headers
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
removeDuplicateRESTCollectionOrFolder(
|
||||
parentCollectionID,
|
||||
`${collectionPath}`
|
||||
|
|
@ -128,6 +136,8 @@ const recursivelySyncCollections = async (
|
|||
variables: collection.variables ?? [],
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
const res = await createRESTChildUserCollection(
|
||||
|
|
@ -150,6 +160,8 @@ const recursivelySyncCollections = async (
|
|||
variables: [],
|
||||
_ref_id: generateUniqueRefId("coll"),
|
||||
description: null,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
}
|
||||
|
||||
collection.id = childCollectionId
|
||||
|
|
@ -159,6 +171,8 @@ const recursivelySyncCollections = async (
|
|||
parentCollectionID = childCollectionId
|
||||
collection.variables = returnedData.variables
|
||||
collection.description = returnedData.description ?? null
|
||||
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||
collection.testScript = returnedData.testScript ?? ""
|
||||
|
||||
removeDuplicateRESTCollectionOrFolder(
|
||||
childCollectionId,
|
||||
|
|
@ -271,6 +285,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
variables: collection.variables,
|
||||
_ref_id: collection._ref_id,
|
||||
description: collection.description ?? null,
|
||||
preRequestScript: collection.preRequestScript ?? "",
|
||||
testScript: collection.testScript ?? "",
|
||||
}
|
||||
|
||||
if (collectionID) {
|
||||
|
|
@ -355,6 +371,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
variables: folder.variables,
|
||||
_ref_id: folder._ref_id,
|
||||
description: folder.description,
|
||||
preRequestScript: folder.preRequestScript ?? "",
|
||||
testScript: folder.testScript ?? "",
|
||||
}
|
||||
if (folderID) {
|
||||
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
||||
|
|
|
|||
Loading…
Reference in a new issue