Skip to content

Commit 9cd6c7d

Browse files
feat(scripting-revamp): chai powered assertions and postman compatibility layer (hoppscotch#5417)
Co-authored-by: nivedin <[email protected]>
1 parent ecf7d25 commit 9cd6c7d

File tree

90 files changed

+30774
-1397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+30774
-1397
lines changed

packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/scripting-revamp-coll.json

Lines changed: 548 additions & 4 deletions
Large diffs are not rendered by default.

packages/hoppscotch-cli/src/utils/pre-request.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,36 @@ export const preRequestScriptRunner = (
6868
const { selected, global } = updatedEnvs;
6969

7070
return {
71-
updatedEnvs: <Environment>{
71+
// Keep the original updatedEnvs with separate global and selected arrays
72+
preRequestUpdatedEnvs: updatedEnvs,
73+
// Create Environment format for getEffectiveRESTRequest
74+
envForEffectiveRequest: <Environment>{
7275
name: "Env",
7376
variables: [...(selected ?? []), ...(global ?? [])],
7477
},
7578
updatedRequest: updatedRequest ?? {},
7679
};
7780
}),
78-
TE.chainW(({ updatedEnvs, updatedRequest }) => {
81+
TE.chainW(({ preRequestUpdatedEnvs, envForEffectiveRequest, updatedRequest }) => {
7982
const finalRequest = { ...request, ...updatedRequest };
8083

8184
return TE.tryCatch(
82-
() =>
83-
getEffectiveRESTRequest(
85+
async () => {
86+
const result = await getEffectiveRESTRequest(
8487
finalRequest,
85-
updatedEnvs,
88+
envForEffectiveRequest,
8689
collectionVariables
87-
),
90+
);
91+
// Replace the updatedEnvs from getEffectiveRESTRequest with the one from pre-request script
92+
// This preserves the global/selected separation
93+
if (E.isRight(result)) {
94+
return E.right({
95+
...result.right,
96+
updatedEnvs: preRequestUpdatedEnvs,
97+
});
98+
}
99+
return result;
100+
},
88101
(reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason })
89102
);
90103
}),

packages/hoppscotch-common/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@
686686
"from_postman": "Import from Postman",
687687
"from_postman_description": "Import from Postman collection",
688688
"from_postman_import_summary": "Collections, Requests and response examples will be imported.",
689+
"import_scripts": "Import scripts",
690+
"import_scripts_description": "Supports Postman Collection v2.0/v2.1.",
689691
"from_url": "Import from URL",
690692
"gist_url": "Enter Gist URL",
691693
"from_har": "Import from HAR",
@@ -712,6 +714,9 @@
712714
"import_summary_pre_request_scripts_title": "Pre-request scripts",
713715
"import_summary_post_request_scripts_title": "Post request scripts",
714716
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now.",
717+
"import_summary_script_found": "script found but not imported",
718+
"import_summary_scripts_found": "scripts found but not imported",
719+
"import_summary_enable_experimental_sandbox": "To import Postman scripts, enable 'Experimental scripting sandbox' in settings. Note: This feature is experimental.",
715720
"cors_error_modal": {
716721
"title": "CORS Error Detected",
717722
"description": "The import failed due to CORS (Cross-Origin Resource Sharing) restrictions imposed by the server.",
@@ -1314,6 +1319,7 @@
13141319
"download_failed": "Download failed",
13151320
"download_started": "Download started",
13161321
"enabled": "Enabled",
1322+
"experimental": "Experimental",
13171323
"file_imported": "File imported",
13181324
"finished_in": "Finished in {duration} ms",
13191325
"hide": "Hide",

packages/hoppscotch-common/src/components/collections/ImportExport.vue

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
5959
import { TeamWorkspace } from "~/services/workspace.service"
6060
import { invokeAction } from "~/helpers/actions"
6161
62-
const isPostmanImporterInProgress = ref(false)
6362
const isInsomniaImporterInProgress = ref(false)
6463
const isOpenAPIImporterInProgress = ref(false)
6564
const isRESTImporterInProgress = ref(false)
@@ -171,6 +170,7 @@ const emit = defineEmits<{
171170
const isHoppMyCollectionExporterInProgress = ref(false)
172171
const isHoppTeamCollectionExporterInProgress = ref(false)
173172
const isHoppGistCollectionExporterInProgress = ref(false)
173+
const isPostmanImporterInProgress = ref(false)
174174
175175
const isTeamWorkspace = computed(() => {
176176
return props.collectionsType.type === "team-collections"
@@ -179,19 +179,83 @@ const isTeamWorkspace = computed(() => {
179179
const currentImportSummary: Ref<{
180180
showImportSummary: boolean
181181
importedCollections: HoppCollection[] | null
182+
scriptsImported?: boolean
183+
originalScriptCounts?: { preRequest: number; test: number }
182184
}> = ref({
183185
showImportSummary: false,
184186
importedCollections: null,
187+
scriptsImported: false,
188+
originalScriptCounts: undefined,
185189
})
186190
187-
const setCurrentImportSummary = (collections: HoppCollection[]) => {
191+
const setCurrentImportSummary = (
192+
collections: HoppCollection[],
193+
scriptsImported?: boolean,
194+
originalScriptCounts?: { preRequest: number; test: number }
195+
) => {
188196
currentImportSummary.value.importedCollections = collections
189197
currentImportSummary.value.showImportSummary = true
198+
currentImportSummary.value.scriptsImported = scriptsImported
199+
currentImportSummary.value.originalScriptCounts = originalScriptCounts
190200
}
191201
192202
const unsetCurrentImportSummary = () => {
193203
currentImportSummary.value.importedCollections = null
194204
currentImportSummary.value.showImportSummary = false
205+
currentImportSummary.value.scriptsImported = false
206+
currentImportSummary.value.originalScriptCounts = undefined
207+
}
208+
209+
// Count scripts in raw Postman collection JSON (before import strips them)
210+
const countPostmanScripts = (
211+
content: string[]
212+
): { preRequest: number; test: number } => {
213+
let preRequestCount = 0
214+
let testCount = 0
215+
216+
const countInItem = (item: any) => {
217+
// Only count if this is a request (has request object), not a folder
218+
const isRequest = item?.request !== undefined
219+
220+
if (isRequest && item?.event) {
221+
const prerequest = item.event.find((e: any) => e.listen === "prerequest")
222+
const test = item.event.find((e: any) => e.listen === "test")
223+
224+
if (
225+
prerequest?.script?.exec &&
226+
Array.isArray(prerequest.script.exec) &&
227+
prerequest.script.exec.some((line: string) => line?.trim())
228+
) {
229+
preRequestCount++
230+
}
231+
232+
if (
233+
test?.script?.exec &&
234+
Array.isArray(test.script.exec) &&
235+
test.script.exec.some((line: string) => line?.trim())
236+
) {
237+
testCount++
238+
}
239+
}
240+
241+
// Recursively count in nested items (folders)
242+
if (item?.item && Array.isArray(item.item)) {
243+
item.item.forEach(countInItem)
244+
}
245+
}
246+
247+
content.forEach((fileContent) => {
248+
try {
249+
const collection = JSON.parse(fileContent)
250+
if (collection?.item && Array.isArray(collection.item)) {
251+
collection.item.forEach(countInItem)
252+
}
253+
} catch (e) {
254+
// Invalid JSON, skip
255+
}
256+
})
257+
258+
return { preRequest: preRequestCount, test: testCount }
195259
}
196260
197261
const HoppRESTImporter: ImporterOrExporter = {
@@ -379,15 +443,20 @@ const HoppPostmanImporter: ImporterOrExporter = {
379443
caption: "import.from_file",
380444
acceptedFileTypes: ".json",
381445
description: "import.from_postman_import_summary",
382-
onImportFromFile: async (content) => {
446+
showPostmanScriptOption: true,
447+
onImportFromFile: async (content: string[], importScripts?: boolean) => {
383448
isPostmanImporterInProgress.value = true
384449
385-
const res = await hoppPostmanImporter(content)()
450+
// Count scripts from raw Postman JSON before importing
451+
const originalCounts =
452+
importScripts === undefined ? countPostmanScripts(content) : undefined
453+
454+
const res = await hoppPostmanImporter(content, importScripts ?? false)()
386455
387456
if (E.isRight(res)) {
388457
await handleImportToStore(res.right)
389458
390-
setCurrentImportSummary(res.right)
459+
setCurrentImportSummary(res.right, importScripts, originalCounts)
391460
392461
platform.analytics?.logEvent({
393462
platform: "rest",

packages/hoppscotch-common/src/components/importExport/Base.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ props.importerModules.forEach((importer) => {
202202
props: () => ({
203203
collections: importSummary.value.importedCollections,
204204
importFormat: importer.metadata.format,
205+
scriptsImported: importSummary.value.scriptsImported,
206+
originalScriptCounts: importSummary.value.originalScriptCounts,
205207
"on-close": () => {
206208
emit("hide-modal")
207209
},

packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,41 @@
5252
}}
5353
</template>
5454
</p>
55+
56+
<!-- Postman-specific: Script import checkbox (only use case so far) -->
57+
<div
58+
v-if="showPostmanScriptOption && experimentalScriptingEnabled"
59+
class="flex items-start space-x-3 px-1"
60+
>
61+
<HoppSmartCheckbox
62+
:on="importScripts"
63+
@change="importScripts = !importScripts"
64+
/>
65+
<label
66+
for="importScriptsCheckbox"
67+
class="cursor-pointer select-none text-secondary flex flex-col space-y-0.5"
68+
>
69+
<span class="font-semibold flex space-x-1">
70+
<span>
71+
{{ t("import.import_scripts") }}
72+
</span>
73+
<span class="text-tiny text-secondaryLight">
74+
({{ t("state.experimental") }})
75+
</span>
76+
</span>
77+
<span class="text-tiny text-secondaryLight">
78+
{{ t("import.import_scripts_description") }}</span
79+
>
80+
</label>
81+
</div>
82+
5583
<div>
5684
<HoppButtonPrimary
5785
:disabled="disableImportCTA"
5886
:label="t('import.title')"
5987
:loading="loading"
6088
class="w-full"
61-
@click="emit('importFromFile', fileContent)"
89+
@click="handleImport"
6290
/>
6391
</div>
6492
</div>
@@ -69,23 +97,32 @@ import { useI18n } from "@composables/i18n"
6997
import { useToast } from "@composables/toast"
7098
import { computed, ref } from "vue"
7199
import { platform } from "~/platform"
100+
import { useSetting } from "~/composables/settings"
72101
73102
const props = withDefaults(
74103
defineProps<{
75104
caption: string
76105
acceptedFileTypes: string
77106
loading?: boolean
78107
description?: string
108+
showPostmanScriptOption?: boolean
79109
}>(),
80110
{
81111
loading: false,
82112
description: undefined,
113+
showPostmanScriptOption: false,
83114
}
84115
)
85116
86117
const t = useI18n()
87118
const toast = useToast()
88119
120+
// Postman-specific: Script import state (only use case so far)
121+
const importScripts = ref(false)
122+
const experimentalScriptingEnabled = useSetting(
123+
"EXPERIMENTAL_SCRIPTING_SANDBOX"
124+
)
125+
89126
const ALLOWED_FILE_SIZE_LIMIT = platform.limits?.collectionImportSizeLimit ?? 10 // Default to 10 MB
90127
91128
const importFilesCount = ref(0)
@@ -97,7 +134,7 @@ const fileContent = ref<string[]>([])
97134
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
98135
99136
const emit = defineEmits<{
100-
(e: "importFromFile", content: string[]): void
137+
(e: "importFromFile", content: string[], ...additionalArgs: any[]): void
101138
}>()
102139
103140
// Disable the import CTA if no file is selected, the file size limit is exceeded, or during an import action indicated by the `isLoading` prop
@@ -106,6 +143,16 @@ const disableImportCTA = computed(
106143
!hasFile.value || showFileSizeLimitExceededWarning.value || props.loading
107144
)
108145
146+
const handleImport = () => {
147+
// If Postman script option is enabled AND experimental sandbox is enabled, pass the importScripts value
148+
// Otherwise, don't pass it (undefined) to indicate the feature wasn't available
149+
if (props.showPostmanScriptOption && experimentalScriptingEnabled.value) {
150+
emit("importFromFile", fileContent.value, importScripts.value)
151+
} else {
152+
emit("importFromFile", fileContent.value)
153+
}
154+
}
155+
109156
const onFileChange = async () => {
110157
// Reset the state on entering the handler to avoid any stale state
111158
if (showFileSizeLimitExceededWarning.value) {

0 commit comments

Comments
 (0)