feat: Import Insomnia environments (#5716)

* feat: Implement environment conversion utilities for Insomnia to Bruno migration

fix tests

fix: test

feat: updated `toBrunoEnv` and merging functions to flatten environment data using dot-notation keys. added tests for `buildV5Environments` and `buildV4Environments` to verify flattened key behavior and shallow overrides.

chore: update package-lock.json

refactor: replace `flat` library with custom `flattenObject` utility for improved environment data flattening

chore: remove package-lock.json updates

feat: update `toBrunoEnv` to convert environment values to strings and adjust tests for flattened key behavior in Insomnia environment imports

refactor: update flattening logic to use JavaScript-style square bracket notation for arrays and adjust related tests

feat: enhance insomnia-to-bruno conversion by normalizing variables in requests, and add tests for v4 and v5 environment imports

refactor: improve variable naming and streamline environment building logic in `buildV5Environments` and `buildV4Environments` functions

test: add cleanup step to environment import tests and update expected version for new feature

* revert package-lock.json changes

* test: Add data-testid attributes to environment variable rows in EnvironmentVariables component
This commit is contained in:
Sanjai Kumar
2025-10-29 19:04:09 +05:30
committed by GitHub
parent cc7f1ea58f
commit e68b2ae3b7
13 changed files with 962 additions and 15 deletions

View File

@@ -185,7 +185,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"

View File

@@ -125,7 +125,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"

View File

@@ -0,0 +1,90 @@
import { uuid } from '../common';
import { flattenObject } from '../utils/flatten';
/**
* Converts an Insomnia environment node into a Bruno environment using JSON-path-like keys.
* - Flattens env.data to dot-notation keys; values are converted to strings.
*/
export const toBrunoEnv = (env, index = 0) => {
const variables = [];
const flatEnvData = flattenObject(env?.data || {});
Object.entries(flatEnvData).forEach(([name, value]) => {
variables.push({
uid: uuid(),
name,
value: String(value),
type: 'text',
enabled: true,
secret: false
});
});
return {
uid: uuid(),
name: (env?.name && String(env.name).trim()) || `Environment ${index + 1}`,
variables
};
};
/**
* Shallowly merges two flattened env data objects.
* - Keys in override replace keys in base.
* - No recursive merging.
*/
const shallowMergeFlat = (baseFlat = {}, overrideFlat = {}) => ({ ...baseFlat, ...overrideFlat });
/**
* Builds Bruno environments from Insomnia v5 environments.
* - Expects a single object (base env) with optional subEnvironments.
* - Creates one env for base and one env per sub using flattened, shallow-merged keys.
*/
export const buildV5Environments = (baseEnv) => {
if (!baseEnv || typeof baseEnv !== 'object') return [];
const result = [];
// include base as standalone
result.push(toBrunoEnv(baseEnv));
const subs = Array.isArray(baseEnv.subEnvironments) ? baseEnv.subEnvironments : [];
const baseFlat = flattenObject(baseEnv?.data || {});
subs.forEach((sub, i) => {
const subFlat = flattenObject(sub?.data || {});
const mergedFlat = shallowMergeFlat(baseFlat, subFlat);
result.push(toBrunoEnv({ name: sub?.name, data: mergedFlat }, i + 1));
});
return result;
};
/**
* Builds Bruno environments from Insomnia v4 resources.
* - Base env: parentId equals workspaceId; included as-is (flattened).
* - Sub envs: merge base (flattened) with sub (flattened) and import.
*
* Note: Insomnia supports only ONE base environment per workspace.
*/
export const buildV4Environments = (resources, workspaceId) => {
const allEnvResources = resources.filter((r) => r._type === 'environment') || [];
const envById = {};
allEnvResources.forEach((e) => (envById[e._id] = e));
const isBaseEnv = (env) => env.parentId === workspaceId;
const result = [];
const baseEnv = allEnvResources.find(isBaseEnv);
if (baseEnv) {
result.push(toBrunoEnv(baseEnv));
}
// sub envs - all inherit from the single base environment
const subEnvs = allEnvResources.filter((e) => !isBaseEnv(e));
const baseFlat = flattenObject(baseEnv?.data || {});
subEnvs.forEach((sub, idx) => {
const subFlat = flattenObject(sub.data || {});
const mergedFlat = shallowMergeFlat(baseFlat, subFlat);
result.push(toBrunoEnv({ name: sub.name, data: mergedFlat }, idx + 1));
});
return result;
};

View File

@@ -2,13 +2,14 @@ import each from 'lodash/each';
import get from 'lodash/get';
import jsyaml from 'js-yaml';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
import { buildV5Environments, buildV4Environments } from './env-utils';
const parseGraphQL = (text) => {
try {
const graphql = JSON.parse(text);
return {
query: graphql.query,
query: normalizeVariables(graphql.query),
variables: JSON.stringify(graphql.variables, null, 2)
};
} catch (e) {
@@ -49,7 +50,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
name,
type: 'http-request',
request: {
url: request.url,
url: normalizeVariables(request.url),
method: request.method,
auth: {
mode: 'none',
@@ -74,7 +75,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.headers.push({
uid: uuid(),
name: header.name,
value: header.value,
value: normalizeVariables(header.value),
description: header.description,
enabled: !header.disabled
});
@@ -84,7 +85,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
type: 'query',
enabled: !param.disabled
@@ -95,7 +96,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: '',
type: 'path',
enabled: true
@@ -121,14 +122,14 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
if (mimeType === 'application/json') {
brunoRequestItem.request.body.mode = 'json';
brunoRequestItem.request.body.json = request.body.text;
brunoRequestItem.request.body.json = normalizeVariables(request.body.text);
} else if (mimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(request.body.params, (param) => {
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
enabled: !param.disabled
});
@@ -140,17 +141,17 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
uid: uuid(),
type: 'text',
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
enabled: !param.disabled
});
});
} else if (mimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = request.body.text;
brunoRequestItem.request.body.text = normalizeVariables(request.body.text);
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = request.body.text;
brunoRequestItem.request.body.xml = normalizeVariables(request.body.text);
} else if (mimeType === 'application/graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
@@ -229,7 +230,7 @@ const parseInsomniaV5Collection = (data) => {
// Parse environments if available
if (data.environments) {
// Handle environments implementation if needed
brunoCollection.environments = buildV5Environments(data.environments);
}
return brunoCollection;
@@ -287,6 +288,9 @@ const parseInsomniaCollection = (data) => {
}
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);
// Build environments from resources
brunoCollection.environments = buildV4Environments(insomniaResources, insomniaCollection._id);
return brunoCollection;
} catch (err) {
console.error('Error parsing collection:', err);

View File

@@ -0,0 +1,51 @@
// Adapted from flat library by Hugh Kennedy (https://github.com/hughsk/flat)
// MIT License
/**
* Recursively flattens a nested object or array into a flat object with JavaScript-style keys.
* Arrays use square bracket notation (e.g., items[0].id).
* Only primitives and null are included as values.
*
* @param {object|array} obj - The object or array to flatten.
* @param {string} [prefix] - Used internally for recursion to build the path.
* @returns {object} A flat object with JavaScript-style keys.
*/
function flattenObject(obj, prefix = '') {
// Store the final flat result
const result = {};
/**
* Internal recursive function to process each value.
* @param {*} value - The current value (can be object, array, primitive, or null)
* @param {string} path - The JavaScript-style key up to this point
*/
function step(value, path) {
// If value is a primitive (string, number, boolean) or null, add it to the result
if (value === null || typeof value !== 'object') {
result[path] = value;
return;
}
// If value is an array, iterate over each item by index
if (Array.isArray(value)) {
value.forEach((item, idx) => {
// Build the next path with array index using square brackets (e.g. "items[0]")
step(item, path ? `${path}[${idx}]` : `[${idx}]`);
});
} else {
// If value is an object, iterate over its keys
Object.entries(value).forEach(([key, val]) => {
// Build the next path with object key (e.g. "user.name")
step(val, path ? `${path}.${key}` : key);
});
}
}
// Start recursive flattening from the root object
step(obj, prefix);
// Return the flat result object
return result;
}
export { flattenObject };

View File

@@ -0,0 +1,100 @@
import { describe, it, expect } from '@jest/globals';
import { buildV5Environments, buildV4Environments } from '../../src/insomnia/env-utils';
const getVar = (env, name) => {
return env.variables.find((v) => v.name === name);
};
describe('env-utils', () => {
describe('buildV5Environments', () => {
it('creates base and sub environments with flattened keys and shallow overrides', () => {
const environmentsNode = {
name: 'Base',
data: {
baseurl: 'https://api.example.com',
nested: { name: 'alice', roles: ['admin'] },
numbers: [1, 2]
},
subEnvironments: [
{
name: 'Staging',
data: {
baseurl: 'https://staging.example.com',
nested: { name: 'bob' }
}
},
{ name: 'Dev', data: {} }
]
};
const envs = buildV5Environments(environmentsNode);
expect(envs.length).toBe(3);
const base = envs[0];
const staging = envs[1];
const dev = envs[2];
expect(base.name).toBe('Base');
expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(base, 'nested.name')?.value).toBe('alice');
expect(getVar(base, 'nested.roles[0]')?.value).toBe('admin');
expect(getVar(base, 'numbers[1]')?.value).toBe('2');
expect(staging.name).toBe('Staging');
// baseurl overridden in sub
expect(getVar(staging, 'baseurl')?.value).toBe('https://staging.example.com');
// nested.name overridden, nested array preserved from base
expect(getVar(staging, 'nested.name')?.value).toBe('bob');
expect(getVar(staging, 'nested.roles[0]')?.value).toBe('admin');
expect(dev.name).toBe('Dev');
// no sub data => inherits base
expect(getVar(dev, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(dev, 'nested.name')?.value).toBe('alice');
});
});
describe('buildV4Environments', () => {
it('merges nearest base and sub env data (flattened) into standalone Bruno envs', () => {
const workspaceId = 'wrk_1';
const resources = [
{ _id: workspaceId, _type: 'workspace', name: 'WS' },
{
_id: 'env_base',
_type: 'environment',
parentId: workspaceId,
name: 'Base',
data: {
baseurl: 'https://api.example.com',
user: { name: 'alice' },
arr: [{ id: 1 }]
}
},
{
_id: 'env_sub',
_type: 'environment',
parentId: 'env_base',
name: 'Sub',
data: {
user: { name: 'bob' }
}
}
];
const envs = buildV4Environments(resources, workspaceId);
expect(envs.length).toBe(2);
const base = envs.find((e) => e.name === 'Base');
const sub = envs.find((e) => e.name === 'Sub');
expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(base, 'user.name')?.value).toBe('alice');
expect(getVar(base, 'arr[0].id')?.value).toBe('1');
// sub should inherit base, override user.name
expect(getVar(sub, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(sub, 'user.name')?.value).toBe('bob');
expect(getVar(sub, 'arr[0].id')?.value).toBe('1');
});
});
});

View File

@@ -84,7 +84,27 @@ environments:
`
const expectedOutput = {
"environments": [],
environments: [
{
name: 'Imported Environment',
variables: [
{
name: 'var1',
value: 'value1',
type: 'text',
enabled: true,
secret: false
},
{
name: 'var2',
value: 'value2',
type: 'text',
enabled: true,
secret: false
}
]
}
],
"items": [
{
"items": [

View File

@@ -65,7 +65,27 @@ const insomniaCollection = {
};
const expectedOutput = {
"environments": [],
environments: [
{
name: 'Environment 1',
variables: [
{
name: 'var1',
value: 'value1',
type: 'text',
enabled: true,
secret: false
},
{
name: 'var2',
value: 'value2',
type: 'text',
enabled: true,
secret: false
}
]
}
],
"items": [
{
"items": [

View File

@@ -0,0 +1,55 @@
import { describe, it, expect } from '@jest/globals';
import { flattenObject } from '../../src/utils/flatten';
describe('flattenObject', () => {
it('returns empty object for empty input object', () => {
expect(flattenObject({})).toEqual({});
});
it('flattens a simple nested object', () => {
const input = { user: { name: 'Tom', info: { id: 1 } } };
expect(flattenObject(input)).toEqual({
'user.name': 'Tom',
'user.info.id': 1
});
});
it('flattens arrays using JavaScript-style square bracket notation', () => {
const input = { tags: ['a', 'b'], nums: [1, 2] };
expect(flattenObject(input)).toEqual({
'tags[0]': 'a',
'tags[1]': 'b',
'nums[0]': 1,
'nums[1]': 2
});
});
it('handles null and primitive leaves correctly', () => {
const input = { a: null, b: true, c: 0, d: 'x' };
expect(flattenObject(input)).toEqual({
a: null,
b: true,
c: 0,
d: 'x'
});
});
it('flattens mixed nested objects and arrays', () => {
const input = {
user: { name: 'Tom', roles: ['admin', 'editor'] },
list: [{ id: 1 }, { id: 2 }]
};
expect(flattenObject(input)).toEqual({
'user.name': 'Tom',
'user.roles[0]': 'admin',
'user.roles[1]': 'editor',
'list[0].id': 1,
'list[1].id': 2
});
});
it('ignores empty arrays/objects (no keys produced for empty containers)', () => {
const input = { emptyObj: {}, emptyArr: [] };
expect(flattenObject(input)).toEqual({});
});
});

View File

@@ -0,0 +1,123 @@
{
"_type": "export",
"__export_format": 4,
"__export_date": "2025-01-01T12:00:00.000Z",
"__export_source": "insomnia.desktop.app:v10.3.1",
"resources": [
{
"_id": "req_fdedb34f7d5541d0aa7a917ce37ec067",
"parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b",
"modified": 1689952276171,
"created": 1689951240510,
"url": "{{baseUrl}}/api/users",
"name": "Get Users",
"description": "Fetch all users from the API",
"method": "GET",
"body": {},
"parameters": [],
"headers": [
{
"name": "Accept",
"value": "application/json"
}
],
"authentication": {},
"metaSortKey": -1689951414329,
"isPrivate": false,
"settingStoreCookies": true,
"settingSendCookies": true,
"settingDisableRenderRequestBody": false,
"settingEncodeUrl": true,
"settingRebuildPath": true,
"settingFollowRedirects": "global",
"_type": "request"
},
{
"_id": "wrk_398c634c4fbc4774bcff39cbff44b31b",
"parentId": null,
"modified": 1743678539806,
"created": 1743678539806,
"name": "Test API Collection v4 with Environments",
"description": "Test collection for Insomnia v4 format with environments",
"scope": "collection",
"_type": "workspace"
},
{
"_id": "env_93781eb62f074459bb67692112b76da0",
"parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b",
"modified": 1743681240772,
"created": 1689951235312,
"name": "Base Environment",
"data": {
"baseUrl": "https://api.example.com",
"authToken": "your_auth_token_here",
"user": {
"name": "admin",
"id": 123,
"roles": ["admin", "user"]
},
"config": {
"timeout": 30000,
"retries": 3,
"debug": true
}
},
"dataPropertyOrder": null,
"color": null,
"isPrivate": false,
"metaSortKey": 1689951235312,
"_type": "environment"
},
{
"_id": "env_staging_123",
"parentId": "env_93781eb62f074459bb67692112b76da0",
"modified": 1743681240772,
"created": 1689951235312,
"name": "Staging",
"data": {
"baseUrl": "https://staging-api.example.com",
"user": {
"name": "staging_admin"
},
"config": {
"timeout": 60000,
"debug": false
}
},
"dataPropertyOrder": null,
"color": null,
"isPrivate": false,
"metaSortKey": 1689951235312,
"_type": "environment"
},
{
"_id": "env_dev_456",
"parentId": "env_93781eb62f074459bb67692112b76da0",
"modified": 1743681240772,
"created": 1689951235312,
"name": "Development",
"data": {
"baseUrl": "https://dev-api.example.com",
"authToken": "dev_token_123",
"newFeature": {
"enabled": true,
"version": 2.099123123
}
},
"dataPropertyOrder": null,
"color": null,
"isPrivate": false,
"metaSortKey": 1689951235312,
"_type": "environment"
},
{
"_id": "jar_09963a0322c24b698ecd2f866ae9a6ab",
"parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b",
"modified": 1689951235313,
"created": 1689951235313,
"name": "Default Jar",
"cookies": [],
"_type": "cookie_jar"
}
]
}

View File

@@ -0,0 +1,87 @@
type: collection.insomnia.rest/5.0
name: Test API Collection v5 with Environments
meta:
id: wrk_7faf891d273e4b7ea82bdbaa641ee17a
created: 1743683067888
modified: 1743683067888
collection:
- name: API Tests
meta:
id: fld_ab2a1533f2be48c194883bf07d693292
created: 1743683080329
modified: 1743683080329
sortKey: -1743683080329
children:
- url: "{{ _.base_url }}/api/users"
name: Get Users
meta:
id: req_0393b8ff4ee1454daddacdda33fd33ea
created: 1743683426423
modified: 1743683632735
isPrivate: false
sortKey: -1743683429031
method: GET
headers:
- name: Authorization
value: Bearer {{ _.auth_token }}
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8
created: 1743683067908
modified: 1743683833282
cookies: []
environments:
name: Base Environment
meta:
id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8
created: 1743683067895
modified: 1743683476058
isPrivate: false
data:
base_url: https://api.example.com
auth_token: your_auth_token_here
user:
name: admin
id: 123
roles:
- admin
- user
config:
timeout: 30000
retries: 3
debug: true
subEnvironments:
- name: Staging
meta:
id: env_staging_123
created: 1743683067895
modified: 1743683476058
isPrivate: false
data:
base_url: https://staging-api.example.com
user:
name: staging_admin
config:
timeout: 60000
debug: false
- name: Development
meta:
id: env_dev_456
created: 1743683067895
modified: 1743683476058
isPrivate: false
data:
base_url: https://dev-api.example.com
auth_token: dev_token_123
new_feature:
enabled: true
version: 2.099123123

View File

@@ -0,0 +1,185 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions';
test.describe('Import Insomnia v4 Collection - Environment Import', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
/**
* Tests Insomnia v4 environment import with nested data flattening and environment merging.
* Verifies that base and sub-environments are imported correctly with JavaScript-style keys
* (e.g., user.name, user.roles[0]) and proper value inheritance/overrides.
*
* Test Structure:
* - Base Environment: Contains nested objects, arrays, and primitive values
* - Staging Environment: Overrides some base values, inherits others
* - Development Environment: Adds new variables while inheriting base values
*/
test('Import Insomnia v4 collection with nested environments and verify flattening', async ({
page,
createTmpDir
}) => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4-with-envs.json');
await test.step('Import Insomnia v4 collection with environments', async () => {
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.getByTestId('import-collection-modal');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await page.setInputFiles('input[type="file"]', insomniaFile);
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const locationModal = page.getByTestId('import-collection-location-modal');
await expect(locationModal.getByText('Test API Collection v4 with Environments')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-env-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4 with Environments')).toBeVisible();
await openCollectionAndAcceptSandbox(page, 'Test API Collection v4 with Environments', 'safe');
});
await test.step('Open collection environments panel', async () => {
await page.getByTestId('environment-selector-trigger').click();
await page.getByTestId('env-tab-collection').click();
await page.getByRole('button', { name: 'Configure' }).click();
});
await test.step('Verify all environments are present', async () => {
await expect(page
.locator('div')
.filter({ hasText: /^Base Environment$/ })
.first()).toBeVisible();
await expect(page
.locator('div')
.filter({ hasText: /^Staging$/ })
.first()).toBeVisible();
await expect(page
.locator('div')
.filter({ hasText: /^Development$/ })
.first()).toBeVisible();
});
await test.step('Test Base Environment - verify flattened keys', async () => {
await page
.locator('div')
.filter({ hasText: /^Base Environment$/ })
.first()
.click();
// **Assertion 1: Basic Variables (Top-level keys)**
// Verifies that simple key-value pairs from the base environment are imported correctly
const v4BaseUrlInput = page.locator('input[value="baseUrl"]');
const v4AuthTokenInput = page.locator('input[value="authToken"]');
await expect(v4BaseUrlInput).toBeVisible();
await expect(v4AuthTokenInput).toBeVisible();
// Assert: Top-level string values are preserved exactly as in the source
await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://api.example.com');
await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');
// **Assertion 2: Nested Object Flattening**
// Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id)
const v4UserNameInput = page.locator('input[value="user.name"]');
const v4UserIdInput = page.locator('input[value="user.id"]');
await expect(v4UserNameInput).toBeVisible();
await expect(v4UserIdInput).toBeVisible();
// Assert: Nested object properties are accessible via dot notation
await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('admin');
// Assert: Numeric values are converted to strings and preserved
await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123');
// **Assertion 3: Array Flattening**
// Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1])
const v4UserRoles0Input = page.locator('input[value="user.roles[0]"]');
const v4UserRoles1Input = page.locator('input[value="user.roles[1]"]');
await expect(v4UserRoles0Input).toBeVisible();
await expect(v4UserRoles1Input).toBeVisible();
// Assert: Array elements are accessible via JavaScript-style square bracket notation
await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin');
await expect(page.getByTestId('env-var-row-user.roles[1]').locator('.CodeMirror-line').first()).toHaveText('user');
});
await test.step('Test Staging Environment - verify merging with base', async () => {
await page
.locator('div')
.filter({ hasText: /^Staging$/ })
.first()
.click();
// **Assertion 1: Top-level Variable Override**
// Verifies that staging environment overrides base environment values
const v4StagingBaseUrlInput = page.locator('input[value="baseUrl"]');
await expect(v4StagingBaseUrlInput).toBeVisible();
// Assert: Staging overrides baseUrl with its own value
await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com');
// **Assertion 2: Top-level Variable Inheritance**
// Verifies that staging environment inherits base environment values when not overridden
const v4StagingAuthTokenInput = page.locator('input[value="authToken"]');
await expect(v4StagingAuthTokenInput).toBeVisible();
// Assert: Staging inherits authToken from base (not overridden in staging)
await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');
// **Assertion 3: Nested Object Variable Override and Inheritance**
// Verifies that nested object properties can be selectively overridden while inheriting others
const v4StagingUserNameInput = page.locator('input[value="user.name"]');
const v4StagingUserIdInput = page.locator('input[value="user.id"]');
const v4StagingUserRoles0Input = page.locator('input[value="user.roles[0]"]');
await expect(v4StagingUserNameInput).toBeVisible();
await expect(v4StagingUserIdInput).toBeVisible();
await expect(v4StagingUserRoles0Input).toBeVisible();
// Assert: Staging overrides user.name with its own value
await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('staging_admin');
// Assert: Staging inherits user.id from base (not overridden in staging)
await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123');
// Assert: Staging inherits user.roles[0] from base (not overridden in staging)
await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin');
});
await test.step('Test Development Environment - verify new variables', async () => {
await page
.locator('div')
.filter({ hasText: /^Development$/ })
.first()
.click();
// **Assertion 1: Multiple Top-level Variable Overrides**
// Verifies that development environment can override multiple base environment values
const v4DevBaseUrlInput = page.locator('input[value="baseUrl"]');
const v4DevAuthTokenInput = page.locator('input[value="authToken"]');
await expect(v4DevBaseUrlInput).toBeVisible();
await expect(v4DevAuthTokenInput).toBeVisible();
// Assert: Development overrides baseUrl with its own value
await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com');
// Assert: Development overrides authToken with its own value
await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('dev_token_123');
// **Assertion 2: New Nested Variables Addition**
// Verifies that development environment can add completely new nested variables not present in base
const v4NewFeatureEnabledInput = page.locator('input[value="newFeature.enabled"]');
const v4NewFeatureVersionInput = page.locator('input[value="newFeature.version"]');
await expect(v4NewFeatureEnabledInput).toBeVisible();
await expect(v4NewFeatureVersionInput).toBeVisible();
// Assert: New boolean variable is added and converted to string
await expect(page.getByTestId('env-var-row-newFeature.enabled').locator('.CodeMirror-line').first()).toHaveText('true');
// Assert: New numeric variable is added and converted to string with full precision
await expect(page.getByTestId('env-var-row-newFeature.version').locator('.CodeMirror-line').first()).toHaveText('2.099123123');
});
await test.step('Close environment modal', async () => {
// Close the environment configuration modal to ensure clean state
await page.getByText('×').click();
});
});
});

View File

@@ -0,0 +1,212 @@
import { test, expect } from '../../../playwright';
import * as path from 'path';
import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions';
test.describe('Import Insomnia v5 Collection - Environment Import', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
/**
* Tests Insomnia v5 environment import with nested data flattening and environment merging.
* Verifies that base and sub-environments are imported correctly with JavaScript-style keys
* (e.g., user.name, user.roles[0]) and proper value inheritance/overrides.
*
* Test Structure:
* - Base Environment: Contains nested objects, arrays, and primitive values
* - Staging Environment: Overrides some base values, inherits others
* - Development Environment: Adds new variables while inheriting base values
*/
test('Import Insomnia v5 collection with nested environments and verify flattening', async ({
page,
createTmpDir
}) => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-with-envs.yaml');
await test.step('Import Insomnia v5 collection with environments', async () => {
await page.getByRole('button', { name: 'Import Collection' }).click();
const importModal = page.getByTestId('import-collection-modal');
await importModal.waitFor({ state: 'visible' });
await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
await page.setInputFiles('input[type="file"]', insomniaFile);
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
const locationModal = page.getByTestId('import-collection-location-modal');
await expect(locationModal.getByText('Test API Collection v5 with Environments')).toBeVisible();
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-env-test'));
await page.getByRole('button', { name: 'Import', exact: true }).click();
await expect(page.getByText('Test API Collection v5 with Environments')).toBeVisible();
await openCollectionAndAcceptSandbox(page, 'Test API Collection v5 with Environments', 'safe');
});
await test.step('Open collection environments panel', async () => {
await page.getByTestId('environment-selector-trigger').click();
await page.getByTestId('env-tab-collection').click();
await page.getByRole('button', { name: 'Configure' }).click();
});
await test.step('Verify all environments are present', async () => {
await expect(page
.locator('div')
.filter({ hasText: /^Base Environment$/ })
.first()).toBeVisible();
await expect(page
.locator('div')
.filter({ hasText: /^Staging$/ })
.first()).toBeVisible();
await expect(page
.locator('div')
.filter({ hasText: /^Development$/ })
.first()).toBeVisible();
});
await test.step('Test Base Environment - verify flattened keys', async () => {
await page
.locator('div')
.filter({ hasText: /^Base Environment$/ })
.first()
.click();
// **Assertion 1: Basic Variables (Top-level keys)**
// Verifies that simple key-value pairs from the base environment are imported correctly
const baseUrlInput = page.locator('input[value="base_url"]');
const authTokenInput = page.locator('input[value="auth_token"]');
await expect(baseUrlInput).toBeVisible();
await expect(authTokenInput).toBeVisible();
// Assert: Top-level string values are preserved exactly as in the source
await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://api.example.com');
await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');
// **Assertion 2: Nested Object Flattening**
// Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id)
const userNameInput = page.locator('input[value="user.name"]');
const userIdInput = page.locator('input[value="user.id"]');
await expect(userNameInput).toBeVisible();
await expect(userIdInput).toBeVisible();
// Assert: Nested object properties are accessible via dot notation
await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('admin');
// Assert: Numeric values are converted to strings and preserved
await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123');
// **Assertion 3: Array Flattening**
// Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1])
const userRoles0Input = page.locator('input[value="user.roles[0]"]');
const userRoles1Input = page.locator('input[value="user.roles[1]"]');
await expect(userRoles0Input).toBeVisible();
await expect(userRoles1Input).toBeVisible();
// Assert: Array elements are accessible via JavaScript-style square bracket notation
await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin');
await expect(page.getByTestId('env-var-row-user.roles[1]').locator('.CodeMirror-line').first()).toHaveText('user');
// **Assertion 4: Deeply Nested Config Objects**
// Verifies that deeply nested objects are properly flattened (e.g., config.timeout, config.debug)
const configTimeoutInput = page.locator('input[value="config.timeout"]');
const configDebugInput = page.locator('input[value="config.debug"]');
await expect(configTimeoutInput).toBeVisible();
await expect(configDebugInput).toBeVisible();
// Assert: Numeric values in nested objects are converted to strings
await expect(page.getByTestId('env-var-row-config.timeout').locator('.CodeMirror-line').first()).toHaveText('30000');
// Assert: Boolean values in nested objects are converted to strings
await expect(page.getByTestId('env-var-row-config.debug').locator('.CodeMirror-line').first()).toHaveText('true');
});
await test.step('Test Staging Environment - verify merging and overrides', async () => {
await page
.locator('div')
.filter({ hasText: /^Staging$/ })
.first()
.click();
// **Assertion 1: Top-level Variable Override**
// Verifies that staging environment overrides base environment values
const stagingBaseUrlInput = page.locator('input[value="base_url"]');
await expect(stagingBaseUrlInput).toBeVisible();
// Assert: Staging overrides base_url with its own value
await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com');
// **Assertion 2: Top-level Variable Inheritance**
// Verifies that staging environment inherits base environment values when not overridden
const stagingAuthTokenInput = page.locator('input[value="auth_token"]');
await expect(stagingAuthTokenInput).toBeVisible();
// Assert: Staging inherits auth_token from base (not overridden in staging)
await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');
// **Assertion 3: Nested Object Variable Override and Inheritance**
// Verifies that nested object properties can be selectively overridden while inheriting others
const stagingUserNameInput = page.locator('input[value="user.name"]');
const stagingUserIdInput = page.locator('input[value="user.id"]');
await expect(stagingUserNameInput).toBeVisible();
await expect(stagingUserIdInput).toBeVisible();
// Assert: Staging overrides user.name with its own value
await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('staging_admin');
// Assert: Staging inherits user.id from base (not overridden in staging)
await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123');
// **Assertion 4: Deeply Nested Config Override**
// Verifies that deeply nested object properties can be overridden
const stagingConfigTimeoutInput = page.locator('input[value="config.timeout"]');
const stagingConfigDebugInput = page.locator('input[value="config.debug"]');
await expect(stagingConfigTimeoutInput).toBeVisible();
await expect(stagingConfigDebugInput).toBeVisible();
// Assert: Staging overrides config.timeout with its own value
await expect(page.getByTestId('env-var-row-config.timeout').locator('.CodeMirror-line').first()).toHaveText('60000');
// Assert: Staging overrides config.debug with its own value
await expect(page.getByTestId('env-var-row-config.debug').locator('.CodeMirror-line').first()).toHaveText('false');
});
await test.step('Test Development Environment - verify new variables', async () => {
await page
.locator('div')
.filter({ hasText: /^Development$/ })
.first()
.click();
// **Assertion 1: Multiple Top-level Variable Overrides**
// Verifies that development environment can override multiple base environment values
const devBaseUrlInput = page.locator('input[value="base_url"]');
const devAuthTokenInput = page.locator('input[value="auth_token"]');
await expect(devBaseUrlInput).toBeVisible();
await expect(devAuthTokenInput).toBeVisible();
// Assert: Development overrides base_url with its own value
await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com');
// Assert: Development overrides auth_token with its own value
await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('dev_token_123');
// **Assertion 2: New Nested Variables Addition**
// Verifies that development environment can add completely new nested variables not present in base
const newFeatureEnabledInput = page.locator('input[value="new_feature.enabled"]');
const newFeatureVersionInput = page.locator('input[value="new_feature.version"]');
await expect(newFeatureEnabledInput).toBeVisible();
await expect(newFeatureVersionInput).toBeVisible();
// Assert: New boolean variable is added and converted to string
await expect(page.getByTestId('env-var-row-new_feature.enabled').locator('.CodeMirror-line').first()).toHaveText('true');
// Assert: New numeric variable is added and converted to string with full precision
await expect(page.getByTestId('env-var-row-new_feature.version').locator('.CodeMirror-line').first()).toHaveText('2.099123123');
// **Assertion 3: Base Variable Inheritance**
// Verifies that development environment still inherits base variables that are not overridden
const devUserRoles0Input = page.locator('input[value="user.roles[0]"]');
await expect(devUserRoles0Input).toBeVisible();
// Assert: Development inherits user.roles[0] from base (not overridden in development)
await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin');
});
await test.step('Close environment modal', async () => {
// Close the environment configuration modal to ensure clean state
await page.getByText('×').click();
});
});
});