From 9fb41a8fcd6f698ee82175c0d9e654b4b0a7081c Mon Sep 17 00:00:00 2001 From: Dmitriy Mozgovoy Date: Sat, 13 Sep 2025 17:50:32 +0300 Subject: [PATCH] chore(ci): add local HTTP server for Karma tests; (#7022) --- .github/workflows/ci.yml | 9 ++++- bin/run-karma-tests.js | 46 ++++++++++++++++++++++++ package.json | 9 +++-- test/helpers/server.js | 70 +++++++++++++++++++++++++++++++++++++ test/specs/__helpers.js | 2 ++ test/specs/formdata.spec.js | 2 +- 6 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 bin/run-karma-tests.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d21e3..08617cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,5 +59,12 @@ jobs: if: steps.changed-ignored.outputs.only_modified == 'false' - run: npm run build if: steps.changed-ignored.outputs.only_modified == 'false' - - run: npm test + - name: Run Server tests + run: npm run test:node if: steps.changed-ignored.outputs.only_modified == 'false' + # We run browser tests only on one version of the node, since client tests do not depend on the server environment. + - name: Run browser tests + run: npm run test:browser + if: steps.changed-ignored.outputs.only_modified == 'false' && matrix.node-version == '22.x' + - name: Run package tests + run: npm run test:package diff --git a/bin/run-karma-tests.js b/bin/run-karma-tests.js new file mode 100644 index 0000000..afa7e14 --- /dev/null +++ b/bin/run-karma-tests.js @@ -0,0 +1,46 @@ +import { startTestServer, stopHTTPServer } from '../test/helpers/server.js'; +import { spawn } from 'child_process'; +import chalk from "chalk"; + +let server; + +async function run() { + + console.log(chalk.red.bold(`[ Starting HTTP server... ]`)); + + server = await startTestServer(3000); + + await new Promise((resolve, reject) => { + console.log('Starting karma runner...'); + + const karma = spawn( + 'npx', + ['karma', 'start', 'karma.conf.cjs', '--single-run'], + { + stdio: 'inherit', + shell: true, + env: { ...process.env, LISTEN_ADDR: '0.0.0.0' }, + }); + + karma.on('exit', (code) => { + code ? reject(new Error(`Karma tests failed with exit code ${code}`)) : resolve(); + }); + }); + +} + + + +(async() => { + try { + await run(); + } finally { + if (server) { + console.log(chalk.red.bold(`[ Terminating HTTP server... ]`)); + + await stopHTTPServer(server); + } + } +})(); + + diff --git a/package.json b/package.json index 6859515..06ad8ff 100644 --- a/package.json +++ b/package.json @@ -40,12 +40,15 @@ "type": "module", "types": "index.d.ts", "scripts": { - "test": "npm run test:eslint && npm run test:mocha && npm run test:karma && npm run test:dtslint && npm run test:exports", + "test": "npm run test:node && npm run test:browser && npm run test:package", + "test:node": "npm run test:mocha", + "test:browser": "npm run test:karma", + "test:package": "npm run test:eslint && npm run test:dtslint && npm run test:exports", "test:eslint": "node bin/ssl_hotfix.js eslint lib/**/*.js", "test:dtslint": "dtslint --localTs node_modules/typescript/lib", "test:mocha": "node bin/ssl_hotfix.js mocha test/unit/**/*.js --timeout 30000 --exit", "test:exports": "node bin/ssl_hotfix.js mocha test/module/test.js --timeout 30000 --exit", - "test:karma": "node bin/ssl_hotfix.js cross-env LISTEN_ADDR=:: karma start karma.conf.cjs --single-run", + "test:karma": "node ./bin/run-karma-tests.js", "test:karma:firefox": "node bin/ssl_hotfix.js cross-env LISTEN_ADDR=:: Browsers=Firefox karma start karma.conf.cjs --single-run", "test:karma:server": "node bin/ssl_hotfix.js cross-env karma start karma.conf.cjs", "test:build:version": "node ./bin/check-build-version.js", @@ -228,4 +231,4 @@ "@commitlint/config-conventional" ] } -} \ No newline at end of file +} diff --git a/test/helpers/server.js b/test/helpers/server.js index e6c57f1..1eb1534 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -4,6 +4,7 @@ import getStream from "get-stream"; import {Throttle} from "stream-throttle"; import formidable from "formidable"; + export const LOCAL_SERVER_URL = 'http://localhost:4444'; export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res); @@ -115,3 +116,72 @@ export const makeEchoStream = (echo) => new WritableStream({ echo && console.log(`Echo chunk`, chunk); } }) + + +export const startTestServer = async (port) => { + const handler = async (req) => { + const parsed = new URL(req.url, `http://localhost:${port}`); + + const params = Object.fromEntries(parsed.searchParams); + + let response = { + url: req.url, + pathname: parsed.pathname, + params, + method: req.method, + headers: req.headers, + } + + const contentType = req.headers['content-type'] || ''; + + const {delay = 0} = params; + + if (+delay) { + await setTimeoutAsync(+delay); + } + + switch (parsed.pathname.replace(/\/$/, '')) { + case '/echo/json': + default: + if (contentType.startsWith('multipart/')) { + let {fields, files} = await handleFormData(req); + response.form = fields; + response.files = files; + } else { + response.body = (await getStream(req, {encoding: 'buffer'})).toString('hex'); + } + + return { + body: response + } + } + }; + + return await startHTTPServer((req, res) => { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', `*`); // Allows all origins, or specify a domain like 'http://example.com' + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // Allowed HTTP methods + res.setHeader('Access-Control-Allow-Headers', '*'); // Allowed request headers + res.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight requests for 24 hours + + // Handle preflight requests (OPTIONS method) + if (req.method === 'OPTIONS') { + res.writeHead(204); // No content + res.end(); + return; + } + + Promise.resolve(handler(req, res)).then(response=>{ + const {status = 200, headers = {}, body} = response || {}; + + + res.statusCode = status; + + Object.entries(headers).forEach((header, value) => { + res.setHeader(header, value); + }); + + res.end(JSON.stringify(body, null, 2)) + }) + }, {port}); +} diff --git a/test/specs/__helpers.js b/test/specs/__helpers.js index c2d14c5..235f858 100644 --- a/test/specs/__helpers.js +++ b/test/specs/__helpers.js @@ -6,6 +6,8 @@ window.axios = _axios; jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; jasmine.getEnv().defaultTimeoutInterval = 60000; +window.TEST_SERVER_URL = "http://localhost:3000"; + // Get Ajax request using an increasing timeout to retry window.getAjaxRequest = (function () { let attempts = 0; diff --git a/test/specs/formdata.spec.js b/test/specs/formdata.spec.js index b43fdef..a0dffe9 100644 --- a/test/specs/formdata.spec.js +++ b/test/specs/formdata.spec.js @@ -3,7 +3,7 @@ import {retryNetwork} from "../helpers/retry.js"; describe('FormData', function() { it('should allow FormData posting', async () => { await retryNetwork(() => { - return axios.postForm('http://httpbin.org/post', { + return axios.postForm(TEST_SERVER_URL, { a: 'foo', b: 'bar' }).then(({data}) => {