diff --git a/package-lock.json b/package-lock.json index ff174d7e6..08f122dc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1474,6 +1475,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -1504,6 +1506,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1521,6 +1524,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -1800,6 +1804,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -7784,6 +7789,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -7806,6 +7812,7 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -7816,6 +7823,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -11060,6 +11068,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -12670,6 +12679,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -13637,6 +13647,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -24196,7 +24207,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -26303,9 +26314,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", - "cheerio": "^1.0.0", "moment": "^2.29.4", - "playwright": "^1.52.0", "rollup": "3.29.5", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-peer-deps-external": "^2.2.4", @@ -26815,21 +26824,6 @@ } } }, - "packages/bruno-common/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "packages/bruno-common/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -26837,38 +26831,6 @@ "dev": true, "license": "MIT" }, - "packages/bruno-common/node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "packages/bruno-common/node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "packages/bruno-common/node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 57e334e86..9801494f8 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -12,7 +12,7 @@ const { rpad } = require('../utils/common'); const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); const { dotenvToJson } = require('@usebruno/lang'); const constants = require('../constants'); -const { findItemInCollection } = require('../utils/collection'); +const { findItemInCollection, getAllRequestsInFolder } = require('../utils/collection'); const command = 'run [filename]'; const desc = 'Run a request'; @@ -22,6 +22,7 @@ const printRunSummary = (results) => { passedRequests, failedRequests, skippedRequests, + errorRequests, totalAssertions, passedAssertions, failedAssertions, @@ -36,6 +37,9 @@ const printRunSummary = (results) => { if (failedRequests > 0) { requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`; } + if (errorRequests > 0) { + requestSummary += `, ${chalk.red(`${errorRequests} error`)}`; + } if (skippedRequests > 0) { requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`; } @@ -62,6 +66,7 @@ const printRunSummary = (results) => { passedRequests, failedRequests, skippedRequests, + errorRequests, totalAssertions, passedAssertions, failedAssertions, @@ -94,12 +99,10 @@ const createCollectionFromPath = (collectionPath) => { !filePath.startsWith('node_modules') ) { let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) } - const folderBruFilePath = path.join(filePath, 'folder.bru'); - const folderBruFileExists = fs.existsSync(folderBruFilePath); - if(folderBruFileExists) { - const folderBruContent = fs.readFileSync(folderBruFilePath, 'utf8'); - let folderBruJson = collectionBruToJson(folderBruContent); + const folderBruJson = getFolderRoot(filePath); + if (folderBruJson) { folderItem.root = folderBruJson; + folderItem.seq = folderBruJson.meta.seq; } currentDirItems.push(folderItem); } @@ -114,15 +117,17 @@ const createCollectionFromPath = (collectionPath) => { if (!stats.isDirectory() && path.extname(filePath) === '.bru') { const bruContent = fs.readFileSync(filePath, 'utf8'); - const bruJson = bruToJson(bruContent); + const requestItem = bruToJson(bruContent); currentDirItems.push({ name: file, pathname: filePath, - ...bruJson + ...requestItem }); } } - return currentDirItems; + const sortedFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder')?.sort((a, b) => a.seq - b.seq); + const sortedRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder')?.sort((a, b) => a.seq - b.seq); + return sortedFolderItems?.concat(sortedRequestItems); }; collection.items = traverse(collectionPath); return collection; @@ -130,82 +135,6 @@ const createCollectionFromPath = (collectionPath) => { return getFilesInOrder(collectionPath); }; -const getBruFilesRecursively = (dir, testsOnly) => { - const environmentsPath = 'environments'; - const collection = {}; - - const getFilesInOrder = (dir) => { - let bruJsons = []; - - const traverse = (currentPath) => { - const filesInCurrentDir = fs.readdirSync(currentPath); - - if (currentPath.includes('node_modules')) { - return; - } - - for (const file of filesInCurrentDir) { - const filePath = path.join(currentPath, file); - const stats = fs.statSync(filePath); - - // todo: we might need a ignore config inside bruno.json - if ( - stats.isDirectory() && - filePath !== environmentsPath && - !filePath.startsWith('.git') && - !filePath.startsWith('node_modules') - ) { - traverse(filePath); - } - } - - const currentDirBruJsons = []; - for (const file of filesInCurrentDir) { - if (['collection.bru', 'folder.bru'].includes(file)) { - continue; - } - const filePath = path.join(currentPath, file); - const stats = fs.lstatSync(filePath); - - if (!stats.isDirectory() && path.extname(filePath) === '.bru') { - const bruContent = fs.readFileSync(filePath, 'utf8'); - const bruJson = bruToJson(bruContent); - const requestHasTests = bruJson.request?.tests; - const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false; - - if (testsOnly) { - if (requestHasTests || requestHasActiveAsserts) { - currentDirBruJsons.push({ - bruFilepath: filePath, - bruJson - }); - } - } else { - currentDirBruJsons.push({ - bruFilepath: filePath, - bruJson - }); - } - } - } - - // order requests by sequence - currentDirBruJsons.sort((a, b) => { - const aSequence = a.bruJson.seq || 0; - const bSequence = b.bruJson.seq || 0; - return aSequence - bSequence; - }); - - bruJsons = bruJsons.concat(currentDirBruJsons); - }; - - traverse(dir); - return bruJsons; - }; - - return getFilesInOrder(dir); -}; - const getCollectionRoot = (dir) => { const collectionRootPath = path.join(dir, 'collection.bru'); const exists = fs.existsSync(collectionRootPath); @@ -221,7 +150,7 @@ const getFolderRoot = (dir) => { const folderRootPath = path.join(dir, 'folder.bru'); const exists = fs.existsSync(folderRootPath); if (!exists) { - return {}; + return null; } const content = fs.readFileSync(folderRootPath, 'utf8'); @@ -444,7 +373,6 @@ const handler = async function (argv) { } } - if (filename && filename.length) { const pathExists = await exists(filename); if (!pathExists) { @@ -566,54 +494,38 @@ const handler = async function (argv) { const _isFile = isFile(filename); let results = []; - let bruJsons = []; + let requestItems = []; if (_isFile) { console.log(chalk.yellow('Running Request \n')); const bruContent = fs.readFileSync(filename, 'utf8'); - const bruJson = bruToJson(bruContent); - bruJsons.push({ - bruFilepath: filename, - bruJson - }); + const requestItem = bruToJson(bruContent); + requestItems.push(requestItem); } const _isDirectory = isDirectory(filename); if (_isDirectory) { if (!recursive) { console.log(chalk.yellow('Running Folder \n')); - const files = fs.readdirSync(filename); - const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru')); - - for (const bruFile of bruFiles) { - const bruFilepath = path.join(filename, bruFile); - const bruContent = fs.readFileSync(bruFilepath, 'utf8'); - const bruJson = bruToJson(bruContent); - const requestHasTests = bruJson.request?.tests; - const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false; - if (testsOnly) { - if (requestHasTests || requestHasActiveAsserts) { - bruJsons.push({ - bruFilepath, - bruJson - }); - } - } else { - bruJsons.push({ - bruFilepath, - bruJson - }); - } - } - bruJsons.sort((a, b) => { - const aSequence = a.bruJson.seq || 0; - const bSequence = b.bruJson.seq || 0; - return aSequence - bSequence; - }); } else { console.log(chalk.yellow('Running Folder Recursively \n')); + } + const resolvedFilepath = path.resolve(filename); + if (resolvedFilepath === collectionPath) { + requestItems = getAllRequestsInFolder(collection?.items, recursive); + } else { + const folderItem = findItemInCollection(collection, resolvedFilepath); + if (folderItem) { + requestItems = getAllRequestsInFolder(folderItem.items, recursive); + } + } - bruJsons = getBruFilesRecursively(filename, testsOnly); + if (testsOnly) { + requestItems = requestItems.filter((iter) => { + const requestHasTests = iter.request?.tests; + const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false; + return requestHasTests || requestHasActiveAsserts; + }); } } @@ -625,11 +537,10 @@ const handler = async function (argv) { if (itemPathname && !itemPathname?.endsWith('.bru')) { itemPathname = `${itemPathname}.bru`; } - const bruJson = cloneDeep(findItemInCollection(collection, itemPathname)); - if (bruJson) { + const requestItem = cloneDeep(findItemInCollection(collection, itemPathname)); + if (requestItem) { const res = await runSingleRequest( - itemPathname, - bruJson, + requestItem, collectionPath, runtimeVariables, envVars, @@ -648,14 +559,13 @@ const handler = async function (argv) { let currentRequestIndex = 0; let nJumps = 0; // count the number of jumps to avoid infinite loops - while (currentRequestIndex < bruJsons.length) { - const iter = cloneDeep(bruJsons[currentRequestIndex]); - const { bruFilepath, bruJson } = iter; + while (currentRequestIndex < requestItems.length) { + const requestItem = cloneDeep(requestItems[currentRequestIndex]); + const { pathname } = requestItem; const start = process.hrtime(); const result = await runSingleRequest( - bruFilepath, - bruJson, + requestItem, collectionPath, runtimeVariables, envVars, @@ -667,7 +577,7 @@ const handler = async function (argv) { runSingleRequestByPathname ); - const isLastRun = currentRequestIndex === bruJsons.length - 1; + const isLastRun = currentRequestIndex === requestItems.length - 1; const isValidDelay = !Number.isNaN(delay) && delay > 0; if(isValidDelay && !isLastRun){ console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay/1000).toFixed(3)}s before next request.`)); @@ -681,7 +591,7 @@ const handler = async function (argv) { results.push({ ...result, runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9, - suitename: bruFilepath.replace('.bru', '') + suitename: pathname.replace('.bru', '') }); if (reporterSkipAllHeaders) { @@ -739,7 +649,7 @@ const handler = async function (argv) { if (nextRequestName === null) { break; } - const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName); + const nextRequestIdx = requestItems.findIndex((iter) => iter.name === nextRequestName); if (nextRequestIdx >= 0) { currentRequestIndex = nextRequestIdx; } else { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 774587614..ed29d84b0 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -31,8 +31,7 @@ const onConsoleLog = (type, args) => { }; const runSingleRequest = async function ( - filename, - bruJson, + item, collectionPath, runtimeVariables, envVariables, @@ -43,14 +42,12 @@ const runSingleRequest = async function ( collection, runSingleRequestByPathname ) { + const { pathname: itemPathname } = item; + const relativeItemPathname = path.relative(collectionPath, itemPathname); try { let request; let nextRequestName; let shouldStopRunnerExecution = false; - let item = { - pathname: path.join(collectionPath, filename), - ...bruJson - } request = prepareRequest(item, collection); request.__bruno__executionMode = 'cli'; @@ -84,7 +81,7 @@ const runSingleRequest = async function ( if (result?.skipRequest) { return { test: { - filename: filename + filename: itemPathname }, request: { method: request.method, @@ -98,7 +95,8 @@ const runSingleRequest = async function ( data: null, responseTime: 0 }, - error: 'Request has been skipped from pre-request script', + error: null, + status: 'skipped', skipped: true, assertionResults: [], testResults: [], @@ -362,10 +360,10 @@ const runSingleRequest = async function ( responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); } else { - console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); + console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`)); return { test: { - filename: filename + filename: itemPathname }, request: { method: request.method, @@ -374,13 +372,14 @@ const runSingleRequest = async function ( data: request.data }, response: { - status: null, + status: 'error', statusText: null, headers: null, data: null, responseTime: 0 }, error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!', + status: 'error', assertionResults: [], testResults: [], nextRequestName: nextRequestName, @@ -392,12 +391,12 @@ const runSingleRequest = async function ( response.responseTime = responseTime; console.log( - chalk.green(stripExtension(filename)) + + chalk.green(stripExtension(relativeItemPathname)) + chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`) ); // run post-response vars - const postResponseVars = get(bruJson, 'request.vars.res'); + const postResponseVars = get(item, 'request.vars.res'); if (postResponseVars?.length) { const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime }); varsRuntime.runPostResponseVars( @@ -438,7 +437,7 @@ const runSingleRequest = async function ( // run assertions let assertionResults = []; - const assertions = get(bruJson, 'request.assertions'); + const assertions = get(item, 'request.assertions'); if (assertions) { const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); assertionResults = assertRuntime.runAssertions( @@ -500,7 +499,7 @@ const runSingleRequest = async function ( return { test: { - filename: filename + filename: itemPathname }, request: { method: request.method, @@ -516,16 +515,17 @@ const runSingleRequest = async function ( responseTime }, error: null, + status: 'pass', assertionResults, testResults, nextRequestName: nextRequestName, shouldStopRunnerExecution }; } catch (err) { - console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); + console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`)); return { test: { - filename: filename + filename: itemPathname }, request: { method: null, @@ -534,12 +534,13 @@ const runSingleRequest = async function ( data: null }, response: { - status: null, + status: 'error', statusText: null, headers: null, data: null, responseTime: 0 }, + status: 'error', error: err.message, assertionResults: [], testResults: [] diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index cb8cee7fe..07844a455 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -15,6 +15,17 @@ const collectionBruToJson = (bru) => { } }; + // add meta if it exists + // this is only for folder bru file + // in the future, all of this will be replaced by standard bru lang + const sequence = _.get(json, 'meta.seq'); + if (json?.meta) { + transformedJson.meta = { + name: json.meta.name, + seq: !isNaN(sequence) ? Number(sequence) : 1 + }; + } + return transformedJson; } catch (error) { return Promise.reject(error); diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 64e17cb39..a6f528389 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -200,10 +200,33 @@ const getTreePathFromCollectionToItem = (collection, _item) => { return path; }; +const getAllRequestsInFolder = (folderItems = [], recursive = true) => { + let requests = []; + + if (folderItems && folderItems.length) { + folderItems.forEach((item) => { + if (item.type !== 'folder') { + requests.push(item); + } else { + if (recursive) { + requests = requests.concat(getAllRequestsInFolder(item.items, recursive)); + } + } + }); + } + return requests; +}; + +const getAllRequestsAtFolderRoot = (folderItems = []) => { + return getAllRequestsInFolder(folderItems, false); +}; + module.exports = { mergeHeaders, mergeVars, mergeScripts, findItemInCollection, - getTreePathFromCollectionToItem + getTreePathFromCollectionToItem, + getAllRequestsInFolder, + getAllRequestsAtFolderRoot } \ No newline at end of file