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',
|
DATABASE_UNREACHABLE = 'P1001',
|
||||||
TABLE_DOES_NOT_EXIST = 'P2021',
|
TABLE_DOES_NOT_EXIST = 'P2021',
|
||||||
UNIQUE_CONSTRAINT_VIOLATION = 'P2002',
|
UNIQUE_CONSTRAINT_VIOLATION = 'P2002',
|
||||||
|
RECORD_NOT_FOUND = 'P2025',
|
||||||
TRANSACTION_TIMEOUT = 'P2028',
|
TRANSACTION_TIMEOUT = 'P2028',
|
||||||
TRANSACTION_DEADLOCK = 'P2034', // write conflict or a deadlock
|
TRANSACTION_DEADLOCK = 'P2034', // write conflict or a deadlock
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -569,23 +569,24 @@ export class TeamCollectionService {
|
||||||
collection.parentID,
|
collection.parentID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deletedCollection = await tx.teamCollection.delete({
|
try {
|
||||||
where: { id: collection.id },
|
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 },
|
|
||||||
});
|
});
|
||||||
|
} 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) {
|
} catch (error) {
|
||||||
throw new ConflictException(error);
|
throw new ConflictException(error);
|
||||||
}
|
}
|
||||||
|
|
@ -749,12 +750,7 @@ export class TeamCollectionService {
|
||||||
// Get collection details of collectionID
|
// Get collection details of collectionID
|
||||||
const collection = await this.getCollection(collectionID, tx);
|
const collection = await this.getCollection(collectionID, tx);
|
||||||
if (E.isLeft(collection)) return E.left(collection.left);
|
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
|
// destCollectionID == null i.e move collection to root
|
||||||
if (!destCollectionID) {
|
if (!destCollectionID) {
|
||||||
if (!collection.right.parentID) {
|
if (!collection.right.parentID) {
|
||||||
|
|
@ -763,6 +759,12 @@ export class TeamCollectionService {
|
||||||
return E.left(TEAM_COL_ALREADY_ROOT);
|
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
|
// 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
|
// Move child collection into root and update orderIndexes for root teamCollections
|
||||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||||
|
|
@ -806,12 +808,41 @@ export class TeamCollectionService {
|
||||||
return E.left(TEAM_COLL_IS_PARENT_COLL);
|
return E.left(TEAM_COLL_IS_PARENT_COLL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lock the rows of the destination collection and its siblings
|
// Acquire locks in deterministic order (sorted by parentID) to prevent deadlocks
|
||||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
// when two concurrent moves happen in opposite directions
|
||||||
tx,
|
const srcParentID = collection.right.parentID ?? '';
|
||||||
destCollection.right.teamID,
|
const destParentID = destCollection.right.parentID ?? '';
|
||||||
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
|
// 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
|
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
TeamRequest as DbTeamRequest,
|
TeamRequest as DbTeamRequest,
|
||||||
} from 'src/generated/prisma/client';
|
} from 'src/generated/prisma/client';
|
||||||
import { SortOptions } from 'src/types/SortOptions';
|
import { SortOptions } from 'src/types/SortOptions';
|
||||||
|
import { PrismaError } from 'src/prisma/prisma-error-codes';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamRequestService {
|
export class TeamRequestService {
|
||||||
|
|
@ -124,21 +125,23 @@ export class TeamRequestService {
|
||||||
dbTeamReq.collectionID,
|
dbTeamReq.collectionID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const deletedTeamRequest = await tx.teamRequest.delete({
|
try {
|
||||||
where: { id: requestID },
|
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 } },
|
|
||||||
});
|
});
|
||||||
|
} 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) {
|
} catch (error) {
|
||||||
throw new ConflictException(error);
|
throw new ConflictException(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2380,3 +2380,165 @@ describe('exportUserCollectionToJSONObject', () => {
|
||||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
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,
|
collection.parentID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deletedCollection = await tx.userCollection.delete({
|
try {
|
||||||
where: { id: collection.id },
|
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 },
|
|
||||||
});
|
});
|
||||||
|
} 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) {
|
} catch (error) {
|
||||||
throw new ConflictException(error);
|
throw new ConflictException(error);
|
||||||
}
|
}
|
||||||
|
|
@ -596,6 +597,12 @@ export class UserCollectionService {
|
||||||
return E.left(USER_COLL_ALREADY_ROOT);
|
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
|
// 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
|
// Move child collection into root and update orderIndexes for child userCollections
|
||||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||||
|
|
@ -643,7 +650,40 @@ export class UserCollectionService {
|
||||||
return E.left(USER_COLL_IS_PARENT_COLL);
|
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
|
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||||
tx,
|
tx,
|
||||||
|
|
|
||||||
|
|
@ -539,6 +539,21 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
|
||||||
// Clean up
|
// Clean up
|
||||||
fs.unlinkSync(junitPath);
|
fs.unlinkSync(junitPath);
|
||||||
}, 600000); // 600 second (10 minute) timeout
|
}, 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:", () => {
|
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: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -192,6 +194,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -224,6 +228,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -273,6 +279,8 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_M
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
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",
|
name: "Multiple child collections with authorization, headers and variables set at each level",
|
||||||
folders: [
|
folders: [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fjgah000110f8a5bs68gd",
|
id: "clx1fjgah000110f8a5bs68gd",
|
||||||
name: "folder-1",
|
name: "folder-1",
|
||||||
folders: [
|
folders: [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fjwmm000410f8l1gkkr1a",
|
id: "clx1fjwmm000410f8l1gkkr1a",
|
||||||
name: "folder-11",
|
name: "folder-11",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -567,9 +575,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fjyxm000510f8pv90dt43",
|
id: "clx1fjyxm000510f8pv90dt43",
|
||||||
name: "folder-12",
|
name: "folder-12",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -634,9 +644,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fk1cv000610f88kc3aupy",
|
id: "clx1fk1cv000610f88kc3aupy",
|
||||||
name: "folder-13",
|
name: "folder-13",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -719,6 +731,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -764,14 +778,16 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fjk9o000210f8j0573pls",
|
id: "clx1fjk9o000210f8j0573pls",
|
||||||
name: "folder-2",
|
name: "folder-2",
|
||||||
folders: [
|
folders: [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fk516000710f87sfpw6bo",
|
id: "clx1fk516000710f87sfpw6bo",
|
||||||
name: "folder-21",
|
name: "folder-21",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -818,9 +834,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fk72t000810f8gfwkpi5y",
|
id: "clx1fk72t000810f8gfwkpi5y",
|
||||||
name: "folder-22",
|
name: "folder-22",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -885,9 +903,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fk95g000910f8bunhaoo8",
|
id: "clx1fk95g000910f8bunhaoo8",
|
||||||
name: "folder-23",
|
name: "folder-23",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -957,6 +977,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -1008,15 +1030,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1fjmlq000310f86o4d3w2o",
|
id: "clx1fjmlq000310f86o4d3w2o",
|
||||||
name: "folder-3",
|
name: "folder-3",
|
||||||
folders: [
|
folders: [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1iwq0p003e10f8u8zg0p85",
|
id: "clx1iwq0p003e10f8u8zg0p85",
|
||||||
name: "folder-31",
|
name: "folder-31",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -1063,9 +1087,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1izut7003m10f894ip59zg",
|
id: "clx1izut7003m10f894ip59zg",
|
||||||
name: "folder-32",
|
name: "folder-32",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -1130,9 +1156,11 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
id: "clx1j2ka9003q10f8cdbzpgpg",
|
id: "clx1j2ka9003q10f8cdbzpgpg",
|
||||||
name: "folder-33",
|
name: "folder-33",
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -1202,6 +1230,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -1266,6 +1296,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [
|
requests: [
|
||||||
|
|
@ -1321,6 +1353,8 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -1428,6 +1462,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: CollectionSchemaVersion,
|
v: CollectionSchemaVersion,
|
||||||
|
|
@ -1476,6 +1512,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: CollectionSchemaVersion,
|
v: CollectionSchemaVersion,
|
||||||
|
|
@ -1490,6 +1528,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: CollectionSchemaVersion,
|
v: CollectionSchemaVersion,
|
||||||
|
|
@ -1518,6 +1558,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requests: [],
|
requests: [],
|
||||||
|
|
@ -1528,6 +1570,8 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_L
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
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 {TestResponse} response Response structure for test script runner.
|
||||||
* @property {HoppEnvs} envs Environment variables for test script runner.
|
* @property {HoppEnvs} envs Environment variables for test script runner.
|
||||||
* @property {boolean} legacySandbox Whether to use the legacy sandbox.
|
* @property {boolean} legacySandbox Whether to use the legacy sandbox.
|
||||||
|
* @property {string[]} inheritedTestScripts Test scripts inherited from parent collections.
|
||||||
*/
|
*/
|
||||||
export interface TestScriptParams {
|
export interface TestScriptParams {
|
||||||
request: HoppRESTRequest;
|
request: HoppRESTRequest;
|
||||||
response: TestResponse;
|
response: TestResponse;
|
||||||
envs: HoppEnvs;
|
envs: HoppEnvs;
|
||||||
legacySandbox: boolean;
|
legacySandbox: boolean;
|
||||||
|
inheritedTestScripts?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,6 @@ export type ProcessRequestParams = {
|
||||||
delay: number;
|
delay: number;
|
||||||
legacySandbox?: boolean;
|
legacySandbox?: boolean;
|
||||||
collectionVariables?: HoppCollectionVariable[];
|
collectionVariables?: HoppCollectionVariable[];
|
||||||
|
inheritedPreRequestScripts?: string[];
|
||||||
|
inheritedTestScripts?: string[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
processRequest,
|
processRequest,
|
||||||
} from "./request";
|
} from "./request";
|
||||||
import { getTestMetrics } from "./test";
|
import { getTestMetrics } from "./test";
|
||||||
|
import { filterValidScripts } from "./scripting";
|
||||||
|
|
||||||
const { WARN, FAIL, INFO } = exceptionColors;
|
const { WARN, FAIL, INFO } = exceptionColors;
|
||||||
|
|
||||||
|
|
@ -109,8 +110,21 @@ const processCollection = async (
|
||||||
envs: HoppEnvs,
|
envs: HoppEnvs,
|
||||||
delay: number,
|
delay: number,
|
||||||
requestsReport: RequestReport[],
|
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
|
// Process each request in the collection
|
||||||
for (const request of collection.requests) {
|
for (const request of collection.requests) {
|
||||||
const _request = preProcessRequest(request as HoppRESTRequest, collection);
|
const _request = preProcessRequest(request as HoppRESTRequest, collection);
|
||||||
|
|
@ -127,6 +141,8 @@ const processCollection = async (
|
||||||
delay,
|
delay,
|
||||||
legacySandbox,
|
legacySandbox,
|
||||||
collectionVariables,
|
collectionVariables,
|
||||||
|
inheritedPreRequestScripts,
|
||||||
|
inheritedTestScripts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Request processing initiated message.
|
// Request processing initiated message.
|
||||||
|
|
@ -188,7 +204,9 @@ const processCollection = async (
|
||||||
envs,
|
envs,
|
||||||
delay,
|
delay,
|
||||||
requestsReport,
|
requestsReport,
|
||||||
legacySandbox
|
legacySandbox,
|
||||||
|
inheritedPreRequestScripts,
|
||||||
|
inheritedTestScripts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import { FormDataEntry } from "../types/request";
|
||||||
import { isHoppErrnoException } from "./checks";
|
import { isHoppErrnoException } from "./checks";
|
||||||
import { getResourceContents } from "./getters";
|
import { getResourceContents } from "./getters";
|
||||||
|
|
||||||
|
// Re-export from the canonical implementation in scripting.ts
|
||||||
|
export { stripModulePrefix } from "./scripting";
|
||||||
|
|
||||||
const getValidRequests = (
|
const getValidRequests = (
|
||||||
collections: HoppCollection[],
|
collections: HoppCollection[],
|
||||||
collectionFilePath: string
|
collectionFilePath: string
|
||||||
|
|
@ -158,19 +161,3 @@ export async function parseCollectionData(
|
||||||
|
|
||||||
return getValidRequests(collectionSchemaParsedResult.data, pathOrId);
|
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 { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||||
import { getEffectiveFinalMetaData, getResolvedVariables } from "./getters";
|
import { getEffectiveFinalMetaData, getResolvedVariables } from "./getters";
|
||||||
import { stripComments } from "./jsonc";
|
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
|
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||||
* applies them on current request to generate updated request.
|
* applies them on current request to generate updated request.
|
||||||
* @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest.
|
* @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest.
|
||||||
* @param envs Environment variables related to request.
|
* @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
|
* @returns EffectiveHoppRESTRequest that includes parsed ENV variables with in
|
||||||
* request OR HoppCLIError with error code and related information.
|
* request OR HoppCLIError with error code and related information.
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,7 +53,8 @@ export const preRequestScriptRunner = (
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
envs: HoppEnvs,
|
envs: HoppEnvs,
|
||||||
legacySandbox: boolean,
|
legacySandbox: boolean,
|
||||||
collectionVariables?: HoppCollectionVariable[]
|
collectionVariables?: HoppCollectionVariable[],
|
||||||
|
inheritedPreRequestScripts: string[] = []
|
||||||
): TE.TaskEither<
|
): TE.TaskEither<
|
||||||
HoppCLIError,
|
HoppCLIError,
|
||||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||||
|
|
@ -57,10 +62,19 @@ export const preRequestScriptRunner = (
|
||||||
const experimentalScriptingSandbox = !legacySandbox;
|
const experimentalScriptingSandbox = !legacySandbox;
|
||||||
const hoppFetchHook = createHoppFetchHook();
|
const hoppFetchHook = createHoppFetchHook();
|
||||||
|
|
||||||
|
// Pre-request order: root → request.
|
||||||
|
const combinedScript = combineScriptsWithIIFE(
|
||||||
|
filterValidScripts([
|
||||||
|
...inheritedPreRequestScripts,
|
||||||
|
request.preRequestScript,
|
||||||
|
]),
|
||||||
|
experimentalScriptingSandbox ? "experimental" : "legacy"
|
||||||
|
);
|
||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
TE.of(request),
|
TE.of(request),
|
||||||
TE.chain(({ preRequestScript }) =>
|
TE.chain(() =>
|
||||||
runPreRequestScript(stripModulePrefix(preRequestScript), {
|
runPreRequestScript(combinedScript, {
|
||||||
envs,
|
envs,
|
||||||
experimentalScriptingSandbox,
|
experimentalScriptingSandbox,
|
||||||
request,
|
request,
|
||||||
|
|
|
||||||
|
|
@ -232,8 +232,16 @@ export const processRequest =
|
||||||
params: ProcessRequestParams
|
params: ProcessRequestParams
|
||||||
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
|
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
|
||||||
async () => {
|
async () => {
|
||||||
const { envs, path, request, delay, legacySandbox, collectionVariables } =
|
const {
|
||||||
params;
|
envs,
|
||||||
|
path,
|
||||||
|
request,
|
||||||
|
delay,
|
||||||
|
legacySandbox,
|
||||||
|
collectionVariables,
|
||||||
|
inheritedPreRequestScripts = [],
|
||||||
|
inheritedTestScripts = [],
|
||||||
|
} = params;
|
||||||
|
|
||||||
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
|
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -258,17 +266,21 @@ export const processRequest =
|
||||||
effectiveFinalParams: [],
|
effectiveFinalParams: [],
|
||||||
effectiveFinalURL: "",
|
effectiveFinalURL: "",
|
||||||
};
|
};
|
||||||
let updatedEnvs = <HoppEnvs>{};
|
|
||||||
|
|
||||||
// Fetch values for secret environment variables from system environment
|
// Fetch values for secret environment variables from system environment
|
||||||
const processedEnvs = processEnvs(envs);
|
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(
|
const preRequestRes = await preRequestScriptRunner(
|
||||||
request,
|
request,
|
||||||
processedEnvs,
|
processedEnvs,
|
||||||
legacySandbox ?? false,
|
legacySandbox ?? false,
|
||||||
collectionVariables
|
collectionVariables,
|
||||||
|
inheritedPreRequestScripts
|
||||||
)();
|
)();
|
||||||
if (E.isLeft(preRequestRes)) {
|
if (E.isLeft(preRequestRes)) {
|
||||||
printPreRequestRunner.fail();
|
printPreRequestRunner.fail();
|
||||||
|
|
@ -317,12 +329,12 @@ export const processRequest =
|
||||||
printRequestRunner.success(_requestRunnerRes);
|
printRequestRunner.success(_requestRunnerRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracting test-script-runner parameters.
|
|
||||||
const testScriptParams = getTestScriptParams(
|
const testScriptParams = getTestScriptParams(
|
||||||
_requestRunnerRes,
|
_requestRunnerRes,
|
||||||
effectiveRequest,
|
effectiveRequest,
|
||||||
updatedEnvs,
|
updatedEnvs,
|
||||||
legacySandbox ?? false
|
legacySandbox ?? false,
|
||||||
|
inheritedTestScripts
|
||||||
);
|
);
|
||||||
|
|
||||||
// Executing test-runner.
|
// 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 { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||||
import { getDurationInSeconds } from "./getters";
|
import { getDurationInSeconds } from "./getters";
|
||||||
import { createHoppFetchHook } from "./hopp-fetch";
|
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
|
* Executes test script and runs testDescriptorParser to generate test-report using
|
||||||
|
|
@ -39,28 +39,46 @@ export const testRunner = (
|
||||||
TE.bind("test_response", () =>
|
TE.bind("test_response", () =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.of(testScriptData),
|
TE.of(testScriptData),
|
||||||
TE.chain(({ request, response, envs, legacySandbox }) => {
|
TE.chain(
|
||||||
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,
|
|
||||||
request,
|
request,
|
||||||
response: effectiveResponse,
|
response,
|
||||||
experimentalScriptingSandbox,
|
envs,
|
||||||
hoppFetchHook,
|
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,
|
reqRunnerRes: RequestRunnerResponse,
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
envs: HoppEnvs,
|
envs: HoppEnvs,
|
||||||
legacySandbox: boolean
|
legacySandbox: boolean,
|
||||||
|
inheritedTestScripts: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const testScriptParams: TestScriptParams = {
|
const testScriptParams: TestScriptParams = {
|
||||||
request,
|
request,
|
||||||
|
|
@ -173,6 +192,7 @@ export const getTestScriptParams = (
|
||||||
},
|
},
|
||||||
envs,
|
envs,
|
||||||
legacySandbox,
|
legacySandbox,
|
||||||
|
inheritedTestScripts,
|
||||||
};
|
};
|
||||||
return testScriptParams;
|
return testScriptParams;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,8 @@ export const transformWorkspaceCollections = (
|
||||||
headers?: HoppRESTHeaders;
|
headers?: HoppRESTHeaders;
|
||||||
variables: HoppCollectionVariable[];
|
variables: HoppCollectionVariable[];
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
preRequestScript?: string;
|
||||||
|
testScript?: string;
|
||||||
} = data ? JSON.parse(data) : {};
|
} = data ? JSON.parse(data) : {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -186,6 +188,8 @@ export const transformWorkspaceCollections = (
|
||||||
headers = [],
|
headers = [],
|
||||||
variables = [],
|
variables = [],
|
||||||
description = null,
|
description = null,
|
||||||
|
preRequestScript = "",
|
||||||
|
testScript = "",
|
||||||
} = parsedData;
|
} = parsedData;
|
||||||
|
|
||||||
const transformedAuth = transformAuth(auth);
|
const transformedAuth = transformAuth(auth);
|
||||||
|
|
@ -211,6 +215,8 @@ export const transformWorkspaceCollections = (
|
||||||
headers: transformedHeaders,
|
headers: transformedHeaders,
|
||||||
variables: filteredCollectionVariables,
|
variables: filteredCollectionVariables,
|
||||||
description,
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -841,6 +841,7 @@
|
||||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
"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_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_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",
|
"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.",
|
"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.",
|
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
||||||
|
|
@ -1256,6 +1257,12 @@
|
||||||
"saved": "Response saved",
|
"saved": "Response saved",
|
||||||
"invalid_name": "Please provide a name for the response"
|
"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": {
|
"settings": {
|
||||||
"accent_color": "Accent color",
|
"accent_color": "Accent color",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
|
|
@ -1781,6 +1788,7 @@
|
||||||
"parameters": "Parameters",
|
"parameters": "Parameters",
|
||||||
"post_request_script": "Post-request Script",
|
"post_request_script": "Post-request Script",
|
||||||
"pre_request_script": "Pre-request Script",
|
"pre_request_script": "Pre-request Script",
|
||||||
|
"scripts": "Scripts",
|
||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ declare module 'vue' {
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
|
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
|
||||||
IconLucideFileQuestion: typeof import('~icons/lucide/file-question')['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']
|
IconLucideFileText: typeof import('~icons/lucide/file-text')['default']
|
||||||
IconLucideFileX: typeof import('~icons/lucide/file-x')['default']
|
IconLucideFileX: typeof import('~icons/lucide/file-x')['default']
|
||||||
IconLucideFolder: typeof import('~icons/lucide/folder')['default']
|
IconLucideFolder: typeof import('~icons/lucide/folder')['default']
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
type: "pre-request" | "post-request"
|
type: "pre-request" | "post-request"
|
||||||
|
readOnly?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
|
readOnly: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,12 +42,15 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const editorModel = ref<monaco.editor.ITextModel | null>(null)
|
const editorModel = ref<monaco.editor.ITextModel | null>(null)
|
||||||
|
|
||||||
const MONACO_EDITOR_OPTIONS: Readonly<monaco.editor.IStandaloneEditorConstructionOptions> =
|
const MONACO_EDITOR_OPTIONS = computed<
|
||||||
{
|
Readonly<monaco.editor.IStandaloneEditorConstructionOptions>
|
||||||
automaticLayout: true,
|
>(() => ({
|
||||||
formatOnType: true,
|
automaticLayout: true,
|
||||||
formatOnPaste: true,
|
formatOnType: !props.readOnly,
|
||||||
}
|
formatOnPaste: !props.readOnly,
|
||||||
|
readOnly: props.readOnly,
|
||||||
|
domReadOnly: props.readOnly,
|
||||||
|
}))
|
||||||
|
|
||||||
// Static imports: import X from "URL"
|
// Static imports: import X from "URL"
|
||||||
const staticImportRegex =
|
const staticImportRegex =
|
||||||
|
|
@ -237,3 +242,27 @@ const monacoEditorTheme = computed(() =>
|
||||||
["dark", "black"].includes(theme.value) ? "vs-dark" : "vs"
|
["dark", "black"].includes(theme.value) ? "vs-dark" : "vs"
|
||||||
)
|
)
|
||||||
</script>
|
</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>
|
||||||
|
|
||||||
|
<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
|
<HoppSmartTab
|
||||||
v-if="showDetails"
|
v-if="showDetails"
|
||||||
:id="'details'"
|
:id="'details'"
|
||||||
|
|
@ -138,11 +215,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||||
import { clone } from "lodash-es"
|
import { clone } from "lodash-es"
|
||||||
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useSetting } from "~/composables/settings"
|
||||||
import { useToast } from "~/composables/toast"
|
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 { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
|
||||||
|
|
@ -154,6 +237,7 @@ import {
|
||||||
HoppRESTHeaders,
|
HoppRESTHeaders,
|
||||||
GQLHeader,
|
GQLHeader,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { PersistenceService } from "~/services/persistence"
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
|
|
@ -206,10 +290,14 @@ const editableCollection = ref<{
|
||||||
headers: HoppCollectionHeaders
|
headers: HoppCollectionHeaders
|
||||||
auth: HoppCollectionAuth
|
auth: HoppCollectionAuth
|
||||||
variables: HoppCollectionVariable[]
|
variables: HoppCollectionVariable[]
|
||||||
|
preRequestScript: string
|
||||||
|
testScript: string
|
||||||
}>({
|
}>({
|
||||||
headers: [],
|
headers: [],
|
||||||
auth: { authType: "inherit", authActive: false },
|
auth: { authType: "inherit", authActive: false },
|
||||||
variables: [],
|
variables: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
|
|
@ -217,9 +305,65 @@ const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
1000
|
1000
|
||||||
)
|
)
|
||||||
const activeTab = useVModel(props, "modelValue", emit)
|
const activeTab = useVModel(props, "modelValue", emit)
|
||||||
|
const activeScriptsTab = ref<"pre-request" | "test-script">("pre-request")
|
||||||
|
|
||||||
const activeTabIsDetails = computed(() => activeTab.value === "details")
|
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 (
|
const persistUnsavedChanges = async (
|
||||||
updated: typeof editableCollection.value
|
updated: typeof editableCollection.value
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -258,23 +402,34 @@ const enforceTabAccessRules = () => {
|
||||||
["headers", "authorization"].includes(activeTab.value)
|
["headers", "authorization"].includes(activeTab.value)
|
||||||
)
|
)
|
||||||
activeTab.value = "variables"
|
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 = () => {
|
const loadEditableCollection = () => {
|
||||||
|
activeScriptsTab.value = "pre-request"
|
||||||
editableCollection.value = {
|
editableCollection.value = {
|
||||||
auth: clone(props.editingProperties.collection!.auth as HoppCollectionAuth),
|
auth: clone(props.editingProperties.collection!.auth as HoppCollectionAuth),
|
||||||
headers: clone(
|
headers: clone(
|
||||||
props.editingProperties.collection!.headers as HoppCollectionHeaders
|
props.editingProperties.collection!.headers as HoppCollectionHeaders
|
||||||
),
|
),
|
||||||
variables: clone(props.editingProperties.collection!.variables || []),
|
variables: clone(props.editingProperties.collection!.variables || []),
|
||||||
|
preRequestScript:
|
||||||
|
props.editingProperties.collection!.preRequestScript || "",
|
||||||
|
testScript: props.editingProperties.collection!.testScript || "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetEditableCollection = () => {
|
const resetEditableCollection = () => {
|
||||||
|
activeScriptsTab.value = "pre-request"
|
||||||
editableCollection.value = {
|
editableCollection.value = {
|
||||||
headers: [],
|
headers: [],
|
||||||
auth: { authType: "inherit", authActive: false },
|
auth: { authType: "inherit", authActive: false },
|
||||||
variables: [],
|
variables: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -737,6 +737,8 @@ const saveCollectionDocumentation = async () => {
|
||||||
headers: collection.headers || [],
|
headers: collection.headers || [],
|
||||||
variables: collection.variables || [],
|
variables: collection.variables || [],
|
||||||
description: documentationDescription.value,
|
description: documentationDescription.value,
|
||||||
|
preRequestScript: collection.preRequestScript || "",
|
||||||
|
testScript: collection.testScript || "",
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
|
|
@ -831,6 +833,8 @@ const saveCollectionDocumentationById = async (
|
||||||
headers: collectionData.headers || [],
|
headers: collectionData.headers || [],
|
||||||
variables: collectionData.variables || [],
|
variables: collectionData.variables || [],
|
||||||
description: documentation,
|
description: documentation,
|
||||||
|
preRequestScript: collectionData.preRequestScript || "",
|
||||||
|
testScript: collectionData.testScript || "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pipe(
|
const result = await pipe(
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ const addNewCollection = () => {
|
||||||
},
|
},
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
description: "",
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -906,6 +906,8 @@ const addNewRootCollection = async (name: string) => {
|
||||||
},
|
},
|
||||||
variables: [],
|
variables: [],
|
||||||
description: "",
|
description: "",
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3254,6 +3256,7 @@ const editProperties = async (payload: {
|
||||||
},
|
},
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
scripts: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentIndex) {
|
if (parentIndex) {
|
||||||
|
|
@ -3307,16 +3310,19 @@ const editProperties = async (payload: {
|
||||||
description: null as string | null,
|
description: null as string | null,
|
||||||
folders: null,
|
folders: null,
|
||||||
requests: null,
|
requests: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentIndex) {
|
if (parentIndex) {
|
||||||
const { auth, headers, variables } =
|
const { auth, headers, variables, scripts } =
|
||||||
teamCollectionService.cascadeParentCollectionForProperties(parentIndex)
|
teamCollectionService.cascadeParentCollectionForProperties(parentIndex)
|
||||||
|
|
||||||
inheritedProperties = {
|
inheritedProperties = {
|
||||||
auth,
|
auth,
|
||||||
headers,
|
headers,
|
||||||
variables,
|
variables,
|
||||||
|
scripts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3337,6 +3343,8 @@ const editProperties = async (payload: {
|
||||||
headers: data.headers,
|
headers: data.headers,
|
||||||
variables: collectionVariables,
|
variables: collectionVariables,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
coll = {
|
coll = {
|
||||||
|
|
@ -3448,6 +3456,8 @@ const setCollectionProperties = (newCollection: {
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
description: collection.description ?? null,
|
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
|
// 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
|
<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"
|
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">
|
<div class="flex items-center gap-2">
|
||||||
{{ t("preRequest.javascript_code") }}
|
<label class="truncate font-semibold text-secondaryLight">
|
||||||
</label>
|
{{ 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">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
|
@ -76,6 +93,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<HttpInheritedScriptsModal
|
||||||
|
:show="showInheritedModal"
|
||||||
|
:scripts="inheritedScripts"
|
||||||
|
script-type="preRequestScript"
|
||||||
|
@close="showInheritedModal = false"
|
||||||
|
/>
|
||||||
<AiexperimentsModifyPreRequestModal
|
<AiexperimentsModifyPreRequestModal
|
||||||
v-if="isModifyPreRequestModalOpen && currentRequest"
|
v-if="isModifyPreRequestModalOpen && currentRequest"
|
||||||
:current-script="preRequestScript"
|
:current-script="preRequestScript"
|
||||||
|
|
@ -101,9 +124,12 @@ import { useReadonlyStream } from "~/composables/stream"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import completer from "~/helpers/editor/completion/preRequest"
|
import completer from "~/helpers/editor/completion/preRequest"
|
||||||
import linter from "~/helpers/editor/linting/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 { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import IconFileSymlink from "~icons/lucide/file-symlink"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import IconSparkles from "~icons/lucide/sparkles"
|
import IconSparkles from "~icons/lucide/sparkles"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
|
@ -114,6 +140,7 @@ const t = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
inheritedProperties?: HoppInheritedProperty
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", value: string): void
|
(e: "update:modelValue", value: string): void
|
||||||
|
|
@ -121,6 +148,16 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
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 preRequestEditor = ref<any | null>(null)
|
||||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,16 @@
|
||||||
:id="'preRequestScript'"
|
:id="'preRequestScript'"
|
||||||
:label="`${t('tab.pre_request_script')}`"
|
:label="`${t('tab.pre_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
'preRequestScript' in request &&
|
('preRequestScript' in request &&
|
||||||
request.preRequestScript &&
|
hasActualScript(request.preRequestScript)) ||
|
||||||
request.preRequestScript.length > 0
|
hasInheritedPreRequestScripts
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<HttpPreRequestScript
|
<HttpPreRequestScript
|
||||||
v-if="'preRequestScript' in request"
|
v-if="'preRequestScript' in request"
|
||||||
v-model="request.preRequestScript"
|
v-model="request.preRequestScript"
|
||||||
:is-active="selectedOptionTab === 'preRequestScript'"
|
:is-active="selectedOptionTab === 'preRequestScript'"
|
||||||
|
:inherited-properties="inheritedProperties"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
|
@ -72,17 +71,15 @@
|
||||||
:id="'tests'"
|
:id="'tests'"
|
||||||
:label="`${t('tab.post_request_script')}`"
|
:label="`${t('tab.post_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
'testScript' in request &&
|
('testScript' in request && hasActualScript(request.testScript)) ||
|
||||||
request.testScript &&
|
hasInheritedTestScripts
|
||||||
request.testScript.length > 0
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<HttpTests
|
<HttpTests
|
||||||
v-if="'testScript' in request"
|
v-if="'testScript' in request"
|
||||||
v-model="request.testScript"
|
v-model="request.testScript"
|
||||||
:is-active="selectedOptionTab === 'tests'"
|
:is-active="selectedOptionTab === 'tests'"
|
||||||
|
:inherited-properties="inheritedProperties"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
|
@ -107,6 +104,7 @@ import { useVModel } from "@vueuse/core"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { AggregateEnvironment } from "~/newstore/environments"
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
|
|
@ -188,6 +186,22 @@ const isBodyFilled = computed(() => {
|
||||||
return Boolean(request.value.body.body && request.value.body.body.length > 0)
|
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 }) => {
|
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||||
selectedOptionTab.value = tab as RESTOptionTabs
|
selectedOptionTab.value = tab as RESTOptionTabs
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,26 @@
|
||||||
<div
|
<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"
|
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">
|
<div class="flex items-center gap-2">
|
||||||
{{ t("test.javascript_code") }}
|
<label class="truncate font-semibold text-secondaryLight">
|
||||||
</label>
|
{{ 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">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
|
@ -76,6 +93,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<HttpInheritedScriptsModal
|
||||||
|
:show="showInheritedModal"
|
||||||
|
:scripts="inheritedScripts"
|
||||||
|
script-type="testScript"
|
||||||
|
@close="showInheritedModal = false"
|
||||||
|
/>
|
||||||
<AiexperimentsModifyTestScriptModal
|
<AiexperimentsModifyTestScriptModal
|
||||||
v-if="isModifyTestScriptModalOpen && currentRequest"
|
v-if="isModifyTestScriptModalOpen && currentRequest"
|
||||||
:current-script="testScript"
|
:current-script="testScript"
|
||||||
|
|
@ -99,10 +122,13 @@ import { useReadonlyStream } from "~/composables/stream"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import completer from "~/helpers/editor/completion/testScript"
|
import completer from "~/helpers/editor/completion/testScript"
|
||||||
import linter from "~/helpers/editor/linting/testScript"
|
import linter from "~/helpers/editor/linting/testScript"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import testSnippets from "~/helpers/testSnippets"
|
import testSnippets from "~/helpers/testSnippets"
|
||||||
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import IconFileSymlink from "~icons/lucide/file-symlink"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import IconSparkles from "~icons/lucide/sparkles"
|
import IconSparkles from "~icons/lucide/sparkles"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
|
@ -113,9 +139,22 @@ const t = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
inheritedProperties?: HoppInheritedProperty
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"])
|
const emit = defineEmits(["update:modelValue"])
|
||||||
const testScript = useVModel(props, "modelValue", emit)
|
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 testScriptEditor = ref<any | null>(null)
|
||||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,11 @@ const runTests = async () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
let resolvedCollection: HoppCollection = collection.value
|
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) {
|
if (!isPersonalWorkspace) {
|
||||||
const requestAuth = tab.value.document.inheritedProperties?.auth
|
const requestAuth = tab.value.document.inheritedProperties?.auth
|
||||||
|
|
@ -270,6 +275,19 @@ const runTests = async () => {
|
||||||
tab.value.document.inheritedProperties?.variables ?? []
|
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 = {
|
resolvedCollection = {
|
||||||
...collection.value,
|
...collection.value,
|
||||||
auth: requestAuth,
|
auth: requestAuth,
|
||||||
|
|
@ -277,12 +295,23 @@ const runTests = async () => {
|
||||||
variables: parentVariables,
|
variables: parentVariables,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { auth, headers, variables } = collectionInheritedProps ?? {
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
ancestorPreRequestScripts: preAncestors,
|
||||||
|
ancestorTestScripts: testAncestors,
|
||||||
|
} = collectionInheritedProps ?? {
|
||||||
auth: { authActive: true, authType: "none" },
|
auth: { authActive: true, authType: "none" },
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
ancestorPreRequestScripts: [],
|
||||||
|
ancestorTestScripts: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ancestorPreRequestScripts = preAncestors
|
||||||
|
ancestorTestScripts = testAncestors
|
||||||
|
|
||||||
resolvedCollection = {
|
resolvedCollection = {
|
||||||
...collection.value,
|
...collection.value,
|
||||||
auth,
|
auth,
|
||||||
|
|
@ -292,10 +321,16 @@ const runTests = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
|
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
|
||||||
testRunnerService.runTests(tab, resolvedCollection, {
|
testRunnerService.runTests(
|
||||||
...testRunnerConfig.value,
|
tab,
|
||||||
stopRef: testRunnerStopRef,
|
resolvedCollection,
|
||||||
})
|
{
|
||||||
|
...testRunnerConfig.value,
|
||||||
|
stopRef: testRunnerStopRef,
|
||||||
|
},
|
||||||
|
ancestorPreRequestScripts,
|
||||||
|
ancestorTestScripts
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopTests = () => {
|
const stopTests = () => {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ import { map } from "fp-ts/Either"
|
||||||
import { runPreRequestScript, runTestScript } from "@hoppscotch/js-sandbox/web"
|
import { runPreRequestScript, runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||||
import { useSetting } from "~/composables/settings"
|
import { useSetting } from "~/composables/settings"
|
||||||
import { getService } from "~/modules/dioc"
|
import { getService } from "~/modules/dioc"
|
||||||
import { stripModulePrefix } from "~/helpers/scripting"
|
import {
|
||||||
|
combineScriptsWithIIFE,
|
||||||
|
hasActualScript,
|
||||||
|
} from "~/helpers/scripting"
|
||||||
import { createHoppFetchHook } from "~/helpers/hopp-fetch"
|
import { createHoppFetchHook } from "~/helpers/hopp-fetch"
|
||||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||||
import {
|
import {
|
||||||
|
|
@ -364,14 +367,21 @@ const delegatePreRequestScriptRunner = (
|
||||||
selected: Environment["variables"]
|
selected: Environment["variables"]
|
||||||
temp: Environment["variables"]
|
temp: Environment["variables"]
|
||||||
},
|
},
|
||||||
cookies: Cookie[] | null
|
cookies: Cookie[] | null,
|
||||||
|
inheritedPreRequestScripts: string[] = []
|
||||||
): Promise<E.Either<string, SandboxPreRequestResult>> => {
|
): Promise<E.Either<string, SandboxPreRequestResult>> => {
|
||||||
const { preRequestScript } = request
|
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
|
// Short-circuit empty scripts to avoid unnecessary WASM initialization
|
||||||
if (cleanScript.trim().length === 0) {
|
if (combinedScript.length === 0) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
E.right({
|
E.right({
|
||||||
updatedEnvs: envs,
|
updatedEnvs: envs,
|
||||||
|
|
@ -380,19 +390,16 @@ const delegatePreRequestScriptRunner = (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
if (!experimentalScriptingSandbox) {
|
||||||
// Strip `export {};\n` before executing in legacy sandbox to prevent syntax errors
|
return runPreRequestScript(combinedScript, {
|
||||||
|
|
||||||
return runPreRequestScript(cleanScript, {
|
|
||||||
envs,
|
envs,
|
||||||
experimentalScriptingSandbox: false,
|
experimentalScriptingSandbox: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Experimental sandbox enabled - use faraday-cage with hook
|
|
||||||
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
||||||
|
|
||||||
return runPreRequestScript(cleanScript, {
|
return runPreRequestScript(combinedScript, {
|
||||||
envs,
|
envs,
|
||||||
request,
|
request,
|
||||||
cookies,
|
cookies,
|
||||||
|
|
@ -405,14 +412,21 @@ const runPostRequestScript = (
|
||||||
envs: TestResult["envs"],
|
envs: TestResult["envs"],
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
response: HoppRESTResponse,
|
response: HoppRESTResponse,
|
||||||
cookies: Cookie[] | null
|
cookies: Cookie[] | null,
|
||||||
|
inheritedTestScripts: string[] = []
|
||||||
): Promise<E.Either<string, SandboxTestResult>> => {
|
): Promise<E.Either<string, SandboxTestResult>> => {
|
||||||
const { testScript } = request
|
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
|
// Short-circuit empty scripts to avoid unnecessary WASM initialization
|
||||||
if (cleanScript.trim().length === 0) {
|
if (combinedScript.length === 0) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
E.right({
|
E.right({
|
||||||
tests: { descriptor: "root", expectResults: [], children: [] },
|
tests: { descriptor: "root", expectResults: [], children: [] },
|
||||||
|
|
@ -423,20 +437,17 @@ const runPostRequestScript = (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
if (!experimentalScriptingSandbox) {
|
||||||
// Strip `export {};\n` before executing in legacy sandbox to prevent syntax errors
|
return runTestScript(combinedScript, {
|
||||||
|
|
||||||
return runTestScript(cleanScript, {
|
|
||||||
envs,
|
envs,
|
||||||
response,
|
response,
|
||||||
experimentalScriptingSandbox: false,
|
experimentalScriptingSandbox: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Experimental sandbox enabled - use faraday-cage with hook
|
|
||||||
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
const hoppFetchHook = createHoppFetchHook(kernelInterceptorService)
|
||||||
|
|
||||||
return runTestScript(cleanScript, {
|
return runTestScript(combinedScript, {
|
||||||
envs,
|
envs,
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
|
|
@ -497,10 +508,20 @@ export function runRESTRequest$(
|
||||||
initialEnvsForComparison,
|
initialEnvsForComparison,
|
||||||
} = captureInitialEnvironmentState()
|
} = 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(
|
const res = delegatePreRequestScriptRunner(
|
||||||
resolvedRequest,
|
resolvedRequest,
|
||||||
initialEnvs,
|
initialEnvs,
|
||||||
cookieJarEntries
|
cookieJarEntries,
|
||||||
|
inheritedPreRequestScripts
|
||||||
).then(async (preRequestScriptResult) => {
|
).then(async (preRequestScriptResult) => {
|
||||||
if (cancelCalled) return E.left("cancellation" as const)
|
if (cancelCalled) return E.left("cancellation" as const)
|
||||||
|
|
||||||
|
|
@ -579,7 +600,8 @@ export function runRESTRequest$(
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
responseTime: res.meta.responseDuration,
|
responseTime: res.meta.responseDuration,
|
||||||
},
|
},
|
||||||
preRequestScriptResult.right.updatedCookies ?? null
|
preRequestScriptResult.right.updatedCookies ?? null,
|
||||||
|
inheritedTestScripts
|
||||||
)
|
)
|
||||||
|
|
||||||
if (E.isRight(postRequestScriptResult)) {
|
if (E.isRight(postRequestScriptResult)) {
|
||||||
|
|
@ -795,7 +817,9 @@ export async function runTestRunnerRequest(
|
||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
persistEnv = true,
|
persistEnv = true,
|
||||||
inheritedVariables: HoppCollectionVariable[] = [],
|
inheritedVariables: HoppCollectionVariable[] = [],
|
||||||
initialEnvironmentState: InitialEnvironmentState
|
initialEnvironmentState: InitialEnvironmentState,
|
||||||
|
inheritedPreRequestScripts: string[] = [],
|
||||||
|
inheritedTestScripts: string[] = []
|
||||||
): Promise<
|
): Promise<
|
||||||
| E.Left<"script_fail">
|
| E.Left<"script_fail">
|
||||||
| E.Right<{
|
| E.Right<{
|
||||||
|
|
@ -824,7 +848,8 @@ export async function runTestRunnerRequest(
|
||||||
return delegatePreRequestScriptRunner(
|
return delegatePreRequestScriptRunner(
|
||||||
request,
|
request,
|
||||||
initialEnvs,
|
initialEnvs,
|
||||||
cookieJarEntries
|
cookieJarEntries,
|
||||||
|
inheritedPreRequestScripts
|
||||||
).then(async (preRequestScriptResult) => {
|
).then(async (preRequestScriptResult) => {
|
||||||
if (E.isLeft(preRequestScriptResult)) {
|
if (E.isLeft(preRequestScriptResult)) {
|
||||||
console.error("[Pre-Request Script Error]", preRequestScriptResult.left)
|
console.error("[Pre-Request Script Error]", preRequestScriptResult.left)
|
||||||
|
|
@ -883,7 +908,8 @@ export async function runTestRunnerRequest(
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
responseTime: res.meta.responseDuration,
|
responseTime: res.meta.responseDuration,
|
||||||
},
|
},
|
||||||
preRequestScriptResult.right.updatedCookies ?? null
|
preRequestScriptResult.right.updatedCookies ?? null,
|
||||||
|
inheritedTestScripts
|
||||||
)
|
)
|
||||||
|
|
||||||
if (E.isRight(postRequestScriptResult)) {
|
if (E.isRight(postRequestScriptResult)) {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ export type CollectionDataProps = {
|
||||||
headers: HoppRESTHeaders
|
headers: HoppRESTHeaders
|
||||||
variables: HoppCollectionVariable[]
|
variables: HoppCollectionVariable[]
|
||||||
description: string | null
|
description: string | null
|
||||||
|
preRequestScript: string
|
||||||
|
testScript: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BACKEND_PAGE_SIZE = 10
|
export const BACKEND_PAGE_SIZE = 10
|
||||||
|
|
@ -121,6 +123,8 @@ const parseCollectionData = (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
@ -159,11 +163,23 @@ const parseCollectionData = (
|
||||||
? parsedData.description
|
? parsedData.description
|
||||||
: defaultDataProps.description
|
: defaultDataProps.description
|
||||||
|
|
||||||
|
const preRequestScript =
|
||||||
|
typeof parsedData?.preRequestScript === "string"
|
||||||
|
? parsedData.preRequestScript
|
||||||
|
: defaultDataProps.preRequestScript
|
||||||
|
|
||||||
|
const testScript =
|
||||||
|
typeof parsedData?.testScript === "string"
|
||||||
|
? parsedData.testScript
|
||||||
|
: defaultDataProps.testScript
|
||||||
|
|
||||||
return {
|
return {
|
||||||
auth,
|
auth,
|
||||||
headers,
|
headers,
|
||||||
variables,
|
variables,
|
||||||
description,
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,9 +187,14 @@ const parseCollectionData = (
|
||||||
export const teamCollectionJSONToHoppRESTColl = (
|
export const teamCollectionJSONToHoppRESTColl = (
|
||||||
coll: TeamCollectionJSON
|
coll: TeamCollectionJSON
|
||||||
): HoppCollection => {
|
): HoppCollection => {
|
||||||
const { auth, headers, variables, description } = parseCollectionData(
|
const {
|
||||||
coll.data
|
auth,
|
||||||
)
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} = parseCollectionData(coll.data)
|
||||||
|
|
||||||
return makeCollection({
|
return makeCollection({
|
||||||
id: coll.id,
|
id: coll.id,
|
||||||
|
|
@ -184,6 +205,8 @@ export const teamCollectionJSONToHoppRESTColl = (
|
||||||
headers,
|
headers,
|
||||||
variables,
|
variables,
|
||||||
description,
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,7 +270,14 @@ export const teamCollToHoppRESTColl = (
|
||||||
description: null,
|
description: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { auth, headers, variables, description } = parseCollectionData(data)
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} = parseCollectionData(data)
|
||||||
|
|
||||||
return makeCollection({
|
return makeCollection({
|
||||||
id: coll.id,
|
id: coll.id,
|
||||||
|
|
@ -258,6 +288,8 @@ export const teamCollToHoppRESTColl = (
|
||||||
headers: headers ?? [],
|
headers: headers ?? [],
|
||||||
variables: variables ?? [],
|
variables: variables ?? [],
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ function parseCollectionDataFromString(data?: string): CollectionDataProps {
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
@ -111,6 +113,9 @@ function parseCollectionDataFromString(data?: string): CollectionDataProps {
|
||||||
headers: parsed.headers || defaultDataProps.headers,
|
headers: parsed.headers || defaultDataProps.headers,
|
||||||
variables: parsed.variables || defaultDataProps.variables,
|
variables: parsed.variables || defaultDataProps.variables,
|
||||||
description: parsed.description || defaultDataProps.description,
|
description: parsed.description || defaultDataProps.description,
|
||||||
|
preRequestScript:
|
||||||
|
parsed.preRequestScript || defaultDataProps.preRequestScript,
|
||||||
|
testScript: parsed.testScript || defaultDataProps.testScript,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse collection data:", error)
|
console.error("Failed to parse collection data:", error)
|
||||||
|
|
@ -127,8 +132,14 @@ export function collectionFolderToHoppCollection(
|
||||||
folder: CollectionFolder
|
folder: CollectionFolder
|
||||||
): HoppCollection {
|
): HoppCollection {
|
||||||
// Parse the data field to extract auth, headers, variables, and description
|
// Parse the data field to extract auth, headers, variables, and description
|
||||||
const { auth, headers, variables, description } =
|
const {
|
||||||
parseCollectionDataFromString(folder.data)
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} = parseCollectionDataFromString(folder.data)
|
||||||
|
|
||||||
return makeCollection({
|
return makeCollection({
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
|
|
@ -139,6 +150,8 @@ export function collectionFolderToHoppCollection(
|
||||||
variables,
|
variables,
|
||||||
description,
|
description,
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,8 @@ export function transformCollectionForImport(
|
||||||
headers: collection.headers,
|
headers: collection.headers,
|
||||||
variables: collection.variables,
|
variables: collection.variables,
|
||||||
description: collection.description,
|
description: collection.description,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj: CollectionFolder = {
|
const obj: CollectionFolder = {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ export const harImporter = (
|
||||||
headers: [],
|
headers: [],
|
||||||
description: null,
|
description: null,
|
||||||
variables: [],
|
variables: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
return E.right([collection])
|
return E.right([collection])
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,8 @@ const getHoppFolder = (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
|
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
|
||||||
description: folderRes.meta?.description ?? null,
|
description: folderRes.meta?.description ?? null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const getHoppCollections = (docs: InsomniaDoc[]) => {
|
const getHoppCollections = (docs: InsomniaDoc[]) => {
|
||||||
|
|
@ -283,6 +285,8 @@ const getParsedHoppFolder = (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: getCollectionVariables(collection.environment),
|
variables: getCollectionVariables(collection.environment),
|
||||||
description: collection.meta.description ?? null,
|
description: collection.meta.description ?? null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,6 +327,8 @@ const getParsedHoppCollections = (docs: InsomniaDocV5[]): HoppCollection[] =>
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: getCollectionVariables(doc.environments?.data),
|
variables: getCollectionVariables(doc.environments?.data),
|
||||||
description: doc.meta.description ?? null,
|
description: doc.meta.description ?? null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1186,6 +1186,8 @@ const convertOpenApiDocsToHopp = (
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "inherit", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
requests: requestsWithoutTags,
|
requests: requestsWithoutTags,
|
||||||
|
|
@ -1193,6 +1195,8 @@ const convertOpenApiDocsToHopp = (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description,
|
description,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,36 @@ const getHoppScripts = (
|
||||||
return { preRequestScript, testScript }
|
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 = (
|
const getCollectionDescription = (
|
||||||
docField?: string | DescriptionDefinition
|
docField?: string | DescriptionDefinition
|
||||||
): string | null => {
|
): string | null => {
|
||||||
|
|
@ -609,8 +639,13 @@ const getHoppRequest = (
|
||||||
const getHoppFolder = (
|
const getHoppFolder = (
|
||||||
ig: ItemGroup<Item>,
|
ig: ItemGroup<Item>,
|
||||||
importScripts: boolean
|
importScripts: boolean
|
||||||
): HoppCollection =>
|
): HoppCollection => {
|
||||||
makeCollection({
|
const { preRequestScript, testScript } = getHoppCollectionScripts(
|
||||||
|
ig,
|
||||||
|
importScripts
|
||||||
|
)
|
||||||
|
|
||||||
|
return makeCollection({
|
||||||
name: ig.name,
|
name: ig.name,
|
||||||
folders: pipe(
|
folders: pipe(
|
||||||
ig.items.all(),
|
ig.items.all(),
|
||||||
|
|
@ -626,7 +661,10 @@ const getHoppFolder = (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: getHoppCollVariables(ig),
|
variables: getHoppCollVariables(ig),
|
||||||
description: getCollectionDescription(ig.description),
|
description: getCollectionDescription(ig.description),
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getHoppCollections = (
|
export const getHoppCollections = (
|
||||||
collections: PMCollection[],
|
collections: PMCollection[],
|
||||||
|
|
|
||||||
|
|
@ -323,5 +323,9 @@ export function createExamplePetStoreCollection(
|
||||||
authActive: true,
|
authActive: true,
|
||||||
},
|
},
|
||||||
headers: [],
|
headers: [],
|
||||||
|
variables: [],
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,9 @@ export async function createMockCollectionForPersonal(
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: data.headers,
|
headers: data.headers,
|
||||||
variables: data.variables,
|
variables: data.variables,
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add the backend ID to the collection
|
// 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.
|
* (non-module context) or when exporting collections.
|
||||||
*/
|
*/
|
||||||
export const stripModulePrefix = (script: string): string => {
|
export const stripModulePrefix = (script: string): string => {
|
||||||
return script.startsWith(MODULE_PREFIX)
|
if (script.startsWith(MODULE_PREFIX)) {
|
||||||
? script.slice(MODULE_PREFIX.length)
|
return script.slice(MODULE_PREFIX.length)
|
||||||
: script
|
}
|
||||||
|
if (script.startsWith("export {};")) {
|
||||||
|
return script.slice("export {};".length)
|
||||||
|
}
|
||||||
|
return script
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regex for stripping the JSON-serialized module prefix (`export {};\\n`)
|
* Anchored to JSON value-opening delimiters so it only matches inside JSON
|
||||||
* from scripts during collection exports.
|
* string values during collection export, not inside script source. Matches
|
||||||
* Note: This matches the literal backslash-n (`\\n`), not an actual newline character.
|
* 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 * as E from "fp-ts/Either"
|
||||||
import { BehaviorSubject, Subscription } from "rxjs"
|
import { BehaviorSubject, Subscription } from "rxjs"
|
||||||
import {
|
import { HoppCollectionVariable, translateToNewRequest } from "@hoppscotch/data"
|
||||||
HoppCollectionVariable,
|
|
||||||
HoppRESTAuth,
|
|
||||||
HoppRESTHeader,
|
|
||||||
translateToNewRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { pull, remove } from "lodash-es"
|
import { pull, remove } from "lodash-es"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
|
import { CollectionDataProps } from "~/helpers/backend/helpers"
|
||||||
import { Subscription as WSubscription } from "wonka"
|
import { Subscription as WSubscription } from "wonka"
|
||||||
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
|
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
|
||||||
import { TeamCollection } from "./TeamCollection"
|
import { TeamCollection } from "./TeamCollection"
|
||||||
|
|
@ -1093,14 +1090,16 @@ export default class NewTeamCollectionAdapter {
|
||||||
|
|
||||||
const variables: HoppInheritedProperty["variables"] = []
|
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("/")
|
const path = folderPath.split("/")
|
||||||
|
|
||||||
// Check if the path is empty or invalid
|
// Check if the path is empty or invalid
|
||||||
if (!path || path.length === 0) {
|
if (!path || path.length === 0) {
|
||||||
console.error("Invalid path:", folderPath)
|
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'
|
// 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
|
// Check if parentFolder is undefined or null
|
||||||
if (!parentFolder) {
|
if (!parentFolder) {
|
||||||
console.error("Parent folder not found for path:", path)
|
console.error("Parent folder not found for path:", path)
|
||||||
return { auth, headers, variables }
|
return { auth, headers, variables, scripts }
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: {
|
const data: Partial<CollectionDataProps> = parentFolder.data
|
||||||
auth: HoppRESTAuth
|
|
||||||
headers: HoppRESTHeader[]
|
|
||||||
variables: HoppCollectionVariable[]
|
|
||||||
} = parentFolder.data
|
|
||||||
? JSON.parse(parentFolder.data)
|
? JSON.parse(parentFolder.data)
|
||||||
: {
|
: {}
|
||||||
auth: null,
|
|
||||||
headers: null,
|
|
||||||
variables: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.auth) {
|
if (!data.auth) {
|
||||||
data.auth = {
|
data.auth = {
|
||||||
|
|
@ -1141,6 +1132,8 @@ export default class NewTeamCollectionAdapter {
|
||||||
const parentFolderAuth = data.auth
|
const parentFolderAuth = data.auth
|
||||||
const parentFolderHeaders = data.headers
|
const parentFolderHeaders = data.headers
|
||||||
const parentFolderVariables = data.variables
|
const parentFolderVariables = data.variables
|
||||||
|
const parentFolderPreRequestScript = data.preRequestScript ?? ""
|
||||||
|
const parentFolderTestScript = data.testScript ?? ""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parentFolderAuth?.authType === "inherit" &&
|
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 {
|
import {
|
||||||
HoppCollectionVariable,
|
|
||||||
HoppRESTAuth,
|
HoppRESTAuth,
|
||||||
HoppRESTHeader,
|
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
getDefaultRESTRequest,
|
getDefaultRESTRequest,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
|
|
@ -10,6 +8,7 @@ import { Service } from "dioc"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { Ref, ref } from "vue"
|
import { Ref, ref } from "vue"
|
||||||
import { getSingleCollection, TeamCollection } from "./TeamCollection"
|
import { getSingleCollection, TeamCollection } from "./TeamCollection"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
|
||||||
|
|
@ -19,6 +18,24 @@ import {
|
||||||
TeamRequest,
|
TeamRequest,
|
||||||
getCollectionChildCollections,
|
getCollectionChildCollections,
|
||||||
} from "./TeamRequest"
|
} 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 = {
|
type CollectionSearchMeta = {
|
||||||
isSearchResult?: boolean
|
isSearchResult?: boolean
|
||||||
|
|
@ -354,6 +371,8 @@ export class TeamSearchService extends Service {
|
||||||
|
|
||||||
const defaultInheritedVariables: HoppInheritedProperty["variables"] = []
|
const defaultInheritedVariables: HoppInheritedProperty["variables"] = []
|
||||||
|
|
||||||
|
const defaultInheritedScripts: HoppInheritedProperty["scripts"] = []
|
||||||
|
|
||||||
const collection = Object.values(this.searchResultsCollections).find(
|
const collection = Object.values(this.searchResultsCollections).find(
|
||||||
(col) => col.id === collectionID
|
(col) => col.id === collectionID
|
||||||
)
|
)
|
||||||
|
|
@ -363,12 +382,14 @@ export class TeamSearchService extends Service {
|
||||||
auth: defaultInheritedAuth,
|
auth: defaultInheritedAuth,
|
||||||
headers: defaultInheritedHeaders,
|
headers: defaultInheritedHeaders,
|
||||||
variables: defaultInheritedVariables,
|
variables: defaultInheritedVariables,
|
||||||
|
scripts: defaultInheritedScripts,
|
||||||
}
|
}
|
||||||
|
|
||||||
const inheritedAuthData = this.findInheritableParentAuth(collectionID)
|
const inheritedAuthData = this.findInheritableParentAuth(collectionID)
|
||||||
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
|
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
|
||||||
const inheritedVariablesData =
|
const inheritedVariablesData =
|
||||||
this.findInheritableParentVariables(collectionID)
|
this.findInheritableParentVariables(collectionID)
|
||||||
|
const inheritedScriptsData = this.findInheritableParentScripts(collectionID)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
auth: E.isRight(inheritedAuthData)
|
auth: E.isRight(inheritedAuthData)
|
||||||
|
|
@ -380,6 +401,9 @@ export class TeamSearchService extends Service {
|
||||||
variables: E.isRight(inheritedVariablesData)
|
variables: E.isRight(inheritedVariablesData)
|
||||||
? Object.values(inheritedVariablesData.right)
|
? Object.values(inheritedVariablesData.right)
|
||||||
: defaultInheritedVariables,
|
: defaultInheritedVariables,
|
||||||
|
scripts: E.isRight(inheritedScriptsData)
|
||||||
|
? Object.values(inheritedScriptsData.right)
|
||||||
|
: defaultInheritedScripts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,13 +427,9 @@ export class TeamSearchService extends Service {
|
||||||
|
|
||||||
// has inherited data
|
// has inherited data
|
||||||
if (collection.data) {
|
if (collection.data) {
|
||||||
const parentInheritedData = JSON.parse(collection.data) as {
|
const parentInheritedData = parseCollectionData(collection.data)
|
||||||
auth?: HoppRESTAuth
|
|
||||||
headers?: HoppRESTHeader[]
|
|
||||||
variables?: HoppCollectionVariable[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const inheritedAuth = parentInheritedData.auth
|
const inheritedAuth = parentInheritedData?.auth
|
||||||
|
|
||||||
if (inheritedAuth && inheritedAuth.authType !== "inherit") {
|
if (inheritedAuth && inheritedAuth.authType !== "inherit") {
|
||||||
return E.right({
|
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
|
// see if it has headers to inherit, if yes, add it to the existing headers
|
||||||
if (collection.data) {
|
if (collection.data) {
|
||||||
const parentInheritedData = JSON.parse(collection.data) as {
|
const parentInheritedData = parseCollectionData(collection.data)
|
||||||
auth?: HoppRESTAuth
|
|
||||||
headers?: HoppRESTHeader[]
|
|
||||||
variables?: HoppCollectionVariable[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const inheritedHeaders = parentInheritedData.headers
|
const inheritedHeaders = parentInheritedData?.headers
|
||||||
|
|
||||||
if (inheritedHeaders) {
|
if (inheritedHeaders) {
|
||||||
inheritedHeaders.forEach((header) => {
|
inheritedHeaders.forEach((header) => {
|
||||||
|
|
@ -493,13 +509,9 @@ export class TeamSearchService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collection.data) {
|
if (collection.data) {
|
||||||
const parentData = JSON.parse(collection.data) as {
|
const parentData = parseCollectionData(collection.data)
|
||||||
auth?: HoppRESTAuth
|
|
||||||
headers?: HoppRESTHeader[]
|
|
||||||
variables?: HoppCollectionVariable[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const variables = parentData.variables
|
const variables = parentData?.variables
|
||||||
|
|
||||||
if (variables) {
|
if (variables) {
|
||||||
vars.push({
|
vars.push({
|
||||||
|
|
@ -517,6 +529,51 @@ export class TeamSearchService extends Service {
|
||||||
return E.right(vars)
|
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) => {
|
expandCollection = async (collectionID: string) => {
|
||||||
if (this.expandingCollections.value.includes(collectionID)) return
|
if (this.expandingCollections.value.includes(collectionID)) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,10 @@ export type HoppInheritedProperty = {
|
||||||
parentName: string
|
parentName: string
|
||||||
inheritedVariables: HoppCollectionVariable[]
|
inheritedVariables: HoppCollectionVariable[]
|
||||||
}[]
|
}[]
|
||||||
|
scripts: {
|
||||||
|
parentID: string
|
||||||
|
parentName: string
|
||||||
|
preRequestScript: string
|
||||||
|
testScript: string
|
||||||
|
}[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
GQLHeader,
|
GQLHeader,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import { pluck } from "rxjs/operators"
|
import { pluck } from "rxjs/operators"
|
||||||
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
|
@ -38,6 +39,8 @@ const defaultRESTCollectionState = {
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +58,8 @@ const defaultGraphqlCollectionState = {
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -142,14 +147,16 @@ export function cascadeParentCollectionForProperties(
|
||||||
|
|
||||||
const variables: HoppInheritedProperty["variables"] = []
|
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))
|
const path = folderPath.split("/").map((i) => parseInt(i))
|
||||||
|
|
||||||
// Check if the path is empty or invalid
|
// Check if the path is empty or invalid
|
||||||
if (!path || path.length === 0) {
|
if (!path || path.length === 0) {
|
||||||
console.error("Invalid path:", folderPath)
|
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'
|
// 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
|
// Check if parentFolder is undefined or null
|
||||||
if (!parentFolder) {
|
if (!parentFolder) {
|
||||||
console.error("Parent folder not found for path:", path)
|
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
|
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) {
|
function reorderItems(array: unknown[], from: number, to: number) {
|
||||||
|
|
@ -365,6 +390,8 @@ const restCollectionDispatchers = defineDispatchers({
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const newState = state
|
const newState = state
|
||||||
|
|
@ -1026,6 +1053,8 @@ const gqlCollectionDispatchers = defineDispatchers({
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
const newState = state
|
const newState = state
|
||||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||||
|
|
@ -1367,18 +1396,26 @@ export function getRESTCollection(collectionIndex: number) {
|
||||||
return restCollectionStore.value.state[collectionIndex]
|
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(
|
function computeCollectionInheritedProps(
|
||||||
collection: HoppCollection,
|
collection: HoppCollection,
|
||||||
ref_id: string,
|
ref_id: string,
|
||||||
type: "my-collections" | "team-collections" = "my-collections",
|
type: "my-collections" | "team-collections" = "my-collections",
|
||||||
parentAuth: HoppRESTAuth | null = null,
|
parentAuth: HoppRESTAuth | null = null,
|
||||||
parentHeaders: HoppRESTHeaders | null = null,
|
parentHeaders: HoppRESTHeaders | null = null,
|
||||||
parentVariables: HoppCollectionVariable[] | null = null
|
parentVariables: HoppCollectionVariable[] | null = null,
|
||||||
): {
|
parentPreRequestScripts: string[] = [],
|
||||||
auth: HoppRESTAuth
|
parentTestScripts: string[] = []
|
||||||
headers: HoppRESTHeaders
|
): RESTCollectionInheritedProps | null {
|
||||||
variables: HoppCollectionVariable[]
|
|
||||||
} | null {
|
|
||||||
// Determine the inherited authentication and headers
|
// Determine the inherited authentication and headers
|
||||||
const inheritedAuth =
|
const inheritedAuth =
|
||||||
collection.auth?.authType === "inherit" && collection.auth.authActive
|
collection.auth?.authType === "inherit" && collection.auth.authActive
|
||||||
|
|
@ -1406,9 +1443,19 @@ function computeCollectionInheritedProps(
|
||||||
auth: inheritedAuth,
|
auth: inheritedAuth,
|
||||||
headers: inheritedHeaders,
|
headers: inheritedHeaders,
|
||||||
variables: inheritedVariables,
|
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
|
// Recursively search in folders
|
||||||
for (const folder of collection.folders) {
|
for (const folder of collection.folders) {
|
||||||
const result = computeCollectionInheritedProps(
|
const result = computeCollectionInheritedProps(
|
||||||
|
|
@ -1417,7 +1464,9 @@ function computeCollectionInheritedProps(
|
||||||
type,
|
type,
|
||||||
inheritedAuth,
|
inheritedAuth,
|
||||||
inheritedHeaders,
|
inheritedHeaders,
|
||||||
inheritedVariables
|
inheritedVariables,
|
||||||
|
nextPreRequestScripts,
|
||||||
|
nextTestScripts
|
||||||
)
|
)
|
||||||
if (result) return result // Return as soon as a match is found
|
if (result) return result // Return as soon as a match is found
|
||||||
}
|
}
|
||||||
|
|
@ -1429,11 +1478,7 @@ export function getRESTCollectionInheritedProps(
|
||||||
collectionID: string,
|
collectionID: string,
|
||||||
collections: HoppCollection[] = restCollectionStore.value.state,
|
collections: HoppCollection[] = restCollectionStore.value.state,
|
||||||
type: "my-collections" | "team-collections" = "my-collections"
|
type: "my-collections" | "team-collections" = "my-collections"
|
||||||
): {
|
): RESTCollectionInheritedProps | null {
|
||||||
auth: HoppRESTAuth
|
|
||||||
headers: HoppRESTHeaders
|
|
||||||
variables: HoppCollectionVariable[]
|
|
||||||
} | null {
|
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
const result = computeCollectionInheritedProps(
|
const result = computeCollectionInheritedProps(
|
||||||
collection,
|
collection,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import {
|
||||||
translateToNewEnvironmentVariables,
|
translateToNewEnvironmentVariables,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import {
|
import {
|
||||||
PublishedDocREST,
|
PublishedDocREST,
|
||||||
PublishedDocsVersion,
|
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) {
|
if (collection.folders && collection.folders.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ describe("DocumentationService", () => {
|
||||||
variables: [],
|
variables: [],
|
||||||
id: "collection-123",
|
id: "collection-123",
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const mockRequest: HoppRESTRequest = makeRESTRequest({
|
const mockRequest: HoppRESTRequest = makeRESTRequest({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
CollectionSchemaVersion,
|
||||||
Environment,
|
Environment,
|
||||||
GlobalEnvironment,
|
GlobalEnvironment,
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
|
|
@ -25,7 +26,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
||||||
|
|
||||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
name: "Echo",
|
name: "Echo",
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
|
|
@ -54,13 +55,15 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
folders: [],
|
folders: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
{
|
{
|
||||||
v: 11,
|
v: CollectionSchemaVersion,
|
||||||
name: "Echo",
|
name: "Echo",
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
|
|
@ -80,6 +83,8 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
folders: [],
|
folders: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,16 @@ const HoppInheritedPropertySchema = z
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch([]),
|
.catch([]),
|
||||||
|
scripts: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
parentID: z.string(),
|
||||||
|
parentName: z.string(),
|
||||||
|
preRequestScript: z.string(),
|
||||||
|
testScript: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch([]),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { Subscription } from "rxjs"
|
import { Subscription } from "rxjs"
|
||||||
import {
|
import { HoppCollectionVariable, translateToNewRequest } from "@hoppscotch/data"
|
||||||
HoppCollectionVariable,
|
|
||||||
HoppRESTAuth,
|
|
||||||
HoppRESTHeader,
|
|
||||||
translateToNewRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { pull, remove } from "lodash-es"
|
import { pull, remove } from "lodash-es"
|
||||||
import { Subscription as WSubscription } from "wonka"
|
import { Subscription as WSubscription } from "wonka"
|
||||||
import {
|
import {
|
||||||
|
|
@ -34,6 +29,8 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
|
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
|
import { CollectionDataProps } from "~/helpers/backend/helpers"
|
||||||
|
|
||||||
export const TEAMS_BACKEND_PAGE_SIZE = 10
|
export const TEAMS_BACKEND_PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
@ -1123,14 +1120,16 @@ export class TeamCollectionsService extends Service<void> {
|
||||||
|
|
||||||
const variables: HoppInheritedProperty["variables"] = []
|
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("/")
|
const path = folderPath.split("/")
|
||||||
|
|
||||||
// Check if the path is empty or invalid
|
// Check if the path is empty or invalid
|
||||||
if (!path || path.length === 0) {
|
if (!path || path.length === 0) {
|
||||||
console.error("Invalid path:", folderPath)
|
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'
|
// 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
|
// Check if parentFolder is undefined or null
|
||||||
if (!parentFolder) {
|
if (!parentFolder) {
|
||||||
console.error("Parent folder not found for path:", path)
|
console.error("Parent folder not found for path:", path)
|
||||||
return { auth, headers, variables }
|
return { auth, headers, variables, scripts }
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: {
|
const data: Partial<CollectionDataProps> = parentFolder.data
|
||||||
auth?: HoppRESTAuth
|
|
||||||
headers?: HoppRESTHeader[]
|
|
||||||
variables?: HoppCollectionVariable[]
|
|
||||||
} = parentFolder.data
|
|
||||||
? JSON.parse(parentFolder.data)
|
? JSON.parse(parentFolder.data)
|
||||||
: {
|
: {}
|
||||||
auth: null,
|
|
||||||
headers: null,
|
|
||||||
variables: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.auth) {
|
if (!data.auth) {
|
||||||
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) {
|
private async waitForCollectionLoading(collectionID: string) {
|
||||||
|
|
@ -1260,6 +1269,7 @@ export class TeamCollectionsService extends Service<void> {
|
||||||
},
|
},
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
scripts: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = folderPath.split("/")
|
const path = folderPath.split("/")
|
||||||
|
|
@ -1278,6 +1288,7 @@ export class TeamCollectionsService extends Service<void> {
|
||||||
},
|
},
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
scripts: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
|
import { hasActualScript } from "~/helpers/scripting"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { nextTick, Ref } from "vue"
|
import { nextTick, Ref } from "vue"
|
||||||
|
|
@ -52,7 +53,9 @@ export class TestRunnerService extends Service {
|
||||||
public runTests(
|
public runTests(
|
||||||
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
tab: Ref<HoppTab<HoppTestRunnerDocument>>,
|
||||||
collection: HoppCollection,
|
collection: HoppCollection,
|
||||||
options: TestRunnerOptions
|
options: TestRunnerOptions,
|
||||||
|
ancestorPreRequestScripts: string[] = [],
|
||||||
|
ancestorTestScripts: string[] = []
|
||||||
) {
|
) {
|
||||||
// Reset the result collection
|
// Reset the result collection
|
||||||
tab.value.document.status = "running"
|
tab.value.document.status = "running"
|
||||||
|
|
@ -66,9 +69,22 @@ export class TestRunnerService extends Service {
|
||||||
requests: [],
|
requests: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: collection.description ?? null,
|
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(() => {
|
.then(() => {
|
||||||
tab.value.document.status = "stopped"
|
tab.value.document.status = "stopped"
|
||||||
})
|
})
|
||||||
|
|
@ -96,7 +112,9 @@ export class TestRunnerService extends Service {
|
||||||
parentHeaders?: HoppRESTHeaders,
|
parentHeaders?: HoppRESTHeaders,
|
||||||
parentAuth?: HoppRESTRequest["auth"],
|
parentAuth?: HoppRESTRequest["auth"],
|
||||||
parentVariables: HoppCollection["variables"] = [],
|
parentVariables: HoppCollection["variables"] = [],
|
||||||
parentID?: string
|
parentID?: string,
|
||||||
|
parentPreRequestScripts: string[] = [],
|
||||||
|
parentTestScripts: string[] = []
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Compute inherited auth and headers for this collection
|
// 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
|
// Process folders progressively
|
||||||
for (let i = 0; i < collection.folders.length; i++) {
|
for (let i = 0; i < collection.folders.length; i++) {
|
||||||
if (options.stopRef?.value) {
|
if (options.stopRef?.value) {
|
||||||
|
|
@ -142,7 +173,6 @@ export class TestRunnerService extends Service {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pass inherited headers and auth to the folder
|
|
||||||
await this.runTestCollection(
|
await this.runTestCollection(
|
||||||
tab,
|
tab,
|
||||||
folder,
|
folder,
|
||||||
|
|
@ -151,7 +181,9 @@ export class TestRunnerService extends Service {
|
||||||
inheritedHeaders,
|
inheritedHeaders,
|
||||||
inheritedAuth,
|
inheritedAuth,
|
||||||
inheritedVariables,
|
inheritedVariables,
|
||||||
collection._ref_id || collection.id
|
collection._ref_id || collection.id,
|
||||||
|
inheritedPreRequestScripts,
|
||||||
|
inheritedTestScripts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +220,9 @@ export class TestRunnerService extends Service {
|
||||||
collection,
|
collection,
|
||||||
options,
|
options,
|
||||||
currentPath,
|
currentPath,
|
||||||
inheritedVariables
|
inheritedVariables,
|
||||||
|
inheritedPreRequestScripts,
|
||||||
|
inheritedTestScripts
|
||||||
)
|
)
|
||||||
|
|
||||||
if (options.delay && options.delay > 0) {
|
if (options.delay && options.delay > 0) {
|
||||||
|
|
@ -279,7 +313,9 @@ export class TestRunnerService extends Service {
|
||||||
collection: HoppCollection,
|
collection: HoppCollection,
|
||||||
options: TestRunnerOptions,
|
options: TestRunnerOptions,
|
||||||
path: number[],
|
path: number[],
|
||||||
inheritedVariables: HoppCollectionVariable[] = []
|
inheritedVariables: HoppCollectionVariable[] = [],
|
||||||
|
inheritedPreRequestScripts: string[] = [],
|
||||||
|
inheritedTestScripts: string[] = []
|
||||||
) {
|
) {
|
||||||
if (options.stopRef?.value) {
|
if (options.stopRef?.value) {
|
||||||
throw new Error("Test execution stopped")
|
throw new Error("Test execution stopped")
|
||||||
|
|
@ -305,7 +341,9 @@ export class TestRunnerService extends Service {
|
||||||
request,
|
request,
|
||||||
options.keepVariableValues,
|
options.keepVariableValues,
|
||||||
inheritedVariables,
|
inheritedVariables,
|
||||||
initialEnvironmentState
|
initialEnvironmentState,
|
||||||
|
inheritedPreRequestScripts,
|
||||||
|
inheritedTestScripts
|
||||||
)
|
)
|
||||||
|
|
||||||
if (options.stopRef?.value) {
|
if (options.stopRef?.value) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import V8_VERSION from "./v/8"
|
||||||
import V9_VERSION from "./v/9"
|
import V9_VERSION from "./v/9"
|
||||||
import V10_VERSION from "./v/10"
|
import V10_VERSION from "./v/10"
|
||||||
import V11_VERSION from "./v/11"
|
import V11_VERSION from "./v/11"
|
||||||
|
import V12_VERSION from "./v/12"
|
||||||
|
|
||||||
export { CollectionVariable } from "./v/10"
|
export { CollectionVariable } from "./v/10"
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ const versionedObject = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const HoppCollection = createVersionedEntity({
|
export const HoppCollection = createVersionedEntity({
|
||||||
latestVersion: 11,
|
latestVersion: 12,
|
||||||
versionMap: {
|
versionMap: {
|
||||||
1: V1_VERSION,
|
1: V1_VERSION,
|
||||||
2: V2_VERSION,
|
2: V2_VERSION,
|
||||||
|
|
@ -37,6 +38,7 @@ export const HoppCollection = createVersionedEntity({
|
||||||
9: V9_VERSION,
|
9: V9_VERSION,
|
||||||
10: V10_VERSION,
|
10: V10_VERSION,
|
||||||
11: V11_VERSION,
|
11: V11_VERSION,
|
||||||
|
12: V12_VERSION,
|
||||||
},
|
},
|
||||||
getVersion(data) {
|
getVersion(data) {
|
||||||
const versionCheck = versionedObject.safeParse(data)
|
const versionCheck = versionedObject.safeParse(data)
|
||||||
|
|
@ -56,7 +58,7 @@ export type HoppCollectionVariable = InferredEntity<
|
||||||
typeof HoppCollection
|
typeof HoppCollection
|
||||||
>["variables"][number]
|
>["variables"][number]
|
||||||
|
|
||||||
export const CollectionSchemaVersion = 11
|
export const CollectionSchemaVersion = 12
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Collection object. This ignores the version number object
|
* 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 description = x.description ?? null
|
||||||
|
|
||||||
|
const preRequestScript = x.preRequestScript ?? ""
|
||||||
|
const testScript = x.testScript ?? ""
|
||||||
|
|
||||||
const obj = makeCollection({
|
const obj = makeCollection({
|
||||||
name,
|
name,
|
||||||
folders,
|
folders,
|
||||||
|
|
@ -96,6 +101,8 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
|
||||||
headers,
|
headers,
|
||||||
variables,
|
variables,
|
||||||
description,
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (x.id) obj.id = x.id
|
if (x.id) obj.id = x.id
|
||||||
|
|
@ -123,6 +130,9 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
|
||||||
|
|
||||||
const description = x.description ?? null
|
const description = x.description ?? null
|
||||||
|
|
||||||
|
const preRequestScript = x.preRequestScript ?? ""
|
||||||
|
const testScript = x.testScript ?? ""
|
||||||
|
|
||||||
const obj = makeCollection({
|
const obj = makeCollection({
|
||||||
name,
|
name,
|
||||||
folders,
|
folders,
|
||||||
|
|
@ -131,6 +141,8 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
|
||||||
headers,
|
headers,
|
||||||
variables,
|
variables,
|
||||||
description,
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (x.id) obj.id = x.id
|
if (x.id) obj.id = x.id
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { defineVersion, entityRefUptoVersion } from "verzod"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { HoppCollection } from ".."
|
import { HoppCollection } from ".."
|
||||||
import { v9_baseCollectionSchema } from "./9"
|
import { v9_baseCollectionSchema, V9_SCHEMA } from "./9"
|
||||||
|
|
||||||
export const CollectionVariable = z.object({
|
export const CollectionVariable = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
|
|
@ -33,7 +33,7 @@ export const V10_SCHEMA = v10_baseCollectionSchema.extend({
|
||||||
export default defineVersion({
|
export default defineVersion({
|
||||||
initial: false,
|
initial: false,
|
||||||
schema: V10_SCHEMA,
|
schema: V10_SCHEMA,
|
||||||
up(old: z.infer<typeof V10_SCHEMA>) {
|
up(old: z.infer<typeof V9_SCHEMA>) {
|
||||||
const result: z.infer<typeof V10_SCHEMA> = {
|
const result: z.infer<typeof V10_SCHEMA> = {
|
||||||
...old,
|
...old,
|
||||||
v: 10 as const,
|
v: 10 as const,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { defineVersion, entityRefUptoVersion } from "verzod"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { HoppCollection } from ".."
|
import { HoppCollection } from ".."
|
||||||
import { v10_baseCollectionSchema } from "./10"
|
import { v10_baseCollectionSchema, V10_SCHEMA } from "./10"
|
||||||
|
|
||||||
export const v11_baseCollectionSchema = v10_baseCollectionSchema.extend({
|
export const v11_baseCollectionSchema = v10_baseCollectionSchema.extend({
|
||||||
v: z.literal(11),
|
v: z.literal(11),
|
||||||
|
|
@ -24,11 +24,11 @@ export const V11_SCHEMA = v11_baseCollectionSchema.extend({
|
||||||
export default defineVersion({
|
export default defineVersion({
|
||||||
initial: false,
|
initial: false,
|
||||||
schema: V11_SCHEMA,
|
schema: V11_SCHEMA,
|
||||||
up(old: z.infer<typeof V11_SCHEMA>) {
|
up(old: z.infer<typeof V10_SCHEMA>) {
|
||||||
const result: z.infer<typeof V11_SCHEMA> = {
|
const result: z.infer<typeof V11_SCHEMA> = {
|
||||||
...old,
|
...old,
|
||||||
v: 11 as const,
|
v: 11 as const,
|
||||||
description: old.description ?? null,
|
description: null,
|
||||||
folders: old.folders.map((folder) => {
|
folders: old.folders.map((folder) => {
|
||||||
const result = HoppCollection.safeParseUpToVersion(folder, 11)
|
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({
|
export default defineVersion({
|
||||||
schema: V6_SCHEMA,
|
schema: V6_SCHEMA,
|
||||||
initial: false,
|
initial: false,
|
||||||
up(old: z.infer<typeof V6_SCHEMA>) {
|
up(old: z.infer<typeof V5_SCHEMA>) {
|
||||||
const headers = old.headers.map((header) => {
|
const headers = old.headers.map((header) => {
|
||||||
return {
|
return {
|
||||||
...header,
|
...header,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ export const V16_SCHEMA = V15_SCHEMA.extend({
|
||||||
const V16_VERSION = defineVersion({
|
const V16_VERSION = defineVersion({
|
||||||
schema: V16_SCHEMA,
|
schema: V16_SCHEMA,
|
||||||
initial: false,
|
initial: false,
|
||||||
up(old: z.infer<typeof V16_SCHEMA>) {
|
up(old: z.infer<typeof V15_SCHEMA>) {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
v: "16" as const,
|
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({
|
const V17_VERSION = defineVersion({
|
||||||
schema: V17_SCHEMA,
|
schema: V17_SCHEMA,
|
||||||
initial: false,
|
initial: false,
|
||||||
up(old: z.infer<typeof V17_SCHEMA>) {
|
up(old: z.infer<typeof V16_SCHEMA>) {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
v: "17" as const,
|
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
|
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
|
||||||
"use strict"
|
"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
|
// Sequential test execution promise chain
|
||||||
// Initialize with a resolved promise to start the chain
|
// Initialize with a resolved promise to start the chain
|
||||||
// Store on globalThis so pm.sendRequest() and test() can both access and modify it
|
// Store on globalThis so pm.sendRequest() and test() can both access and modify it
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,28 @@
|
||||||
;(inputs) => {
|
;(inputs) => {
|
||||||
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
|
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
|
||||||
"use strict"
|
"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 = {
|
globalThis.pw = {
|
||||||
env: {
|
env: {
|
||||||
get: (key) => convertMarkerToValue(inputs.envGet(key)),
|
get: (key) => convertMarkerToValue(inputs.envGet(key)),
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,11 @@ const createScriptingModule = (
|
||||||
type: ModuleType,
|
type: ModuleType,
|
||||||
bootstrapCode: string,
|
bootstrapCode: string,
|
||||||
config: ModuleConfig,
|
config: ModuleConfig,
|
||||||
captureHook?: { capture?: () => void; bootstrapError?: unknown }
|
captureHook?: {
|
||||||
|
capture?: () => void
|
||||||
|
bootstrapError?: unknown
|
||||||
|
scriptExecutionError?: { name: string; message: string; stack: string }
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
return defineCageModule((ctx) => {
|
return defineCageModule((ctx) => {
|
||||||
// Track test promises for keepAlive (only for post-request scripts)
|
// 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 sandboxInputsObj = defineSandboxObject(ctx, inputsObj)
|
||||||
|
|
||||||
const bootstrapResult = ctx.vm.callFunction(
|
const bootstrapResult = ctx.vm.callFunction(
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ const executePreRequestOnCage = async (
|
||||||
let finalRequest = request
|
let finalRequest = request
|
||||||
let finalCookies = cookies
|
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, [
|
const result = await cage.runCode(preRequestScript, [
|
||||||
...defaultModules({
|
...defaultModules({
|
||||||
|
|
@ -72,6 +76,16 @@ const executePreRequestOnCage = async (
|
||||||
return E.left(`Script execution failed: ${String(result.err)}`)
|
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) {
|
if (captureHook.capture) {
|
||||||
captureHook.capture()
|
captureHook.capture()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,11 @@ const executeTestOnCage = async (
|
||||||
let finalTestResults = testRunStack
|
let finalTestResults = testRunStack
|
||||||
const testPromises: Promise<void>[] = []
|
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, [
|
const result = await cage.runCode(testScript, [
|
||||||
...defaultModules({
|
...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) {
|
if (captureHook.capture) {
|
||||||
captureHook.capture()
|
captureHook.capture()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ const executePreRequestOnCage = async (
|
||||||
let finalRequest = request
|
let finalRequest = request
|
||||||
let finalCookies = cookies
|
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, [
|
const result = await cage.runCode(preRequestScript, [
|
||||||
...defaultModules({
|
...defaultModules({
|
||||||
|
|
@ -103,6 +107,16 @@ const executePreRequestOnCage = async (
|
||||||
return E.left(`Script execution failed: ${String(result.err)}`)
|
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) {
|
if (captureHook.capture) {
|
||||||
captureHook.capture()
|
captureHook.capture()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,11 @@ const executeTestOnCage = async (
|
||||||
let finalCookies = cookies
|
let finalCookies = cookies
|
||||||
const testPromises: Promise<void>[] = []
|
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, [
|
const result = await cage.runCode(testScript, [
|
||||||
...defaultModules({
|
...defaultModules({
|
||||||
|
|
@ -122,6 +126,16 @@ const executeTestOnCage = async (
|
||||||
await Promise.all(testPromises)
|
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) {
|
if (captureHook.capture) {
|
||||||
captureHook.capture()
|
captureHook.capture()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -79,6 +82,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
const res = await createGQLRootUserCollection(
|
const res = await createGQLRootUserCollection(
|
||||||
collection.name,
|
collection.name,
|
||||||
|
|
@ -98,6 +104,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = parentCollectionID
|
collection.id = parentCollectionID
|
||||||
|
|
@ -105,6 +114,9 @@ const recursivelySyncCollections = async (
|
||||||
collection.headers = returnedData.headers
|
collection.headers = returnedData.headers
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||||
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateGraphqlCollectionOrFolder(
|
removeDuplicateGraphqlCollectionOrFolder(
|
||||||
parentCollectionID,
|
parentCollectionID,
|
||||||
|
|
@ -124,6 +136,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await createGQLChildUserCollection(
|
const res = await createGQLChildUserCollection(
|
||||||
|
|
@ -145,6 +160,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = childCollectionId
|
collection.id = childCollectionId
|
||||||
|
|
@ -153,6 +171,9 @@ const recursivelySyncCollections = async (
|
||||||
parentCollectionID = childCollectionId
|
parentCollectionID = childCollectionId
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
|
||||||
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateGraphqlCollectionOrFolder(
|
removeDuplicateGraphqlCollectionOrFolder(
|
||||||
childCollectionId,
|
childCollectionId,
|
||||||
|
|
@ -268,6 +289,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
headers: collection.headers,
|
headers: collection.headers,
|
||||||
variables: collection.variables,
|
variables: collection.variables,
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionID) {
|
if (collectionID) {
|
||||||
|
|
@ -314,6 +338,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
headers: folder.headers,
|
headers: folder.headers,
|
||||||
variables: folder.variables,
|
variables: folder.variables,
|
||||||
_ref_id: folder._ref_id,
|
_ref_id: folder._ref_id,
|
||||||
|
description: folder.description ?? null,
|
||||||
|
preRequestScript: folder.preRequestScript ?? "",
|
||||||
|
testScript: folder.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderBackendId) {
|
if (folderBackendId) {
|
||||||
|
|
|
||||||
|
|
@ -141,12 +141,14 @@ export function exportedCollectionToHoppCollection(
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: restCollection.id,
|
id: restCollection.id,
|
||||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||||
v: 11,
|
v: 12,
|
||||||
name: restCollection.name,
|
name: restCollection.name,
|
||||||
folders: restCollection.folders.map((folder) =>
|
folders: restCollection.folders.map((folder) =>
|
||||||
exportedCollectionToHoppCollection(folder, collectionType)
|
exportedCollectionToHoppCollection(folder, collectionType)
|
||||||
|
|
@ -200,6 +202,8 @@ export function exportedCollectionToHoppCollection(
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const gqlCollection = collection as ExportedUserCollectionGQL
|
const gqlCollection = collection as ExportedUserCollectionGQL
|
||||||
|
|
@ -213,12 +217,14 @@ export function exportedCollectionToHoppCollection(
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: gqlCollection.id,
|
id: gqlCollection.id,
|
||||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||||
v: 11,
|
v: 12,
|
||||||
name: gqlCollection.name,
|
name: gqlCollection.name,
|
||||||
folders: gqlCollection.folders.map((folder) =>
|
folders: gqlCollection.folders.map((folder) =>
|
||||||
exportedCollectionToHoppCollection(folder, collectionType)
|
exportedCollectionToHoppCollection(folder, collectionType)
|
||||||
|
|
@ -248,6 +254,8 @@ export function exportedCollectionToHoppCollection(
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -398,6 +406,9 @@ function setupUserCollectionCreatedSubscription() {
|
||||||
headers: [],
|
headers: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
variables: [],
|
variables: [],
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
runDispatchWithOutSyncing(() => {
|
runDispatchWithOutSyncing(() => {
|
||||||
|
|
@ -406,23 +417,27 @@ function setupUserCollectionCreatedSubscription() {
|
||||||
name: res.right.userCollectionCreated.title,
|
name: res.right.userCollectionCreated.title,
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id: data._ref_id,
|
_ref_id: data._ref_id,
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
})
|
})
|
||||||
: addRESTCollection({
|
: addRESTCollection({
|
||||||
name: res.right.userCollectionCreated.title,
|
name: res.right.userCollectionCreated.title,
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id: data._ref_id,
|
_ref_id: data._ref_id,
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const localIndex = collectionStore.value.state.length - 1
|
const localIndex = collectionStore.value.state.length - 1
|
||||||
|
|
@ -625,7 +640,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Incoming data transformed to the respective internal representations
|
// Incoming data transformed to the respective internal representations
|
||||||
const { auth, headers, variables, description } =
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} =
|
||||||
data && data != "null"
|
data && data != "null"
|
||||||
? JSON.parse(data)
|
? JSON.parse(data)
|
||||||
: {
|
: {
|
||||||
|
|
@ -633,6 +655,8 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
// Duplicated collection will have a unique ref id
|
// Duplicated collection will have a unique ref id
|
||||||
const _ref_id = generateUniqueRefId("coll")
|
const _ref_id = generateUniqueRefId("coll")
|
||||||
|
|
@ -649,12 +673,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
name,
|
name,
|
||||||
folders,
|
folders,
|
||||||
requests,
|
requests,
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id,
|
_ref_id,
|
||||||
auth,
|
auth,
|
||||||
headers: addDescriptionField(headers),
|
headers: addDescriptionField(headers),
|
||||||
variables: variables ?? [],
|
variables: variables ?? [],
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// only folders will have parent collection id
|
// only folders will have parent collection id
|
||||||
|
|
@ -1122,7 +1148,14 @@ function transformDuplicatedCollections(
|
||||||
requests: userRequests,
|
requests: userRequests,
|
||||||
title: name,
|
title: name,
|
||||||
}) => {
|
}) => {
|
||||||
const { auth, headers, variables, description } =
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} =
|
||||||
data && data !== "null"
|
data && data !== "null"
|
||||||
? JSON.parse(data)
|
? JSON.parse(data)
|
||||||
: {
|
: {
|
||||||
|
|
@ -1130,6 +1163,8 @@ function transformDuplicatedCollections(
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ref_id = generateUniqueRefId("coll")
|
const _ref_id = generateUniqueRefId("coll")
|
||||||
|
|
@ -1144,11 +1179,13 @@ function transformDuplicatedCollections(
|
||||||
folders,
|
folders,
|
||||||
requests,
|
requests,
|
||||||
_ref_id,
|
_ref_id,
|
||||||
v: 11,
|
v: 12,
|
||||||
auth,
|
auth,
|
||||||
headers: addDescriptionField(headers),
|
headers: addDescriptionField(headers),
|
||||||
variables: variables ?? [],
|
variables: variables ?? [],
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -83,6 +85,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
const res = await createRESTRootUserCollection(
|
const res = await createRESTRootUserCollection(
|
||||||
collection.name,
|
collection.name,
|
||||||
|
|
@ -102,6 +106,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = parentCollectionID
|
collection.id = parentCollectionID
|
||||||
|
|
@ -110,6 +116,9 @@ const recursivelySyncCollections = async (
|
||||||
collection.headers = returnedData.headers
|
collection.headers = returnedData.headers
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection.description = returnedData.description ?? null
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
|
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
|
||||||
} else {
|
} else {
|
||||||
parentCollectionID = undefined
|
parentCollectionID = undefined
|
||||||
|
|
@ -125,6 +134,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await createRESTChildUserCollection(
|
const res = await createRESTChildUserCollection(
|
||||||
|
|
@ -147,6 +158,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = childCollectionId
|
collection.id = childCollectionId
|
||||||
|
|
@ -156,6 +169,8 @@ const recursivelySyncCollections = async (
|
||||||
parentCollectionID = childCollectionId
|
parentCollectionID = childCollectionId
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection.description = returnedData.description ?? null
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateRESTCollectionOrFolder(
|
removeDuplicateRESTCollectionOrFolder(
|
||||||
childCollectionId,
|
childCollectionId,
|
||||||
|
|
@ -268,6 +283,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
variables: collection.variables,
|
variables: collection.variables,
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionID) {
|
if (collectionID) {
|
||||||
|
|
@ -351,6 +368,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
variables: folder.variables,
|
variables: folder.variables,
|
||||||
_ref_id: folder._ref_id,
|
_ref_id: folder._ref_id,
|
||||||
description: folder.description ?? null,
|
description: folder.description ?? null,
|
||||||
|
preRequestScript: folder.preRequestScript ?? "",
|
||||||
|
testScript: folder.testScript ?? "",
|
||||||
}
|
}
|
||||||
if (folderID) {
|
if (folderID) {
|
||||||
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -79,6 +82,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
const res = await createGQLRootUserCollection(
|
const res = await createGQLRootUserCollection(
|
||||||
collection.name,
|
collection.name,
|
||||||
|
|
@ -98,6 +104,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = parentCollectionID
|
collection.id = parentCollectionID
|
||||||
|
|
@ -105,6 +114,9 @@ const recursivelySyncCollections = async (
|
||||||
collection.auth = returnedData.auth
|
collection.auth = returnedData.auth
|
||||||
collection.headers = returnedData.headers
|
collection.headers = returnedData.headers
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateGraphqlCollectionOrFolder(
|
removeDuplicateGraphqlCollectionOrFolder(
|
||||||
parentCollectionID,
|
parentCollectionID,
|
||||||
|
|
@ -124,6 +136,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: collection.headers ?? [],
|
headers: collection.headers ?? [],
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await createGQLChildUserCollection(
|
const res = await createGQLChildUserCollection(
|
||||||
|
|
@ -145,6 +160,9 @@ const recursivelySyncCollections = async (
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = childCollectionId
|
collection.id = childCollectionId
|
||||||
|
|
@ -153,6 +171,9 @@ const recursivelySyncCollections = async (
|
||||||
collection.headers = returnedData.headers
|
collection.headers = returnedData.headers
|
||||||
parentCollectionID = childCollectionId
|
parentCollectionID = childCollectionId
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateGraphqlCollectionOrFolder(
|
removeDuplicateGraphqlCollectionOrFolder(
|
||||||
childCollectionId,
|
childCollectionId,
|
||||||
|
|
@ -268,6 +289,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
headers: collection.headers,
|
headers: collection.headers,
|
||||||
variables: collection.variables,
|
variables: collection.variables,
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionID) {
|
if (collectionID) {
|
||||||
|
|
@ -314,6 +338,9 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
headers: folder.headers,
|
headers: folder.headers,
|
||||||
variables: folder.variables,
|
variables: folder.variables,
|
||||||
_ref_id: folder._ref_id,
|
_ref_id: folder._ref_id,
|
||||||
|
description: folder.description ?? null,
|
||||||
|
preRequestScript: folder.preRequestScript ?? "",
|
||||||
|
testScript: folder.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderBackendId) {
|
if (folderBackendId) {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ export function translateToPersonalCollectionFormat(x: HoppCollection) {
|
||||||
headers: x.headers,
|
headers: x.headers,
|
||||||
variables: x.variables,
|
variables: x.variables,
|
||||||
description: x.description,
|
description: x.description,
|
||||||
|
preRequestScript: x.preRequestScript ?? "",
|
||||||
|
testScript: x.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
|
|
|
||||||
|
|
@ -142,12 +142,14 @@ export function exportedCollectionToHoppCollection(
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: restCollection.id,
|
id: restCollection.id,
|
||||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||||
v: 11,
|
v: 12,
|
||||||
name: restCollection.name,
|
name: restCollection.name,
|
||||||
folders: restCollection.folders.map((folder) =>
|
folders: restCollection.folders.map((folder) =>
|
||||||
exportedCollectionToHoppCollection(folder, collectionType)
|
exportedCollectionToHoppCollection(folder, collectionType)
|
||||||
|
|
@ -201,6 +203,8 @@ export function exportedCollectionToHoppCollection(
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const gqlCollection = collection as ExportedUserCollectionGQL
|
const gqlCollection = collection as ExportedUserCollectionGQL
|
||||||
|
|
@ -219,7 +223,7 @@ export function exportedCollectionToHoppCollection(
|
||||||
return {
|
return {
|
||||||
id: gqlCollection.id,
|
id: gqlCollection.id,
|
||||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||||
v: 11,
|
v: 12,
|
||||||
name: gqlCollection.name,
|
name: gqlCollection.name,
|
||||||
folders: gqlCollection.folders.map((folder) =>
|
folders: gqlCollection.folders.map((folder) =>
|
||||||
exportedCollectionToHoppCollection(folder, collectionType)
|
exportedCollectionToHoppCollection(folder, collectionType)
|
||||||
|
|
@ -249,6 +253,8 @@ export function exportedCollectionToHoppCollection(
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -400,6 +406,8 @@ function setupUserCollectionCreatedSubscription() {
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
runDispatchWithOutSyncing(() => {
|
runDispatchWithOutSyncing(() => {
|
||||||
|
|
@ -408,23 +416,27 @@ function setupUserCollectionCreatedSubscription() {
|
||||||
name: res.right.userCollectionCreated.title,
|
name: res.right.userCollectionCreated.title,
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id: data._ref_id,
|
_ref_id: data._ref_id,
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
})
|
})
|
||||||
: addRESTCollection({
|
: addRESTCollection({
|
||||||
name: res.right.userCollectionCreated.title,
|
name: res.right.userCollectionCreated.title,
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id: data._ref_id,
|
_ref_id: data._ref_id,
|
||||||
auth: data.auth,
|
auth: data.auth,
|
||||||
headers: addDescriptionField(data.headers),
|
headers: addDescriptionField(data.headers),
|
||||||
variables: data.variables ?? [],
|
variables: data.variables ?? [],
|
||||||
description: data.description ?? null,
|
description: data.description ?? null,
|
||||||
|
preRequestScript: data.preRequestScript ?? "",
|
||||||
|
testScript: data.testScript ?? "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const localIndex = collectionStore.value.state.length - 1
|
const localIndex = collectionStore.value.state.length - 1
|
||||||
|
|
@ -627,7 +639,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Incoming data transformed to the respective internal representations
|
// Incoming data transformed to the respective internal representations
|
||||||
const { auth, headers, variables, description } =
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} =
|
||||||
data && data != "null"
|
data && data != "null"
|
||||||
? JSON.parse(data)
|
? JSON.parse(data)
|
||||||
: {
|
: {
|
||||||
|
|
@ -635,6 +654,8 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
// Duplicated collection will have a unique ref id
|
// Duplicated collection will have a unique ref id
|
||||||
const _ref_id = generateUniqueRefId("coll")
|
const _ref_id = generateUniqueRefId("coll")
|
||||||
|
|
@ -651,12 +672,14 @@ function setupUserCollectionDuplicatedSubscription() {
|
||||||
name,
|
name,
|
||||||
folders,
|
folders,
|
||||||
requests,
|
requests,
|
||||||
v: 11,
|
v: 12,
|
||||||
_ref_id,
|
_ref_id,
|
||||||
auth,
|
auth,
|
||||||
headers: addDescriptionField(headers),
|
headers: addDescriptionField(headers),
|
||||||
variables: variables ?? [],
|
variables: variables ?? [],
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// only folders will have parent collection id
|
// only folders will have parent collection id
|
||||||
|
|
@ -1122,7 +1145,14 @@ function transformDuplicatedCollections(
|
||||||
requests: userRequests,
|
requests: userRequests,
|
||||||
title: name,
|
title: name,
|
||||||
}) => {
|
}) => {
|
||||||
const { auth, headers, variables, description } =
|
const {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
variables,
|
||||||
|
description,
|
||||||
|
preRequestScript,
|
||||||
|
testScript,
|
||||||
|
} =
|
||||||
data && data !== "null"
|
data && data !== "null"
|
||||||
? JSON.parse(data)
|
? JSON.parse(data)
|
||||||
: {
|
: {
|
||||||
|
|
@ -1130,6 +1160,8 @@ function transformDuplicatedCollections(
|
||||||
headers: [],
|
headers: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ref_id = generateUniqueRefId("coll")
|
const _ref_id = generateUniqueRefId("coll")
|
||||||
|
|
@ -1144,11 +1176,13 @@ function transformDuplicatedCollections(
|
||||||
folders,
|
folders,
|
||||||
requests,
|
requests,
|
||||||
_ref_id,
|
_ref_id,
|
||||||
v: 11,
|
v: 12,
|
||||||
auth,
|
auth,
|
||||||
headers: addDescriptionField(headers),
|
headers: addDescriptionField(headers),
|
||||||
variables: variables ?? [],
|
variables: variables ?? [],
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
|
preRequestScript: preRequestScript ?? "",
|
||||||
|
testScript: testScript ?? "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ const transformCollectionForBackend = (collection: HoppCollection): any => {
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -83,6 +85,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
const res = await createRESTRootUserCollection(
|
const res = await createRESTRootUserCollection(
|
||||||
collection.name,
|
collection.name,
|
||||||
|
|
@ -102,6 +106,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = parentCollectionID
|
collection.id = parentCollectionID
|
||||||
|
|
@ -110,6 +116,8 @@ const recursivelySyncCollections = async (
|
||||||
collection.headers = returnedData.headers
|
collection.headers = returnedData.headers
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection.description = returnedData.description ?? null
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
removeDuplicateRESTCollectionOrFolder(
|
removeDuplicateRESTCollectionOrFolder(
|
||||||
parentCollectionID,
|
parentCollectionID,
|
||||||
`${collectionPath}`
|
`${collectionPath}`
|
||||||
|
|
@ -128,6 +136,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: collection.variables ?? [],
|
variables: collection.variables ?? [],
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await createRESTChildUserCollection(
|
const res = await createRESTChildUserCollection(
|
||||||
|
|
@ -150,6 +160,8 @@ const recursivelySyncCollections = async (
|
||||||
variables: [],
|
variables: [],
|
||||||
_ref_id: generateUniqueRefId("coll"),
|
_ref_id: generateUniqueRefId("coll"),
|
||||||
description: null,
|
description: null,
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.id = childCollectionId
|
collection.id = childCollectionId
|
||||||
|
|
@ -159,6 +171,8 @@ const recursivelySyncCollections = async (
|
||||||
parentCollectionID = childCollectionId
|
parentCollectionID = childCollectionId
|
||||||
collection.variables = returnedData.variables
|
collection.variables = returnedData.variables
|
||||||
collection.description = returnedData.description ?? null
|
collection.description = returnedData.description ?? null
|
||||||
|
collection.preRequestScript = returnedData.preRequestScript ?? ""
|
||||||
|
collection.testScript = returnedData.testScript ?? ""
|
||||||
|
|
||||||
removeDuplicateRESTCollectionOrFolder(
|
removeDuplicateRESTCollectionOrFolder(
|
||||||
childCollectionId,
|
childCollectionId,
|
||||||
|
|
@ -271,6 +285,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
variables: collection.variables,
|
variables: collection.variables,
|
||||||
_ref_id: collection._ref_id,
|
_ref_id: collection._ref_id,
|
||||||
description: collection.description ?? null,
|
description: collection.description ?? null,
|
||||||
|
preRequestScript: collection.preRequestScript ?? "",
|
||||||
|
testScript: collection.testScript ?? "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionID) {
|
if (collectionID) {
|
||||||
|
|
@ -355,6 +371,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
variables: folder.variables,
|
variables: folder.variables,
|
||||||
_ref_id: folder._ref_id,
|
_ref_id: folder._ref_id,
|
||||||
description: folder.description,
|
description: folder.description,
|
||||||
|
preRequestScript: folder.preRequestScript ?? "",
|
||||||
|
testScript: folder.testScript ?? "",
|
||||||
}
|
}
|
||||||
if (folderID) {
|
if (folderID) {
|
||||||
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
updateUserCollection(folderID, folderName, JSON.stringify(data))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue