From e5a608f96265f630d68e0a9cd9c2b2d3bcd90264 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:05:22 +0530 Subject: [PATCH] feat: add persistent environment variable handling in IPC events and Bru class (#5172) --- .../001-sanity-tests/001-home-screen.spec.ts | 2 +- .../001-persistent-env-test.spec.ts | 33 +++++++++ .../002-env-test-without-persistence.spec.ts | 40 +++++++++++ .../collection/bruno.json | 5 ++ .../collection/collection.bru | 0 .../collection/environments/Env.bru | 4 ++ .../collection/environments/Stage.bru | 3 + .../collection/persist-env-request.bru | 15 ++++ .../collection/request.bru | 15 ++++ .../init-user-data/preferences.json | 6 ++ .../src/providers/App/useIpcEvents.js | 7 +- .../ReduxStore/slices/collections/actions.js | 62 ++++++++++++++++ .../src/utils/codemirror/autocomplete.js | 3 +- .../bruno-electron/src/ipc/network/index.js | 20 ++++++ packages/bruno-js/src/bru.js | 18 ++++- .../bruno-js/src/runtime/script-runtime.js | 4 ++ packages/bruno-js/src/runtime/test-runtime.js | 1 + packages/bruno-js/src/runtime/vars-runtime.js | 1 + .../bruno-js/src/sandbox/quickjs/shims/bru.js | 4 +- packages/bruno-js/tests/runtime.spec.js | 71 +++++++++++++++++++ playwright/index.ts | 46 ++++++++++-- 21 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts create mode 100644 e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts create mode 100644 e2e-tests/persistent-env-tests/collection/bruno.json create mode 100644 e2e-tests/persistent-env-tests/collection/collection.bru create mode 100644 e2e-tests/persistent-env-tests/collection/environments/Env.bru create mode 100644 e2e-tests/persistent-env-tests/collection/environments/Stage.bru create mode 100644 e2e-tests/persistent-env-tests/collection/persist-env-request.bru create mode 100644 e2e-tests/persistent-env-tests/collection/request.bru create mode 100644 e2e-tests/persistent-env-tests/init-user-data/preferences.json diff --git a/e2e-tests/001-sanity-tests/001-home-screen.spec.ts b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts index d993fb7bc..326ff895c 100644 --- a/e2e-tests/001-sanity-tests/001-home-screen.spec.ts +++ b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts @@ -2,4 +2,4 @@ import { test, expect } from '../../playwright'; test('Check if the logo on top left is visible', async ({ page }) => { await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible(); -}); \ No newline at end of file +}); diff --git a/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts b/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts new file mode 100644 index 000000000..e319e178f --- /dev/null +++ b/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '../../playwright'; + +test.describe.serial('Persistent Environment Test', () => { + test.setTimeout(2 * 10 * 1000); + + test('add env using script', async ({ pageWithUserData: page, restartApp }) => { + await page.locator('#sidebar-collection-name').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByText('ping', { exact: true }).click(); + await page.getByText('No Environment').click(); + await page.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click(); + await page.locator('#send-request').getByRole('img').nth(2).click(); + await page.waitForTimeout(1000); + await page.locator('div').filter({ hasText: /^Env$/ }).nth(3).click(); + await page.getByText('Configure', { exact: true }).click(); + await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible(); + await page.getByText('×').click(); + + const newApp = await restartApp(); + const newPage = await newApp.firstWindow(); + await newPage.locator('#sidebar-collection-name').click(); + await newPage.getByRole('button', { name: 'Save' }).click(); + await newPage.getByText('ping', { exact: true }).click(); + await newPage.getByText('No Environment').click(); + await newPage.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click(); + await newPage.locator('div').filter({ hasText: /^Env$/ }).nth(3).click(); + await newPage.getByText('Configure', { exact: true }).click(); + await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible(); + await newPage.getByText('×').click(); + await newPage.waitForTimeout(1000); + await newPage.close(); + }); +}); diff --git a/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts b/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts new file mode 100644 index 000000000..4be151bb0 --- /dev/null +++ b/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '../../playwright'; + +test.describe.serial('Persistent Environment Test', () => { + test.setTimeout(2 * 10 * 1000); + + test('add env using script', async ({ pageWithUserData: page, restartApp }) => { + await page.locator('#sidebar-collection-name').click(); + await page.getByText('ping2', { exact: true }).click(); + await page.getByText('Env', { exact: true }).click(); + await page.getByText('Stage', { exact: true }).click(); + await page.locator('#send-request').getByRole('img').nth(2).click(); + await page.waitForTimeout(1000); + await page + .locator('div') + .filter({ hasText: /^Stage$/ }) + .nth(3) + .click(); + await page.getByText('Configure', { exact: true }).click(); + await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible(); + await page.getByText('×').click(); + + const newApp = await restartApp(); + const newPage = await newApp.firstWindow(); + await newPage.locator('#sidebar-collection-name').click(); + await newPage.getByRole('button', { name: 'Save' }).click(); + await newPage.getByText('ping2', { exact: true }).click(); + await newPage.getByText('No Environment').click(); + await newPage.getByText('Stage').click(); + await newPage + .locator('div') + .filter({ hasText: /^Stage$/ }) + .nth(3) + .click(); + await newPage.getByText('Configure', { exact: true }).click(); + await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).not.toBeVisible(); + await newPage.getByText('×').click(); + await newPage.waitForTimeout(1000); + await newPage.close(); + }); +}); diff --git a/e2e-tests/persistent-env-tests/collection/bruno.json b/e2e-tests/persistent-env-tests/collection/bruno.json new file mode 100644 index 000000000..fa729847c --- /dev/null +++ b/e2e-tests/persistent-env-tests/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "collection", + "type": "collection" +} \ No newline at end of file diff --git a/e2e-tests/persistent-env-tests/collection/collection.bru b/e2e-tests/persistent-env-tests/collection/collection.bru new file mode 100644 index 000000000..e69de29bb diff --git a/e2e-tests/persistent-env-tests/collection/environments/Env.bru b/e2e-tests/persistent-env-tests/collection/environments/Env.bru new file mode 100644 index 000000000..909243fd2 --- /dev/null +++ b/e2e-tests/persistent-env-tests/collection/environments/Env.bru @@ -0,0 +1,4 @@ +vars { + host: https://testbench-sanity.usebruno.com + persistent-env-test: persistent-env-test-value +} diff --git a/e2e-tests/persistent-env-tests/collection/environments/Stage.bru b/e2e-tests/persistent-env-tests/collection/environments/Stage.bru new file mode 100644 index 000000000..0b756aa68 --- /dev/null +++ b/e2e-tests/persistent-env-tests/collection/environments/Stage.bru @@ -0,0 +1,3 @@ +vars { + host: https://testbench-sanity.usebruno.com +} \ No newline at end of file diff --git a/e2e-tests/persistent-env-tests/collection/persist-env-request.bru b/e2e-tests/persistent-env-tests/collection/persist-env-request.bru new file mode 100644 index 000000000..eefb4e827 --- /dev/null +++ b/e2e-tests/persistent-env-tests/collection/persist-env-request.bru @@ -0,0 +1,15 @@ +meta { + name: ping2 + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:pre-request { + bru.setEnvVar("persistent-env-test", "persistent-env-test-value"); +} \ No newline at end of file diff --git a/e2e-tests/persistent-env-tests/collection/request.bru b/e2e-tests/persistent-env-tests/collection/request.bru new file mode 100644 index 000000000..9ae6899c5 --- /dev/null +++ b/e2e-tests/persistent-env-tests/collection/request.bru @@ -0,0 +1,15 @@ +meta { + name: ping + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:pre-request { + bru.setEnvVar("persistent-env-test", "persistent-env-test-value", { persist: true }); +} \ No newline at end of file diff --git a/e2e-tests/persistent-env-tests/init-user-data/preferences.json b/e2e-tests/persistent-env-tests/init-user-data/preferences.json new file mode 100644 index 000000000..f9c1fdc7e --- /dev/null +++ b/e2e-tests/persistent-env-tests/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": true, + "lastOpenedCollections": [ + "{{projectRoot}}/e2e-tests/persistent-env-tests/collection" + ] +} \ No newline at end of file diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 34b3e3d5d..3ad176bd4 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -19,7 +19,7 @@ import { runRequestEvent, scriptEnvironmentUpdateEvent } from 'providers/ReduxStore/slices/collections'; -import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot } from 'providers/ReduxStore/slices/collections/actions'; +import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -112,6 +112,10 @@ const useIpcEvents = () => { dispatch(scriptEnvironmentUpdateEvent(val)); }); + const removePersistentEnvVariablesUpdateListener = ipcRenderer.on('main:persistent-env-variables-update', (val) => { + dispatch(mergeAndPersistEnvironment(val)); + }); + const removeGlobalEnvironmentVariablesUpdateListener = ipcRenderer.on('main:global-environment-variables-update', (val) => { dispatch(globalEnvironmentsUpdateEvent(val)); }); @@ -204,6 +208,7 @@ const useIpcEvents = () => { removeSnapshotHydrationListener(); removeCollectionOauth2CredentialsUpdatesListener(); removeCollectionLoadingStateListener(); + removePersistentEnvVariablesUpdateListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 7e3a758d3..e581bfc97 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1050,6 +1050,68 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di }); }; +export const mergeAndPersistEnvironment = + ({ persistentEnvVariables, collectionUid }) => + (_dispatch, getState) => { + return new Promise((resolve, reject) => { + const state = getState(); + const collection = findCollectionByUid(state.collections.collections, collectionUid); + + if (!collection) { + return reject(new Error('Collection not found')); + } + + const environmentUid = collection.activeEnvironmentUid; + if (!environmentUid) { + return reject(new Error('No active environment found')); + } + + const collectionCopy = cloneDeep(collection); + const environment = findEnvironmentInCollection(collectionCopy, environmentUid); + if (!environment) { + return reject(new Error('Environment not found')); + } + + // Only proceed if there are persistent variables to save + if (!persistentEnvVariables || Object.keys(persistentEnvVariables).length === 0) { + return resolve(); + } + + let existingVars = environment.variables || []; + + let normalizedNewVars = Object.entries(persistentEnvVariables).map(([name, value]) => ({ + uid: uuid(), + name, + value, + type: 'text', + enabled: true, + secret: false + })); + + const merged = existingVars.map((v) => { + const found = normalizedNewVars.find((nv) => nv.name === v.name); + if (found) { + return { ...v, value: found.value }; + } + return v; + }); + normalizedNewVars.forEach((nv) => { + if (!merged.some((v) => v.name === nv.name)) { + merged.push(nv); + } + }); + + environment.variables = merged; + + const { ipcRenderer } = window; + environmentSchema + .validate(environment) + .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment)) + .then(resolve) + .catch(reject); + }); + }; + export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, getState) => { return new Promise((resolve, reject) => { const state = getState(); diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index c2d1ba9ba..c105caa63 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -60,7 +60,8 @@ const STATIC_API_HINTS = { 'bru.getEnvVar(key)', 'bru.getFolderVar(key)', 'bru.getCollectionVar(key)', - 'bru.setEnvVar(key,value)', + 'bru.setEnvVar(key, value)', + 'bru.setEnvVar(key, value, options)', 'bru.deleteEnvVar(key)', 'bru.hasVar(key)', 'bru.getVar(key)', diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 45e14b78c..40bbd7d5f 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -468,6 +468,11 @@ const registerNetworkIpc = (mainWindow) => { collectionUid }); + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: scriptResult.persistentEnvVariables, + collectionUid + }); + mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: scriptResult.globalEnvironmentVariables }); @@ -542,6 +547,11 @@ const registerNetworkIpc = (mainWindow) => { collectionUid }); + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: result.persistentEnvVariables, + collectionUid + }); + mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: result.globalEnvironmentVariables }); @@ -583,6 +593,11 @@ const registerNetworkIpc = (mainWindow) => { collectionUid }); + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: scriptResult.persistentEnvVariables, + collectionUid + }); + mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: scriptResult.globalEnvironmentVariables }); @@ -887,6 +902,11 @@ const registerNetworkIpc = (mainWindow) => { collectionUid }); + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: testResults.persistentEnvVariables, + collectionUid + }); + mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: testResults.globalEnvironmentVariables }); diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index c00ac9b55..c717166d6 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -18,7 +18,6 @@ class Bru { this.collectionPath = collectionPath; this.collectionName = collectionName; this.sendRequest = sendRequest; - this.cookies = { jar: () => { const cookieJar = createCookieJar(); @@ -62,6 +61,8 @@ class Bru { }; } }; + // Holds variables that are marked as persistent by scripts + this.persistentEnvVariables = {}; this.runner = { skipRequest: () => { this.skipRequest = true; @@ -119,12 +120,25 @@ class Bru { return this.interpolate(this.envVariables[key]); } - setEnvVar(key, value) { + setEnvVar(key, value, options = {}) { if (!key) { throw new Error('Creating a env variable without specifying a name is not allowed.'); } + // When persist is true, only string values are allowed + if (options?.persist && typeof value !== 'string') { + throw new Error(`Persistent environment variables must be strings. Received ${typeof value} for key "${key}".`); + } + this.envVariables[key] = value; + + if (options?.persist) { + this.persistentEnvVariables[key] = value + } else { + if (this.persistentEnvVariables[key]) { + delete this.persistentEnvVariables[key]; + } + } } deleteEnvVar(key) { diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2f3e8ef9f..7a9b1a5bc 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -122,6 +122,7 @@ class ScriptRuntime { request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), + persistentEnvVariables: bru.persistentEnvVariables, globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, @@ -177,6 +178,7 @@ class ScriptRuntime { request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), + persistentEnvVariables: bru.persistentEnvVariables, globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, @@ -268,6 +270,7 @@ class ScriptRuntime { return { response, envVariables: cleanJson(envVariables), + persistentEnvVariables: cleanJson(bru.persistentEnvVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), results: cleanJson(__brunoTestResults.getResults()), @@ -323,6 +326,7 @@ class ScriptRuntime { return { response, envVariables: cleanJson(envVariables), + persistentEnvVariables: cleanJson(bru.persistentEnvVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), results: cleanJson(__brunoTestResults.getResults()), diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index 2088a5cb7..ae4e2c963 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -184,6 +184,7 @@ class TestRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + persistentEnvVariables: cleanJson(bru.persistentEnvVariables), results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest }; diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js index fba98e0fe..05f502c2d 100644 --- a/packages/bruno-js/src/runtime/vars-runtime.js +++ b/packages/bruno-js/src/runtime/vars-runtime.js @@ -74,6 +74,7 @@ class VarsRuntime { envVariables, runtimeVariables, globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + persistentEnvVariables: cleanJson(bru.persistentEnvVariables), error }; } diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index d99aec94b..4a67a80f8 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -47,8 +47,8 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getEnvVar', getEnvVar); getEnvVar.dispose(); - let setEnvVar = vm.newFunction('setEnvVar', function (key, value) { - bru.setEnvVar(vm.dump(key), vm.dump(value)); + let setEnvVar = vm.newFunction('setEnvVar', function (key, value, options = {}) { + bru.setEnvVar(vm.dump(key), vm.dump(value), vm.dump(options)); }); vm.setProp(bruObject, 'setEnvVar', setEnvVar); setEnvVar.dispose(); diff --git a/packages/bruno-js/tests/runtime.spec.js b/packages/bruno-js/tests/runtime.spec.js index 766569d03..64f0d2589 100644 --- a/packages/bruno-js/tests/runtime.spec.js +++ b/packages/bruno-js/tests/runtime.spec.js @@ -1,6 +1,8 @@ const { describe, it, expect } = require('@jest/globals'); const TestRuntime = require('../src/runtime/test-runtime'); const ScriptRuntime = require('../src/runtime/script-runtime'); +const Bru = require('../src/bru'); +const VarsRuntime = require('../src/runtime/vars-runtime'); describe('runtime', () => { describe('test-runtime', () => { @@ -175,4 +177,73 @@ describe('runtime', () => { }); }); }); + + describe('persistent environment variables validation', () => { + it('should throw error when trying to persist non-string values', async () => { + const script = `bru.setEnvVar('number', 42, {persist: true});`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env)) + .rejects.toThrow('Persistent environment variables must be strings. Received number for key "number".'); + }); + + it('should throw error when trying to persist boolean values', async () => { + const script = `bru.setEnvVar('isActive', true, {persist: true});`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env)) + .rejects.toThrow('Persistent environment variables must be strings. Received boolean for key "isActive".'); + }); + + it('should throw error when trying to persist object values', async () => { + const script = `bru.setEnvVar('config', {port: 3000}, {persist: true});`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env)) + .rejects.toThrow('Persistent environment variables must be strings. Received object for key "config".'); + }); + + it('should throw error when trying to persist array values', async () => { + const script = `bru.setEnvVar('items', ['item1', 'item2'], {persist: true});`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env)) + .rejects.toThrow('Persistent environment variables must be strings. Received object for key "items".'); + }); + + it('should allow string values when persist is true', async () => { + const script = `bru.setEnvVar('api_key', 'abc123', {persist: true});`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env); + + expect(result.envVariables.api_key).toBe('abc123'); + }); + + it('should allow non-string values when persist is false', async () => { + const script = ` + bru.setEnvVar('number', 42, {persist: false}); + bru.setEnvVar('boolean', true, {persist: false}); + bru.setEnvVar('object', {key: 'value'}, {persist: false}); + bru.setEnvVar('array', [1, 2, 3], {persist: false}); + `; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env); + + expect(result.envVariables.number).toBe(42); + expect(result.envVariables.boolean).toBe(true); + expect(result.envVariables.object).toEqual({key: 'value'}); + expect(result.envVariables.array).toEqual([1, 2, 3]); + }); + + it('should allow non-string values when persist is not specified', async () => { + const script = `bru.setEnvVar('number', 42);`; + const runtime = new ScriptRuntime({ runtime: 'vm2' }); + + const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env); + + expect(result.envVariables.number).toBe(42); + }); + }); }); diff --git a/playwright/index.ts b/playwright/index.ts index 555ea9551..a17809da4 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -11,6 +11,7 @@ export const test = baseTest.extend< page: Page; newPage: Page; pageWithUserData: Page; + restartApp: (options?: { initUserDataPath?: string }) => Promise; }, { createTmpDir: (tag?: string) => Promise; @@ -67,17 +68,22 @@ export const test = baseTest.extend< args: [electronAppPath], env: { ...process.env, - ELECTRON_USER_DATA_PATH: userDataPath, + ELECTRON_USER_DATA_PATH: userDataPath } }); const { workerIndex } = workerInfo; - app.process()?.stdout?.on('data', (data) => { - process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); - }); - app.process()?.stderr?.on('data', (error) => { - process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); - }); + const electronProcess = app.process(); + if (electronProcess?.stdout) { + electronProcess.stdout.on('data', (data) => { + process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); + }); + } + if (electronProcess?.stderr) { + electronProcess.stderr.on('data', (error) => { + process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); + }); + } apps.push(app); return app; @@ -157,6 +163,32 @@ export const test = baseTest.extend< { scope: 'worker' } ], + restartApp: async ({ launchElectronApp }, use, testInfo) => { + const appInstances: Array<{ app: ElectronApplication; initUserDataPath?: string }> = []; + await use(async ({ initUserDataPath } = {}) => { + // Get the test directory and check for init-user-data folder + const testDir = path.dirname(testInfo.file); + const defaultInitUserDataPath = path.join(testDir, 'init-user-data'); + + // Use provided initUserDataPath, or check if default path exists, or use undefined + let userDataPath = initUserDataPath; + if (!userDataPath) { + const hasInitUserData = await fs.promises.stat(defaultInitUserDataPath).catch(() => false); + userDataPath = hasInitUserData ? defaultInitUserDataPath : undefined; + } + + const app = await launchElectronApp({ initUserDataPath: userDataPath }); + appInstances.push({ app, initUserDataPath: userDataPath }); + return app; + }); + + // Clean up all app instances + for (const { app } of appInstances) { + await app.context().close(); + await app.close(); + } + }, + pageWithUserData: async ({ reuseOrLaunchElectronApp }, use, testInfo) => { const testDir = path.dirname(testInfo.file); const initUserDataPath = path.join(testDir, 'init-user-data');