From 25dba6031b2db1113be145f8fc2aeb27e8686f67 Mon Sep 17 00:00:00 2001 From: hveldstra Date: Thu, 12 Dec 2024 14:35:59 +0000 Subject: [PATCH 1/9] refactor: remove unused function Should have been removed in https://github.com/artilleryio/artillery/pull/3318 --- packages/artillery/lib/cmds/run.js | 52 ------------------------------ 1 file changed, 52 deletions(-) diff --git a/packages/artillery/lib/cmds/run.js b/packages/artillery/lib/cmds/run.js index 4394d582e4..d5a7419aa9 100644 --- a/packages/artillery/lib/cmds/run.js +++ b/packages/artillery/lib/cmds/run.js @@ -448,58 +448,6 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) { } }; -function replaceProcessorIfTypescript(script, scriptPath) { - const relativeProcessorPath = script.config.processor; - const userExternalPackages = script.config.bundling?.external || []; - - if (!relativeProcessorPath) { - return script; - } - const extensionType = path.extname(relativeProcessorPath); - - if (extensionType != '.ts') { - return script; - } - - const actualProcessorPath = path.resolve( - path.dirname(scriptPath), - relativeProcessorPath - ); - const processorFileName = path.basename(actualProcessorPath, extensionType); - - const processorDir = path.dirname(actualProcessorPath); - const newProcessorPath = path.join( - processorDir, - `dist/${processorFileName}.js` - ); - - //TODO: move require to top of file when Lambda bundle size issue is solved - //must be conditionally required for now as this package is removed in Lambda for now to avoid bigger package sizes - const esbuild = require('esbuild-wasm'); - - try { - esbuild.buildSync({ - entryPoints: [actualProcessorPath], - outfile: newProcessorPath, - bundle: true, - platform: 'node', - format: 'cjs', - sourcemap: 'inline', - external: ['@playwright/test', ...userExternalPackages] - }); - } catch (error) { - throw new Error(`Failed to compile Typescript processor\n${error.message}`); - } - - global.artillery.hasTypescriptProcessor = newProcessorPath; - console.log( - `Bundled Typescript file into JS. New processor path: ${newProcessorPath}` - ); - - script.config.processor = newProcessorPath; - return script; -} - async function sendTelemetry(script, flags, extraProps) { if (process.env.WORKER_ID) { debug('Telemetry: Running in cloud worker, skipping test run event'); From 87eda3a07f070f882da4d4a9ebd987bd0501b736 Mon Sep 17 00:00:00 2001 From: hveldstra Date: Fri, 13 Dec 2024 15:59:05 +0000 Subject: [PATCH 2/9] feat: add initial support for defining Playwright tests in TypeScript --- .../browser-load-testing-playwright/README.md | 28 +++++++-- .../browser-load-test.ts | 22 +++++++ .../browser-smoke-test.ts | 27 +++++++++ packages/artillery-engine-playwright/index.js | 3 +- .../platform/local/artillery-worker-local.js | 16 ++++- .../artillery/lib/platform/local/worker.js | 12 +++- .../lib/util/prepare-test-execution-plan.js | 58 ++++++++++++++++++- 7 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 examples/browser-load-testing-playwright/browser-load-test.ts create mode 100644 examples/browser-load-testing-playwright/browser-smoke-test.ts diff --git a/examples/browser-load-testing-playwright/README.md b/examples/browser-load-testing-playwright/README.md index 8cdd120e5b..427fad5197 100644 --- a/examples/browser-load-testing-playwright/README.md +++ b/examples/browser-load-testing-playwright/README.md @@ -1,14 +1,24 @@ -# Load testing and smoke testing with real browsers +# Load testing and smoke testing with headless browsers +Artillery can run Playwright scripts as performance tests. This example shows you how to run a simple load test, a smoke test, and how to track custom metrics for part of the flow. -Ever wished you could run load tests with *real browsers*? Well, now you can! You can combine Artillery with Playwright to run full browser tests, and this example shows you how. We can run both load tests, and smoke tests with headless browsers. +- [Why load test with headless browsers?](https://www.artillery.io/docs/playwright#why-load-test-with-headless-browsers) +> [!TIP] +> Artillery's uses YAML as its default configuration format, but Playwright tests can be written as TypeScript. The examples below are shown as both TypeScript-only, and YAML + TypeScript. ## Example 1: A simple load test Run a simple load test using a plain Playwright script (recorded with `playwright codegen` - no Artillery-specific changes required): + ```sh +# Run the TypeScript example: +npx artillery run browser-load-test.ts +``` + +```sh +# The same example configured with a separate YAML config file: npx artillery run browser-load-test.yml ``` @@ -25,6 +35,12 @@ For every row in the CSV file, we'll load the URL from the first column, and che The test will load each page specified in the CSV file, and check that it contains the text ```sh +# Run the TypeScript example: +npx artillery run browser-smoke-test.ts +``` + +```sh +# The same example configured with a separate YAML config file: npx artillery run browser-smoke-test.yml ``` @@ -92,6 +108,10 @@ browser.page_domcontentloaded.dominteractive.https://artillery.io/pro/: p99: ...................................................... 1380.5 ``` -## Scale out +## Scaling browser tests + +Running headless browsers in parallel will quickly exhaust CPU and memory of a single machine. + +Artillery has built-in support for cloud-native distributed load testing on AWS Fargate or Azure Container Instances. -Want to run 1,000 browsers at the same time? 10,000? more? Run your load tests on AWS Fargate with built-in support in Artillery. See our guide for [Load testing on AWS Fargate](https://www.artillery.io/docs/load-testing-at-scale/aws-fargate) for more information. +See our guide for [Distributed load testing](https://www.artillery.io/docs/load-testing-at-scale) for more information. diff --git a/examples/browser-load-testing-playwright/browser-load-test.ts b/examples/browser-load-testing-playwright/browser-load-test.ts new file mode 100644 index 0000000000..4581946e07 --- /dev/null +++ b/examples/browser-load-testing-playwright/browser-load-test.ts @@ -0,0 +1,22 @@ +import { checkOutArtilleryCoreConceptsFlow } from './flows.js'; + +export const config = { + target: 'https://www.artillery.io', + phases: [ + { + arrivalRate: 1, + duration: 10 + } + ], + engines: { + playwright: {} + } +}; + +export const scenarios = [ + { + engine: 'playwright', + name: 'check_out_core_concepts_scenario', + testFunction: checkOutArtilleryCoreConceptsFlow + } +]; diff --git a/examples/browser-load-testing-playwright/browser-smoke-test.ts b/examples/browser-load-testing-playwright/browser-smoke-test.ts new file mode 100644 index 0000000000..c71892f234 --- /dev/null +++ b/examples/browser-load-testing-playwright/browser-smoke-test.ts @@ -0,0 +1,27 @@ +import { checkPage } from './flows'; +export const config = { + target: 'https://www.artillery.io', + phases: [ + { + arrivalCount: 1, + duration: 1 + } + ], + payload: { + path: './pages.csv', + fields: ['url', 'title'], + loadAll: true, + name: 'pageChecks' + }, + engines: { + playwright: {} + } +}; + +export const scenarios = [ + { + name: 'smoke_test_page', + engine: 'playwright', + testFunction: checkPage + } +]; diff --git a/packages/artillery-engine-playwright/index.js b/packages/artillery-engine-playwright/index.js index a01cb5e661..39d8242427 100644 --- a/packages/artillery-engine-playwright/index.js +++ b/packages/artillery-engine-playwright/index.js @@ -351,7 +351,8 @@ class PlaywrightEngine { const fn = self.processor[spec.testFunction] || - self.processor[spec.flowFunction]; + self.processor[spec.flowFunction] || + spec.testFunction; if (!fn) { console.error('Playwright test function not found:', fn); diff --git a/packages/artillery/lib/platform/local/artillery-worker-local.js b/packages/artillery/lib/platform/local/artillery-worker-local.js index a193eba1de..819c4c1db4 100644 --- a/packages/artillery/lib/platform/local/artillery-worker-local.js +++ b/packages/artillery/lib/platform/local/artillery-worker-local.js @@ -104,9 +104,23 @@ class ArtilleryWorker { this.state = STATES.preparing; const { script, payload, options } = opts; + let scriptForWorker = script; + + if (script.__transpiledTypeScriptPath && script.__originalScriptPath) { + scriptForWorker = { + __transpiledTypeScriptPath: script.__transpiledTypeScriptPath, + __originalScriptPath: script.__originalScriptPath + }; + } + this.worker.postMessage({ command: 'prepare', - opts: { script, payload, options, testRunId: global.artillery.testRunId } + opts: { + script: scriptForWorker, + payload, + options, + testRunId: global.artillery.testRunId + } }); await awaitOnEE(this.workerEvents, 'readyWaiting', 50); diff --git a/packages/artillery/lib/platform/local/worker.js b/packages/artillery/lib/platform/local/worker.js index 8e2472abb6..8b12597e1d 100644 --- a/packages/artillery/lib/platform/local/worker.js +++ b/packages/artillery/lib/platform/local/worker.js @@ -105,7 +105,17 @@ async function prepare(opts) { send({ event: 'log', args }); }); - const { script: _script, payload, options } = opts; + let _script; + if ( + opts.script.__transpiledTypeScriptPath && + opts.script.__originalScriptPath + ) { + _script = require(opts.script.__transpiledTypeScriptPath); + } else { + _script = opts.script; + } + + const { payload, options } = opts; const script = await loadProcessor(_script, options); global.artillery.testRunId = opts.testRunId; diff --git a/packages/artillery/lib/util/prepare-test-execution-plan.js b/packages/artillery/lib/util/prepare-test-execution-plan.js index 02f1b489f7..cc9c6dfa42 100644 --- a/packages/artillery/lib/util/prepare-test-execution-plan.js +++ b/packages/artillery/lib/util/prepare-test-execution-plan.js @@ -2,6 +2,7 @@ const csv = require('csv-parse'); const fs = require('node:fs'); const path = require('node:path'); const p = require('util').promisify; +const debug = require('debug')('artillery'); const { readScript, @@ -23,9 +24,44 @@ async function prepareTestExecutionPlan(inputFiles, flags, _args) { let script1 = {}; for (const fn of inputFiles) { - const data = await readScript(fn); - const parsedData = await parseScript(data); - script1 = _.merge(script1, parsedData); + const fn2 = fn.toLowerCase(); + const absoluteFn = path.resolve(process.cwd(), fn); + if ( + fn2.endsWith('.yml') || + fn2.endsWith('.yaml') || + fn2.endsWith('.json') + ) { + const data = await readScript(absoluteFn); + const parsedData = await parseScript(data); + script1 = _.merge(script1, parsedData); + } else { + if (fn2.endsWith('.js')) { + const parsedData = require(absoluteFn); + script1 = _.merge(script1, parsedData); + } else if (fn2.endsWith('.ts')) { + const outputPath = path.join( + path.dirname(absoluteFn), + `dist/${path.basename(fn)}.js` + ); + + const entryPoint = path.resolve(process.cwd(), fn); + // TODO: external packages will have to be specified externally to the script + transpileTypeScript(entryPoint, outputPath, []); + debug('transpiled TypeScript file into JS. Bundled file:', outputPath); + const parsedData = require(outputPath); + script1 = _.merge(script1, parsedData); + // These magic properties are used by the worker to load the transpiled file + script1.__transpiledTypeScriptPath = outputPath; + script1.__originalScriptPath = entryPoint; + } else { + console.log('Unknown file type', fn); + console.log( + 'Only JSON (.json), YAML (.yml/.yaml) and TypeScript (.ts) files are supported' + ); + console.log('https://docs.art/e/file-types'); + throw new Error('Unknown file type'); + } + } } // We run the check here because subsequent steps can overwrite the target to undefined in @@ -114,6 +150,22 @@ async function readPayload(script) { return script; } +function transpileTypeScript(entryPoint, outputPath, userExternalPackages) { + const esbuild = require('esbuild-wasm'); + + esbuild.buildSync({ + entryPoints: [entryPoint], + outfile: outputPath, + bundle: true, + platform: 'node', + format: 'cjs', + sourcemap: 'inline', + external: ['@playwright/test', ...userExternalPackages] + }); + + return outputPath; +} + function replaceProcessorIfTypescript(script, scriptPath) { const relativeProcessorPath = script.config.processor; const userExternalPackages = script.config.bundling?.external || []; From 4cb6d2036b94293bb2cc9c4d3b15849a232b1236 Mon Sep 17 00:00:00 2001 From: hveldstra Date: Mon, 16 Dec 2024 11:11:00 +0000 Subject: [PATCH 3/9] feat: run prepareTestExecutionPlan() in workers for TypeScript scenarios --- packages/artillery/lib/platform/local/index.js | 1 + packages/artillery/lib/platform/local/worker.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/artillery/lib/platform/local/index.js b/packages/artillery/lib/platform/local/index.js index 0e5decc189..28a44a08e9 100644 --- a/packages/artillery/lib/platform/local/index.js +++ b/packages/artillery/lib/platform/local/index.js @@ -52,6 +52,7 @@ class PlatformLocal { } for (const [workerId, w] of Object.entries(this.workers)) { + this.opts.cliArgs = this.platformOpts.cliArgs; await this.prepareWorker(workerId, { script: w.script, payload: this.payload, diff --git a/packages/artillery/lib/platform/local/worker.js b/packages/artillery/lib/platform/local/worker.js index 8b12597e1d..10e2855015 100644 --- a/packages/artillery/lib/platform/local/worker.js +++ b/packages/artillery/lib/platform/local/worker.js @@ -31,6 +31,8 @@ const EventEmitter = require('eventemitter3'); const p = require('util').promisify; const { loadProcessor } = core.runner.runnerFuncs; +const prepareTestExecutionPlan = require('../../util/prepare-test-execution-plan'); + process.env.LOCAL_WORKER_ID = threadId; parentPort.on('message', onMessage); @@ -110,7 +112,12 @@ async function prepare(opts) { opts.script.__transpiledTypeScriptPath && opts.script.__originalScriptPath ) { - _script = require(opts.script.__transpiledTypeScriptPath); + // Load and process pre-compiled TypeScript file + _script = await prepareTestExecutionPlan( + [opts.script.__originalScriptPath], + opts.options.cliArgs, + [] + ); } else { _script = opts.script; } From 557a5ece0706032023100e62614b2d397e91544b Mon Sep 17 00:00:00 2001 From: hveldstra Date: Mon, 16 Dec 2024 12:24:03 +0000 Subject: [PATCH 4/9] fix: create temporary file with .yml extension The change in [1] makes file extensions mandatory. 1. https://github.com/artilleryio/artillery/pull/3436/commits/15ed666469c308d2433267b94fc0205073296b3a --- packages/artillery/lib/cmds/quick.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/artillery/lib/cmds/quick.js b/packages/artillery/lib/cmds/quick.js index d3cc1a86b0..959085683d 100644 --- a/packages/artillery/lib/cmds/quick.js +++ b/packages/artillery/lib/cmds/quick.js @@ -70,8 +70,8 @@ class QuickCommand extends Command { script.scenarios[0].engine = 'ws'; } - const tmpf = tmp.fileSync(); - fs.writeFileSync(tmpf.name, JSON.stringify(script, null, 2), { flag: 'w' }); + const tmpf = `${tmp.fileSync().name}.yml`; + fs.writeFileSync(tmpf, JSON.stringify(script, null, 2), { flag: 'w' }); const runArgs = []; if (flags.output) { @@ -82,7 +82,7 @@ class QuickCommand extends Command { runArgs.push('--quiet'); } - runArgs.push(`${tmpf.name}`); + runArgs.push(tmpf); RunCommand.run(runArgs); } From c1b7e6426eed561790fb88b0d448ed34715e230a Mon Sep 17 00:00:00 2001 From: hveldstra Date: Mon, 16 Dec 2024 13:00:07 +0000 Subject: [PATCH 5/9] feat: handle TypeScript in BOM --- packages/artillery/lib/artillery-global.js | 4 ---- .../lib/platform/aws-ecs/legacy/bom.js | 22 +++++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/artillery/lib/artillery-global.js b/packages/artillery/lib/artillery-global.js index 78f3373ab3..945789ec68 100644 --- a/packages/artillery/lib/artillery-global.js +++ b/packages/artillery/lib/artillery-global.js @@ -17,10 +17,6 @@ async function createGlobalObject(opts) { global.artillery._workerThreadSend = global.artillery._workerThreadSend || null; - // TODO: Refactor these special fields away - global.artillery.__util = global.artillery.__util || {}; - global.artillery.__util.parseScript = parseScript; - global.artillery.__util.readScript = readScript; global.artillery.__createReporter = require('./console-reporter'); global.artillery._exitCode = 0; diff --git a/packages/artillery/lib/platform/aws-ecs/legacy/bom.js b/packages/artillery/lib/platform/aws-ecs/legacy/bom.js index c6e3928b88..fb6bf20d77 100644 --- a/packages/artillery/lib/platform/aws-ecs/legacy/bom.js +++ b/packages/artillery/lib/platform/aws-ecs/legacy/bom.js @@ -15,6 +15,10 @@ const BUILTIN_ENGINES = require('./plugins').getOfficialEngines(); const Table = require('cli-table3'); const { resolveConfigTemplates } = require('../../../../util'); + +const prepareTestExecutionPlan = require('../../../../lib/util/prepare-test-execution-plan'); +const { readScript, parseScript } = require('../../../../util'); + // NOTE: Code below presumes that all paths are absolute //Tests in Fargate run on ubuntu, which uses posix paths @@ -28,8 +32,22 @@ function createBOM(absoluteScriptPath, extraFiles, opts, callback) { A.waterfall( [ A.constant(absoluteScriptPath), - global.artillery.__util.readScript, - global.artillery.__util.parseScript, + async function (scriptPath) { + let scriptData; + if (scriptPath.toLowerCase().endsWith('.ts')) { + scriptData = await prepareTestExecutionPlan( + [scriptPath], + opts.flags, + [] + ); + scriptData.config.processor = scriptPath; + } else { + const data = await readScript(scriptPath); + scriptData = await parseScript(data); + } + + return scriptData; + }, (scriptData, next) => { return next(null, { opts: { From 650457132469a884dd9ffde165d94ee72bd36c5e Mon Sep 17 00:00:00 2001 From: hveldstra Date: Mon, 16 Dec 2024 16:25:12 +0000 Subject: [PATCH 6/9] ci: run browser load test TypeScript example --- .github/workflows/examples.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index af3fba9dac..891c97a756 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -12,6 +12,32 @@ env: CLI_NOTE: Running from the Official Artillery Github Action! ðŸ˜€ jobs: + browser-load-test: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + CWD: ./examples/browser-load-testing-playwright + defaults: + run: + working-directory: ${{ env.CWD }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install + run: npm ci + + - name: Run example + uses: actions/setup-node@v3 + with: + node-version: '20.x' + - run: | + artillery run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note ${{ env.CLI_NOTE }} + env: + ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} + ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} + ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} + http-metrics-by-endpoint: runs-on: ubuntu-latest timeout-minutes: 10 From 6b5ff62859acbb8613438e2346612a9db50d340f Mon Sep 17 00:00:00 2001 From: hveldstra Date: Mon, 16 Dec 2024 16:34:47 +0000 Subject: [PATCH 7/9] ci: use artilleryio/action-cli --- .github/workflows/examples.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 891c97a756..6c7a5dc48c 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -28,11 +28,10 @@ jobs: run: npm ci - name: Run example - uses: actions/setup-node@v3 + uses: artilleryio/action-cli@v1 with: - node-version: '20.x' - - run: | - artillery run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note ${{ env.CLI_NOTE }} + command: run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note ${{ env.CLI_NOTE }} + working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} From a9f086a5e1c8760d3ad59b36eef1172f195297fa Mon Sep 17 00:00:00 2001 From: hveldstra Date: Tue, 17 Dec 2024 09:40:12 +0000 Subject: [PATCH 8/9] ci: revert to plain Node image, no browsers in our action --- .github/workflows/examples.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6c7a5dc48c..acdbb63a6a 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -24,14 +24,16 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Install - run: npm ci - - name: Run example - uses: artilleryio/action-cli@v1 + uses: actions/setup-node@v3 with: - command: run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note ${{ env.CLI_NOTE }} - working-directory: ${{ env.CWD }} + node-version: '20.x' + - name: Install dependencies + run: npm ci + - name: Run test + run: | + $ARTILLERY_BINARY_PATH run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note "${{ env.CLI_NOTE }}" + working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} From 93898887ed8c488974aba0cfed83d491dee9f735 Mon Sep 17 00:00:00 2001 From: hveldstra Date: Tue, 17 Dec 2024 09:50:46 +0000 Subject: [PATCH 9/9] test: run a Playwright TypeScript test on Fargate --- .../test/cloud-e2e/fargate/misc.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/artillery/test/cloud-e2e/fargate/misc.test.js b/packages/artillery/test/cloud-e2e/fargate/misc.test.js index 37a8a751d0..40b0175260 100644 --- a/packages/artillery/test/cloud-e2e/fargate/misc.test.js +++ b/packages/artillery/test/cloud-e2e/fargate/misc.test.js @@ -8,6 +8,8 @@ const { checkAggregateCounterSums } = require('../../helpers/expectations'); +const path = require('path'); + const A9_PATH = process.env.A9_PATH || 'artillery'; before(async () => { @@ -23,6 +25,20 @@ beforeEach(async (t) => { reportFilePath = generateTmpReportPath(t.name, 'json'); }); +test('Playwright test in TypeScript (example)', async (t) => { + const scenarioPath = path.resolve( + __dirname, + '../../../../../examples/browser-load-testing-playwright/browser-load-test.ts' + ); + const output = + await $`${A9_PATH} run-fargate ${scenarioPath} --record --tags ${baseTags}`; + t.ok(output.stdout.includes('Summary report')); + t.ok(output.stdout.includes('p99')); + t.ok(output.stdout.includes('vusers.completed')); + t.ok(output.stdout.includes('browser.page.FCP.https://www.artillery.io/')); + t.equal(output.exitCode, 0, 'CLI Exit Code should be 0'); +}); + test('Kitchen Sink Test - multiple features together', async (t) => { const scenarioPath = `${__dirname}/fixtures/cli-kitchen-sink/kitchen-sink.yml`; const dotEnvPath = `${__dirname}/fixtures/cli-kitchen-sink/kitchen-sink-env`;