From cdb4f167b5a34f7d260217137059a3ad7ddcbfbf Mon Sep 17 00:00:00 2001 From: Aaron Perez Date: Tue, 14 Jan 2025 14:29:10 -0600 Subject: [PATCH] [DLT-1110] Mapped Collection Endpoint Browse Tests (2/4) (#1207) * [DLT-1110] Add tests * [DLT-1110] Update chai with mocha, attempt usage of mock-import * [DLT-1110] Add mocha config, correct prettier * [DLT-1110] Update tests, correct logic * [DLT-1110] Update tests, add fixtures, update setup, lock in packages * [DLT-1110] Update test --- .github/workflows/javascript-format.yml | 2 +- web/.mocharc.cjs | 32 +++ web/package.json.in | 14 +- .../transfer-dialog-controller.test.js | 80 ++++++ .../transfer-endpoint-manager.test.js | 197 ++++++++++++++ .../transfer/transfer-ui-manager.test.js | 240 ++++++++++++++++++ web/test/fixtures/transfer-fixtures.js | 45 ++++ web/test/setup.js | 132 ++++++++++ 8 files changed, 736 insertions(+), 6 deletions(-) create mode 100644 web/.mocharc.cjs create mode 100644 web/test/components/transfer/transfer-dialog-controller.test.js create mode 100644 web/test/components/transfer/transfer-endpoint-manager.test.js create mode 100644 web/test/components/transfer/transfer-ui-manager.test.js create mode 100644 web/test/fixtures/transfer-fixtures.js create mode 100644 web/test/setup.js diff --git a/.github/workflows/javascript-format.yml b/.github/workflows/javascript-format.yml index e11e5d1e4..f5028263a 100644 --- a/.github/workflows/javascript-format.yml +++ b/.github/workflows/javascript-format.yml @@ -18,7 +18,7 @@ jobs: # Step 3: Install Prettier and ESLint globally - name: Install Prettier run: | - npm install -g prettier + npm install -g prettier@3.4.2 # Step 4: Run Prettier to format code - name: Run prettier diff --git a/web/.mocharc.cjs b/web/.mocharc.cjs new file mode 100644 index 000000000..35e83b833 --- /dev/null +++ b/web/.mocharc.cjs @@ -0,0 +1,32 @@ +/** + * Mocha configuration file. + * + * This configuration file sets up various options for running Mocha tests. + * + * @property {boolean} diff - Show diff on failure. + * @property {boolean} recursive - Include subdirectories. + * @property {boolean} exit - Force Mocha to quit after tests complete. + * @property {string[]} extension - File extensions to include. + * @property {string} package - Path to the package.json file. + * @property {string} reporter - Reporter to use. + * @property {number} timeout - Test-case timeout in milliseconds. + * @property {string} ui - User interface to use (e.g., BDD, TDD). + * @property {string[]} require - Modules to require before running tests. + * @property {string[]} watch-files - Files to watch for changes. + * @property {string[]} watch-ignore - Files to ignore when watching. + * @property {string[]} spec - Test files to run. + */ +module.exports = { + diff: true, + recursive: true, + exit: true, + extension: ["js"], + package: "./package.json", + reporter: "spec", + timeout: 2000, + ui: "bdd", + require: ["test/setup.js"], + "watch-files": ["test/**/*.js", "static/**/*.js"], + "watch-ignore": ["node_modules", "coverage"], + spec: ["test/**/*.test.js"], +}; diff --git a/web/package.json.in b/web/package.json.in index 02dc5b3fe..715e621ab 100644 --- a/web/package.json.in +++ b/web/package.json.in @@ -18,7 +18,7 @@ "sanitize-html": "^2.11.0" }, "scripts": { - "test": "mocha" + "test": "mocha --config .mocharc.cjs" }, "repository": { "type": "git", @@ -36,10 +36,14 @@ }, "homepage": "https://github.com/ORNL/DataFed#readme", "devDependencies": { - "chai": "^4", - "esm": "^3.2.25", - "mocha": "^10.8.2", - "pug": "^3.0.3" + "chai": "5.1.2", + "esm": "3.2.25", + "jsdom": "26.0.0", + "jsdom-global": "3.0.2", + "mocha": "11.0.1", + "prettier": "3.4.2", + "pug": "3.0.3", + "sinon": "19.0.2" }, "type": "module" } diff --git a/web/test/components/transfer/transfer-dialog-controller.test.js b/web/test/components/transfer/transfer-dialog-controller.test.js new file mode 100644 index 000000000..c734e4186 --- /dev/null +++ b/web/test/components/transfer/transfer-dialog-controller.test.js @@ -0,0 +1,80 @@ +import { expect, sinon } from "../../setup.js"; +import { createMockServices, setupJQueryMocks } from "../../fixtures/transfer-fixtures.js"; +import { TransferDialogController } from "../../../static/components/transfer/transfer-dialog-controller.js"; +import { TransferEndpointManager } from "../../../static/components/transfer/transfer-endpoint-manager.js"; +import { TransferMode } from "../../../static/models/transfer-model.js"; +import { TransferUIManager } from "../../../static/components/transfer/transfer-ui-manager.js"; + +describe("TransferDialogController", () => { + let controller; + let mockCallback; + let mockServices; + let sandbox; + + const TEST_MODE = TransferMode.TT_DATA_PUT; + const TEST_IDS = [{ id: 1 }, { id: 2 }]; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockCallback = sandbox.stub(); + mockServices = createMockServices(); + setupJQueryMocks(sandbox); + + controller = new TransferDialogController(TEST_MODE, TEST_IDS, mockCallback, mockServices); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("constructor", () => { + it("should initialize with correct parameters and components", () => { + expect(controller.endpointManager).to.be.instanceOf(TransferEndpointManager); + expect(controller.uiManager).to.be.instanceOf(TransferUIManager); + expect(controller.ids).to.deep.equal(TEST_IDS); + expect(controller.callback).to.equal(mockCallback); + + expect(controller.endpointManager.api).to.equal(mockServices.api); + expect(controller.uiManager.api).to.equal(mockServices.api); + expect(controller.endpointManager.dialogs).to.equal(mockServices.dialogs); + expect(controller.uiManager.dialogs).to.equal(mockServices.dialogs); + }); + + it("should initialize with default services if none provided", () => { + const defaultController = new TransferDialogController( + TEST_MODE, + TEST_IDS, + mockCallback, + ); + expect(defaultController.services).to.have.property("dialogs"); + expect(defaultController.services).to.have.property("api"); + }); + }); + + describe("show", () => { + it("should successfully show the transfer dialog", async () => { + sandbox.stub(controller.uiManager, "initializeComponents"); + sandbox.stub(controller.uiManager, "attachMatchesHandler"); + sandbox.stub(controller.uiManager, "showDialog"); + + await controller.show(); + + expect(controller.uiManager.initializeComponents.called).to.be.true; + expect(controller.uiManager.attachMatchesHandler.called).to.be.true; + expect(controller.endpointManager.initialized).to.be.true; + expect(controller.uiManager.showDialog.called).to.be.true; + }); + + it("should handle errors gracefully", async () => { + sandbox + .stub(controller.uiManager, "initializeComponents") + .throws(new Error("Test error")); + + await controller.show(); + + expect( + mockServices.dialogs.dlgAlert.calledWith("Error", "Failed to open transfer dialog"), + ).to.be.true; + }); + }); +}); diff --git a/web/test/components/transfer/transfer-endpoint-manager.test.js b/web/test/components/transfer/transfer-endpoint-manager.test.js new file mode 100644 index 000000000..9f691af2d --- /dev/null +++ b/web/test/components/transfer/transfer-endpoint-manager.test.js @@ -0,0 +1,197 @@ +import { expect, sinon } from "../../setup.js"; +import { createMockServices, setupJQueryMocks } from "../../fixtures/transfer-fixtures.js"; +import { TransferEndpointManager } from "../../../static/components/transfer/transfer-endpoint-manager.js"; + +describe("TransferEndpointManager", () => { + let jQueryStub; + let manager; + let mockController; + let mockServices; + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockServices = createMockServices(); + jQueryStub = setupJQueryMocks(sandbox); + + document.body.innerHTML = ` +
+ + +
+ `; + + mockController = { + uiManager: { + state: { + frame: $("#frame"), + endpointOk: false, + }, + updateEndpoint: sandbox.stub().returnsThis(), + updateButtonStates: sandbox.stub().returnsThis(), + }, + }; + + manager = new TransferEndpointManager(mockController, mockServices); + manager.initialized = true; + manager.controller = mockController; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("searchEndpoint", () => { + beforeEach(() => { + manager.currentSearchToken = "test-token"; + }); + + it("should update UI on successful direct endpoint match", () => { + const mockData = { name: "test-endpoint" }; + mockServices.api.epView.callsFake((endpoint, callback) => callback(true, mockData)); + manager.searchEndpoint("test-endpoint", "test-token"); + + expect(manager.controller.uiManager.updateEndpoint.calledWith(mockData)).to.be.true; + expect(manager.controller.uiManager.state.endpointOk).to.be.true; + expect(manager.controller.uiManager.updateButtonStates.called).to.be.true; + }); + + it("should fall back to autocomplete when no direct match found", () => { + mockServices.api.epView.callsFake((endpoint, callback) => + callback(true, { code: "ERROR" }), + ); + const searchAutocompleteSpy = sandbox.spy(manager, "searchEndpointAutocomplete"); + + manager.searchEndpoint("test-endpoint", "test-token"); + + expect(searchAutocompleteSpy.calledWith("test-endpoint", "test-token")).to.be.true; + }); + + it("should handle API errors", () => { + mockServices.api.epView.throws(new Error("API Error")); + + manager.searchEndpoint("test-endpoint", "test-token"); + + expect(mockServices.dialogs.dlgAlert.calledWith("Globus Error", sinon.match.any)).to.be + .true; + }); + }); + + describe("searchEndpointAutocomplete", () => { + beforeEach(() => { + manager.currentSearchToken = "test-token"; + }); + + it("should update matches list with autocomplete results", () => { + const updateMatchesListSpy = sandbox.spy(manager, "updateMatchesList"); + const mockData = { + DATA: [ + { id: "1", canonical_name: "endpoint1" }, + { id: "2", canonical_name: "endpoint2" }, + ], + }; + mockServices.api.epAutocomplete.callsFake((endpoint, callback) => + callback(true, mockData), + ); + + manager.searchEndpointAutocomplete("test", "test-token"); + + expect(updateMatchesListSpy.calledWith(mockData.DATA)).to.be.true; + expect(manager.endpointManagerList).to.deep.equal(mockData.DATA); + expect(jQueryStub.html.called).to.be.true; + expect(jQueryStub.prop.calledWith("disabled", false)).to.be.true; + }); + + it("should handle no matches case", () => { + const updateMatchesListSpy = sandbox.spy(manager, "updateMatchesList"); + mockServices.api.epAutocomplete.callsFake((endpoint, callback) => + callback(true, { DATA: [] }), + ); + const consoleWarnStub = sinon.stub(console, "warn"); + + manager.searchEndpointAutocomplete("test", "test-token"); + + expect(manager.endpointManagerList).to.be.null; + expect(updateMatchesListSpy.calledWith([])).to.be.true; + expect(jQueryStub.html.calledWith("")).to + .be.true; + expect(jQueryStub.prop.calledWith("disabled", true)).to.be.true; + expect(consoleWarnStub.calledWith("No matches found")).to.be.true; + }); + + it("should handle error responses", () => { + mockServices.api.epAutocomplete.callsFake((endpoint, callback) => + callback(true, { code: "ERROR", DATA: [] }), + ); + + manager.searchEndpointAutocomplete("test", "test-token"); + + expect(mockServices.dialogs.dlgAlert.calledWith("Globus Error", "ERROR")).to.be.true; + }); + }); + + describe("handlePathInput", () => { + beforeEach(() => { + manager.currentSearchToken = "test-token"; + }); + + it("should process valid path input", () => { + jQueryStub.val.returns("endpoint/path"); + const searchEndpointSpy = sandbox.spy(manager, "searchEndpoint"); + + manager.handlePathInput("test-token"); + + expect(searchEndpointSpy.calledWith("endpoint", "test-token")).to.be.true; + }); + + it("should handle empty path input", () => { + jQueryStub.val.returns(""); + + manager.handlePathInput("test-token"); + + expect(manager.endpointManagerList).to.be.null; + expect(manager.controller.uiManager.updateButtonStates.called).to.be.true; + }); + + it("should ignore stale requests", () => { + jQueryStub.val.returns("endpoint/path"); + manager.currentSearchToken = "different-token"; + const searchEndpointSpy = sandbox.spy(manager, "searchEndpoint"); + + manager.handlePathInput("test-token"); + + expect(searchEndpointSpy.called).to.be.false; + }); + + it("should handle uninitialized state", () => { + manager.initialized = false; + const handlePathInputSpy = sandbox.spy(manager, "handlePathInput"); + + manager.handlePathInput("test-token"); + + expect(handlePathInputSpy.calledOnce).to.be.true; + }); + }); + + describe("updateMatchesList", () => { + it("should update matches list with endpoints", () => { + const endpoints = [ + { id: "1", name: "endpoint1" }, + { id: "2", name: "endpoint2" }, + ]; + + manager.updateMatchesList(endpoints); + + expect(jQueryStub.html.called).to.be.true; + expect(jQueryStub.prop.calledWith("disabled", false)).to.be.true; + }); + + it("should handle empty endpoints list", () => { + manager.updateMatchesList([]); + + expect(jQueryStub.html.calledWith("")).to + .be.true; + expect(jQueryStub.prop.calledWith("disabled", true)).to.be.true; + }); + }); +}); diff --git a/web/test/components/transfer/transfer-ui-manager.test.js b/web/test/components/transfer/transfer-ui-manager.test.js new file mode 100644 index 000000000..93509bee8 --- /dev/null +++ b/web/test/components/transfer/transfer-ui-manager.test.js @@ -0,0 +1,240 @@ +import { expect, sinon } from "../../setup.js"; +import { createMockServices, setupJQueryMocks } from "../../fixtures/transfer-fixtures.js"; +import { TransferUIManager } from "../../../static/components/transfer/transfer-ui-manager.js"; +import { TransferMode } from "../../../static/models/transfer-model.js"; + +describe("TransferUIManager", () => { + let uiManager; + let mockController; + let mockServices; + let jQueryStub; + let sandbox; + + const testPath = "/test/path/dat.txt"; + const records = ["record1", "record2"]; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockController = { + model: { + mode: TransferMode.TT_DATA_GET, + records, + getRecordInfo: sandbox.stub().returns({ selectable: true, info: "test info" }), + }, + endpointManager: { + currentEndpoint: { + id: "test-endpoint", + name: "test-endpoint", + default_directory: "/default", + activated: true, + expires_in: 3600, + DATA: [{ scheme: "https" }], + }, + currentSearchToken: 0, + searchTokenIterator: 0, + initialized: true, + handlePathInput: sandbox.stub(), + }, + callback: sandbox.stub(), + ids: records, + }; + mockServices = createMockServices(); + jQueryStub = setupJQueryMocks(sandbox); + + document.body.innerHTML = ` +
+
+
+ +
+ + + + + + + + +
+ `; + + uiManager = new TransferUIManager(mockController, mockServices); + uiManager.state = { + frame: $("#frame"), + selectionOk: true, + endpointOk: true, + recordTree: { + getSelectedNodes: sinon.stub().returns([{ key: records[0] }, { key: records[1] }]), + }, + encryptRadios: { + none: $("#encrypt_none"), + available: $("#encrypt_avail"), + required: $("#encrypt_req"), + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("Constructor and Initialization", () => { + it("should properly initialize state", () => { + const freshUiManager = new TransferUIManager(mockController, mockServices); + expect(freshUiManager.api).to.equal(mockServices.api); + expect(freshUiManager.dialogs).to.equal(mockServices.dialogs); + }); + + it("should initialize components", () => { + uiManager.initializeComponents(); + expect(jQueryStub.button.called).to.be.true; + }); + }); + + describe("Button Management", () => { + it("should set button state correctly", () => { + uiManager.setButtonState("#browse", true); + expect($("#browse").button.calledWith("enable")).to.be.true; + }); + + it("should initialize buttons", () => { + uiManager.initializeButtons(); + expect($(".btn").button.called).to.be.true; + }); + }); + + describe("Dialog Management", () => { + it("should get correct dialog labels for GET mode", () => { + const uiManager = new TransferUIManager( + { + model: { mode: TransferMode.TT_DATA_GET }, + }, + mockServices, + ); + const labels = uiManager.getDialogLabels(); + expect(labels.endpoint).to.equal("Destination"); + expect(labels.record).to.equal("Source"); + expect(labels.dialogTitle).to.equal("Download Raw Data"); + }); + + it("should get correct dialog labels for PUT mode", () => { + const uiManager = new TransferUIManager( + { + model: { mode: TransferMode.TT_DATA_PUT }, + }, + mockServices, + ); + const labels = uiManager.getDialogLabels(); + expect(labels.endpoint).to.equal("Source"); + expect(labels.record).to.equal("Destination"); + expect(labels.dialogTitle).to.equal("Upload Raw Data"); + }); + }); + + describe("Path Management", () => { + it("should get default path correctly", () => { + const endpoint = { + name: "test-endpoint", + default_directory: "/default/path", + }; + const result = uiManager.getDefaultPath(endpoint); + expect(result).to.equal("test-endpoint/default/path"); + }); + + it("should handle empty default directory", () => { + const endpoint = { + name: "test-endpoint", + }; + const result = uiManager.getDefaultPath(endpoint); + expect(result).to.equal("test-endpoint/"); + }); + }); + + describe("Record Management", () => { + it("should get selected IDs correctly", () => { + uiManager.state.recordTree = { + getSelectedNodes: () => [{ key: "record1" }, { key: "record2" }], + }; + + const ids = uiManager.getSelectedIds(); + expect(ids).to.deep.equal(["record1", "record2"]); + }); + }); + + describe("Transfer Handling", () => { + it("should handle successful transfer start", () => { + sinon.stub(uiManager, "startTransfer"); + sinon.stub(uiManager, "getTransferConfig").returns({ + path: "/test/path", + encrypt: "none", + origFilename: true, + extension: undefined, + }); + }); + + it("should handle empty path in transfer config", () => { + jQueryStub.val.returns(""); + const config = uiManager.getTransferConfig(); + expect(config).to.be.null; + expect(mockServices.dialogs.dlgAlert.called).to.be.true; + }); + }); + + describe("Transfer Operations", () => { + it("should handle successful transfer response", () => { + const mockData = { task: { id: "test-task" } }; + const closeDialogSpy = sandbox.spy(uiManager, "closeDialog"); + + uiManager.handleTransferResponse(true, mockData); + + expect(closeDialogSpy.calledOnce).to.be.true; + }); + + it("should handle transfer errors", () => { + uiManager.handleTransferResponse(false, "Error message"); + expect(mockServices.dialogs.dlgAlert.calledWith("Transfer Error", "Error message")).to + .be.true; + }); + + it("should start transfer with correct parameters", () => { + const config = { + path: "/test/path", + encrypt: "none", + origFilename: true, + extension: "txt", + }; + + uiManager.startTransfer(config); + + expect(mockServices.api.xfrStart.called).to.be.true; + expect(mockServices.api.xfrStart.firstCall.args[0]).to.deep.equal(records); + }); + }); + + describe("UI Operations", () => { + it("should update button states correctly", () => { + uiManager.updateButtonStates(); + expect(jQueryStub.button.called).to.be.true; + }); + + it("should update encryption options correctly", () => { + const endpoint = { force_encryption: true }; + const scheme = "https"; + + const radioStub = { + length: 1, + hasClass: sandbox.stub().returns(true), + checkboxradio: sandbox.stub().returnsThis(), + prop: sandbox.stub().returnsThis(), + }; + uiManager.state.encryptRadios = { + none: radioStub, + available: radioStub, + required: radioStub, + }; + + uiManager.updateEncryptionOptions(endpoint, scheme); + expect(radioStub.checkboxradio.called).to.be.true; + }); + }); +}); diff --git a/web/test/fixtures/transfer-fixtures.js b/web/test/fixtures/transfer-fixtures.js new file mode 100644 index 000000000..8ba20a657 --- /dev/null +++ b/web/test/fixtures/transfer-fixtures.js @@ -0,0 +1,45 @@ +import sinon from "sinon"; + +export function createMockServices() { + return { + api: { + epView: sinon.stub(), + epAutocomplete: sinon.stub(), + xfrStart: sinon.stub(), + }, + dialogs: { + dlgAlert: sinon.stub(), + }, + }; +} + +export function setupJQueryMocks(sandbox) { + const jQueryStub = { + addClass: sandbox.stub().returnsThis(), + autocomplete: sandbox.stub().returnsThis(), + button: sandbox.stub().returnsThis(), + checkboxradio: sandbox.stub().returnsThis(), + dialog: sandbox.stub().returnsThis(), + fancytree: sandbox.stub(), + hasClass: sandbox.stub().returns(false), + html: sandbox.stub().returnsThis(), + length: 1, + on: sandbox.stub().returnsThis(), + prop: sandbox.stub().returnsThis(), + removeClass: sandbox.stub().returnsThis(), + select: sandbox.stub().returnsThis(), + show: sandbox.stub().returnsThis(), + val: sandbox.stub().returns("/test/path"), + }; + + global.$ = sandbox.stub().returns(jQueryStub); + global.$.ui = { + fancytree: { + getTree: sandbox.stub().returns({ + getSelectedNodes: () => [{ key: "record1" }, { key: "record2" }], + }), + }, + }; + + return jQueryStub; +} diff --git a/web/test/setup.js b/web/test/setup.js new file mode 100644 index 000000000..509cd8c31 --- /dev/null +++ b/web/test/setup.js @@ -0,0 +1,132 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { JSDOM } from "jsdom"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { readFileSync } from "fs"; + +/** + * Get directory name for ES modules + */ +const __dirname = dirname(fileURLToPath(import.meta.url)); +const jqueryPath = join(__dirname, "..", "static", "jquery", "jquery.js"); +const jqueryCode = readFileSync(jqueryPath, "utf-8"); + +/** + * Sets up a minimal DOM environment for testing + * - Creates a JSDOM instance with basic HTML structure + * - Sets up global browser-like objects (window, document, etc.) + * + * @returns {JSDOM} The configured JSDOM instance + */ +function setupTestDOM() { + const dom = new JSDOM( + ` + + +
+ + `, + { + url: "http://localhost", + pretendToBeVisual: true, + runScripts: "dangerously", + }, + ); + + // Make DOM elements available globally + global.window = dom.window; + global.document = dom.window.document; + global.navigator = dom.window.navigator; + global.location = dom.window.location; + global.HTMLElement = window.HTMLElement; + global.Element = window.Element; + global.Node = window.Node; + global.Event = window.Event; + + return dom; +} + +/** + * Sets up a mock localStorage for testing + * Use this when your code interacts with localStorage + */ +function setupLocalStorage() { + global.localStorage = { + getItem: () => {}, + setItem: () => {}, + removeItem: () => {}, + }; +} + +/** + * Sets up jQuery in the test environment + * - Evaluates real jQuery code in JSDOM context + * - Adds mock implementations of common jQuery plugins + * + * @param {JSDOM} dom - The JSDOM instance to set up jQuery in + */ +function setupJQuery(dom) { + // Evaluate jQuery in the JSDOM context + dom.window.eval(jqueryCode); + // Get jQuery from the JSDOM window and set it globally + global.$ = global.jQuery = dom.window.jQuery; + // Add mock implementations of jQuery plugins + $.fn.extend({ + dialog: function () { + this.trigger = () => {}; + this.close = () => {}; + return this; + }, + button: function () { + return this; + }, + checkboxradio: function () { + return this; + }, + }); +} + +/** + * Sets up error handling for tests + * - Adds listeners for uncaught errors and unhandled rejections + * - Enhances console.error to throw in test environment + */ +function setupErrorHandling() { + // Handle uncaught errors + window.addEventListener("error", (event) => { + console.error("Error:", { + message: event.error?.message || "Unknown error", + stack: event.error?.stack, + type: event.type, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }); + }); + + // Handle unhandled promise rejections + window.addEventListener("unhandledrejection", (event) => { + console.error("Unhandled Promise Rejection:", { + reason: event.reason, + promise: event.promise, + }); + }); + + // Make console.error throw in test environment + const originalConsoleError = console.error; + console.error = (...args) => { + originalConsoleError.apply(console, args); + if (process.env.NODE_ENV === "test") { + throw new Error(args.join(" ")); + } + }; +} + +// Setup test environment +const dom = setupTestDOM(); +setupLocalStorage(); +setupJQuery(dom); +setupErrorHandling(); + +export { expect, sinon };