Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/browser-mode/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

describe('browser mode - env', () => {
it('should inject env into browser runtime via process shim', async () => {
it('should inject env into browser runtime without process shim', async () => {
const { expectExecSuccess } = await runRstestCli({
command: 'rstest',
args: ['run'],
Expand Down
4 changes: 2 additions & 2 deletions e2e/browser-mode/fixtures/env/rstest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export default defineConfig({
env: {
RSTEST_E2E_ENV_FOO: 'bar',
RSTEST_E2E_ENV_EMPTY: '',
// In browser mode, env is proxied into a process shim.
// Setting a key to undefined should remove it from process.env.
// In browser mode, env is injected into runtime env store.
// Setting a key to undefined should remove it from the store.
RSTEST_E2E_ENV_UNSET: undefined,
},
});
26 changes: 23 additions & 3 deletions e2e/browser-mode/fixtures/env/tests/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import { describe, expect, it } from '@rstest/core';
import { describe, expect, it, rstest } from '@rstest/core';

describe('browser env injection', () => {
it('should expose process.env in browser and apply env changes', () => {
it('should apply env changes without injecting global process', () => {
// Browser client ensures global alias exists for libraries expecting Node globals.
expect((globalThis as any).global).toBe(globalThis);

expect(typeof (globalThis as any).process).toBe('object');
expect((globalThis as any).process).toBeUndefined();
expect((globalThis as any).__RSTEST_ENV__).toBeUndefined();
expect(Object.hasOwn(globalThis, '__RSTEST_ENV__')).toBe(false);

expect(process.env.RSTEST_E2E_ENV_FOO).toBe('bar');
expect(process.env.RSTEST_E2E_ENV_EMPTY).toBe('');
expect(process.env.RSTEST_E2E_ENV_UNSET).toBeUndefined();
expect(Object.hasOwn(process.env, 'RSTEST_E2E_ENV_UNSET')).toBe(false);

const originalFoo = process.env.RSTEST_E2E_ENV_FOO;

rstest.stubEnv('RSTEST_E2E_ENV_FOO', 'changed');
rstest.stubEnv('RSTEST_E2E_ENV_DYNAMIC', 'dynamic');

expect(process.env.RSTEST_E2E_ENV_FOO).toBe('changed');
expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBe('dynamic');

rstest.stubEnv('RSTEST_E2E_ENV_DYNAMIC', undefined);

expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBeUndefined();

rstest.unstubAllEnvs();

expect(process.env.RSTEST_E2E_ENV_FOO).toBe(originalFoo);
expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBeUndefined();
});
});
53 changes: 27 additions & 26 deletions packages/browser/src/client/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ const debugLog = (...args: unknown[]): void => {
}
};

type GlobalWithProcess = typeof globalThis & {
global?: typeof globalThis;
process?: NodeJS.Process;
};
type RuntimeEnvStore = Record<string, string | undefined>;
const RSTEST_ENV_SYMBOL = Symbol.for('rstest.env');

type GlobalWithRuntimeEnv = typeof globalThis &
Record<symbol, unknown> & {
global?: typeof globalThis;
};

const REGEXP_FLAG_PREFIX = 'RSTEST_REGEXP:';

Expand Down Expand Up @@ -94,36 +97,34 @@ const restoreRuntimeConfig = (
};
};

const ensureProcessEnv = (env: RuntimeConfig['env'] | undefined): void => {
const globalRef = globalThis as GlobalWithProcess;
const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => {
const globalRef = globalThis as GlobalWithRuntimeEnv;
if (!globalRef.global) {
globalRef.global = globalRef;
}

if (!globalRef.process) {
const processShim: Partial<NodeJS.Process> & {
env: Record<string, string | undefined>;
} = {
env: {},
argv: [],
version: 'browser',
cwd: () => '/',
platform: 'linux',
nextTick: (cb: (...args: unknown[]) => void, ...args: unknown[]) =>
queueMicrotask(() => cb(...args)),
};

globalRef.process = processShim as unknown as NodeJS.Process;
const existingEnv = globalRef[RSTEST_ENV_SYMBOL];
let runtimeEnv: RuntimeEnvStore;
if (existingEnv && typeof existingEnv === 'object') {
runtimeEnv = existingEnv as RuntimeEnvStore;
} else {
runtimeEnv = {};
globalRef[RSTEST_ENV_SYMBOL] = runtimeEnv;
}

globalRef.process.env ??= {};

if (env) {
for (const [key, value] of Object.entries(env)) {
if (value === undefined) {
delete globalRef.process.env[key];
const normalizedValue =
typeof value === 'string'
? value
: value == null
? undefined
: String(value);

if (normalizedValue === undefined) {
delete runtimeEnv[key];
} else {
globalRef.process.env[key] = value;
runtimeEnv[key] = normalizedValue;
}
}
}
Expand Down Expand Up @@ -448,7 +449,7 @@ const run = async () => {
}

const runtimeConfig = restoreRuntimeConfig(projectRuntime.runtimeConfig);
ensureProcessEnv(runtimeConfig.env);
ensureRuntimeEnv(runtimeConfig.env);

// Get this project's setup loaders and test context
const currentSetupLoaders =
Expand Down
6 changes: 6 additions & 0 deletions packages/browser/src/hostController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,12 @@ const createBrowserRuntime = async ({
resolve: {
alias: rstestInternalAliases,
},
source: {
define: {
'process.env': 'globalThis[Symbol.for("rstest.env")]',
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
},
},
output: {
target: 'web',
// Enable source map for inline snapshot support
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const applyCommonOptions = (cli: CAC) => {
)
.option(
'--unstubEnvs',
'Restores all `process.env` values that were changed with `rstest.stubEnv` before every test',
'Restores all runtime env values that were changed with `rstest.stubEnv` before every test',
)
.option(
'--includeTaskLocation',
Expand Down
38 changes: 31 additions & 7 deletions packages/core/src/runtime/api/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { initSpy } from './spy';
export const createRstestUtilities: (
workerState: WorkerState,
) => Promise<RstestUtilities> = async (workerState) => {
type RuntimeEnvStore = Record<string, string | undefined>;
const RSTEST_ENV_SYMBOL = Symbol.for('rstest.env');
type GlobalWithRuntimeEnv = typeof globalThis & Record<symbol, unknown>;

const originalEnvValues = new Map<string, string | undefined>();
const originalGlobalValues = new Map<
string | symbol | number,
Expand All @@ -21,6 +25,22 @@ export const createRstestUtilities: (

let originalConfig: undefined | RuntimeConfig;

const resolveRuntimeEnv = (): RuntimeEnvStore => {
const globalRef = globalThis as GlobalWithRuntimeEnv;
const runtimeEnv = globalRef[RSTEST_ENV_SYMBOL];
if (runtimeEnv && typeof runtimeEnv === 'object') {
return runtimeEnv as RuntimeEnvStore;
}

if (typeof process !== 'undefined' && process.env) {
return process.env;
}

const createdEnv: RuntimeEnvStore = {};
globalRef[RSTEST_ENV_SYMBOL] = createdEnv;
return createdEnv;
};

const timers = () => {
if (!_timers) {
_timers = new FakeTimers({
Expand Down Expand Up @@ -155,26 +175,30 @@ export const createRstestUtilities: (
},

stubEnv: (name: string, value: string | undefined): RstestUtilities => {
const runtimeEnv = resolveRuntimeEnv();

if (!originalEnvValues.has(name)) {
originalEnvValues.set(name, process.env[name]);
originalEnvValues.set(name, runtimeEnv[name]);
}

// update process.env
// update runtime env store
if (value === undefined) {
delete process.env[name];
delete runtimeEnv[name];
} else {
process.env[name] = value;
runtimeEnv[name] = value;
}

return rstest;
},
unstubAllEnvs: (): RstestUtilities => {
// restore process.env
const runtimeEnv = resolveRuntimeEnv();

// restore runtime env store
for (const [name, value] of originalEnvValues) {
if (value === undefined) {
delete process.env[name];
delete runtimeEnv[name];
} else {
process.env[name] = value;
runtimeEnv[name] = value;
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export interface RstestConfig {
*/
unstubGlobals?: boolean;
/**
* Restores all `process.env` values that were changed with `rstest.stubEnv` before every test.
* Restores all runtime env values that were changed with `rstest.stubEnv` before every test.
* @default false
*/
unstubEnvs?: boolean;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/types/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,12 +469,13 @@ export interface RstestUtilities {
resetModules: () => RstestUtilities;

/**
* Changes the value of environmental variable on `process.env`.
* Changes the value of an environment variable in the current runtime env store.
* Uses `process.env` in Node.js and runtime env store in browser mode.
*/
stubEnv: (name: string, value: string | undefined) => RstestUtilities;

/**
* Restores all `process.env` values that were changed with `rstest.stubEnv`.
* Restores all env values that were changed with `rstest.stubEnv`.
*/
unstubAllEnvs: () => RstestUtilities;

Expand Down
Loading