diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e403f92 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: Test + +on: + pull_request: + branches: + - main + workflow_call: + +permissions: + contents: read + +jobs: + test: + name: Test + strategy: + fail-fast: false + matrix: + node-version: + - '22.12.x' + os: + - macos-latest + # TODO(dsanders11: Enable these once we fix tests on those platforms in CI + # - ubuntu-latest + # - windows-latest + runs-on: "${{ matrix.os }}" + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "${{ matrix.node-version }}" + cache: 'npm' + - name: Install dependencies + run: npm ci + # TODO(dsanders11): Enable this once we get a clean lint state on `main` + # - name: Lint + # run: npm run lint + - name: Build + run: npm run build + - name: Test + run: npm run test diff --git a/eslint.config.mjs b/eslint.config.mjs index 9a80f70..92eae3a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import globals from 'globals'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; +import pluginChaiFriendly from 'eslint-plugin-chai-friendly'; export default tseslint.config( eslint.configs.recommended, @@ -14,10 +15,13 @@ export default tseslint.config( { files: ['**/*.{js,jsx,ts,tsx}'], plugins: { + 'chai-friendly': pluginChaiFriendly, unicorn: eslintPluginUnicorn, react: react, }, rules: { + '@typescript-eslint/no-unused-expressions': 'off', + 'chai-friendly/no-unused-expressions': 'error', 'unicorn/prefer-node-protocol': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/consistent-type-imports': 'error', @@ -37,6 +41,12 @@ export default tseslint.config( }, }, }, + { + files: ['spec-electron-setup/**/*.{js,cjs,ts}'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + }, { settings: { react: { diff --git a/package-lock.json b/package-lock.json index 351f58c..7318879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,20 +22,27 @@ "devDependencies": { "@eslint/js": "^9.27.0", "@tsconfig/node22": "^22.0.2", + "@types/chai": "^5.2.2", "@types/chrome": "^0.0.326", + "@types/mocha": "^10.0.10", "@types/node": "~22.10.7", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/webpack-env": "^1.18.8", + "chai": "^5.2.1", + "colors": "^1.4.0", "copy-webpack-plugin": "^13.0.0", + "cross-env": "^7.0.3", "css-loader": "^7.1.2", "electron": "^36.5.0", "eslint": "^9.28.0", + "eslint-plugin-chai-friendly": "^1.1.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-unicorn": "^59.0.1", "globals": "^16.2.0", "html-webpack-plugin": "^5.6.3", + "mocha": "^11.7.1", "postcss": "^8.5.4", "postcss-loader": "^8.1.1", "postcss-preset-env": "^10.2.1", @@ -44,6 +51,8 @@ "style-loader": "^4.0.0", "tailwindcss": "^3.4.17", "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", "typescript": "^5.8.3", "typescript-eslint": "^8.34.0", "webpack": "^5.99.9", @@ -101,6 +110,30 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", @@ -1277,6 +1310,448 @@ "semver": "bin/semver.js" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1769,6 +2244,34 @@ "node": ">=10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node22": { "version": "22.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz", @@ -1816,6 +2319,16 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/chrome": { "version": "0.0.326", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.326.tgz", @@ -1848,6 +2361,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1988,6 +2508,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.10.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", @@ -2670,6 +3197,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ag-charts-types": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-11.3.2.tgz", @@ -2977,6 +3517,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3167,6 +3717,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/browserslist": { "version": "4.25.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", @@ -3356,6 +3913,19 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3387,6 +3957,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3404,6 +3991,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3504,6 +4101,61 @@ "node": ">=0.8.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3593,6 +4245,16 @@ "dev": true, "license": "MIT" }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -3781,6 +4443,32 @@ } } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4047,6 +4735,19 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4076,6 +4777,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4216,6 +4927,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4660,6 +5381,48 @@ "license": "MIT", "optional": true }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4751,6 +5514,19 @@ } } }, + "node_modules/eslint-plugin-chai-friendly": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.1.0.tgz", + "integrity": "sha512-+T1rClpDdXkgBAhC16vRQMI5umiWojVqkj9oUTdpma50+uByCZM/oBfxitZiOkjMRlm725mwFfz/RVgyDRvCKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=3.0.0" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -5468,6 +6244,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5541,6 +6327,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -6654,6 +7453,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7020,6 +7832,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7032,6 +7861,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -7068,6 +7904,13 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -7259,6 +8102,114 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mocha": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7813,6 +8764,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -9453,6 +10414,16 @@ "strip-ansi": "^6.0.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -9528,6 +10499,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -10959,6 +11940,67 @@ "node": ">= 8" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10966,6 +12008,26 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11279,6 +12341,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -11699,6 +12768,13 @@ "node": ">=0.10.0" } }, + "node_modules/workerpool": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -11845,6 +12921,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", @@ -11858,6 +12944,83 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -11869,6 +13032,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 1b844f2..1253a2b 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,15 @@ }, "scripts": { "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", - "build:browser": "webpack --config webpack.extension.config.ts", + "build:browser": "cross-env NODE_OPTIONS='--import=tsx' webpack --config webpack.extension.config.ts", "build:types": "tsc -b ./tsconfig.types.json", - "build:node": "webpack --config webpack.node.config.ts", + "build:node": "cross-env NODE_OPTIONS='--import=tsx' webpack --config webpack.node.config.ts", "build": "npm run clean && npm run build:node && npm run build:types && npm run build:browser", - "dev": "webpack serve --config webpack.dev.config.ts", + "dev": "cross-env NODE_OPTIONS='--import=tsx' webpack serve --config webpack.dev.config.ts", "lint:eslint": "npx eslint -c eslint.config.mjs", "lint": "npx tsc --noEmit && prettier . --check --experimental-cli", - "lint:fix": "npx prettier . --write --experimental-cli && npm run lint:eslint --fix" + "lint:fix": "npx prettier . --write --experimental-cli && npm run lint:eslint --fix", + "test": "mocha spec-electron-setup/scripts/mocha-cli.ts" }, "author": "Hitarth Rajput", "license": "MIT", @@ -47,20 +48,27 @@ "devDependencies": { "@eslint/js": "^9.27.0", "@tsconfig/node22": "^22.0.2", + "@types/chai": "^5.2.2", "@types/chrome": "^0.0.326", + "@types/mocha": "^10.0.10", "@types/node": "~22.10.7", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/webpack-env": "^1.18.8", + "chai": "^5.2.1", + "colors": "^1.4.0", "copy-webpack-plugin": "^13.0.0", + "cross-env": "^7.0.3", "css-loader": "^7.1.2", "electron": "^36.5.0", "eslint": "^9.28.0", + "eslint-plugin-chai-friendly": "^1.1.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-unicorn": "^59.0.1", "globals": "^16.2.0", "html-webpack-plugin": "^5.6.3", + "mocha": "^11.7.1", "postcss": "^8.5.4", "postcss-loader": "^8.1.1", "postcss-preset-env": "^10.2.1", @@ -69,6 +77,8 @@ "style-loader": "^4.0.0", "tailwindcss": "^3.4.17", "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", "typescript": "^5.8.3", "typescript-eslint": "^8.34.0", "webpack": "^5.99.9", diff --git a/spec-electron-setup/electron/index.html b/spec-electron-setup/electron/index.html new file mode 100644 index 0000000..e69de29 diff --git a/spec-electron-setup/electron/main.js b/spec-electron-setup/electron/main.js new file mode 100644 index 0000000..b465e1e --- /dev/null +++ b/spec-electron-setup/electron/main.js @@ -0,0 +1,143 @@ +const { app, BrowserWindow } = require('electron'); +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); +const Mocha = require('mocha'); +require('colors'); +const fs = require('node:fs/promises'); +// app.commandLine.appendSwitch('enable-logging'); + +/** + * Terminate the app if an uncaught exception or unhandled rejection occurs + * instead of opening a dialog box or leaving the app running. + */ +process.on('uncaughtException', (err) => { + console.error('Uncaught exception while running main spec:', err); + process.exit(1); +}); + +process.on('unhandledRejection', (reason) => { + console.error('Unhandled rejection while running main spec:', reason); + process.exit(1); +}); + +const pass = '[PASS]'.green; +const fail = '[FAIL]'.red; + +let mainTestDone = false; +let mainFailures = 0; +const cleanupTestSessions = async () => { + const userDataPath = app.getPath('userData'); + const sessionsPath = path.join(userDataPath, 'Partitions'); + const serviceWorkerPath = path.join(userDataPath, 'Service Worker'); + let sessions; + + console.log(`Deleting: ${serviceWorkerPath}`.cyan); + await fs.rm(serviceWorkerPath, { recursive: true, force: true }); + + try { + sessions = await fs.readdir(sessionsPath); + } catch (err) { + if (err.code === 'ENOENT') console.log(`No sessions found at ${sessionsPath}`.yellow); + else console.error(`Error reading sessions directory: ${err.message}`.red); + return; + } + + sessions = sessions.filter((session) => session.startsWith('devtron-test-')); + if (sessions.length === 0) return; + + for (const session of sessions) { + const sessionPath = path.join(sessionsPath, session); + console.log(`Deleting session: ${sessionPath}`.cyan); + await fs.rm(sessionPath, { recursive: true, force: true }); + } +}; + +/* Exit the app after tests are done */ +async function maybeExit() { + if (mainTestDone) { + console.log('\n/* ==================== TEST SUMMARY ==================== */'.cyan); + if (mainFailures) { + console.log(`Main process failures: ${mainFailures}`); + console.log(`${fail} Test suite finished with ${mainFailures} failure(s).`); + } else { + console.log(`${pass} All electron tests passed.`); + } + + console.log('\n/* ====================================================== */'.cyan); + + app.exit(mainFailures > 0 ? 1 : 0); + } +} + +/* Create test browser window */ +let mainWindow; +function createTestWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + sandbox: false, + }, + }); + + // mainWindow.webContents.openDevTools(); + mainWindow.loadURL(pathToFileURL(path.join(__dirname, 'index.html')).toString()); +} + +/* Run Mocha tests in the main process */ +async function runMainProcessTests() { + require('ts-node').register({ + compilerOptions: { + module: 'commonjs', + }, + }); + + const mochaOptions = {}; + if (process.env.MOCHA_REPORTER) { + mochaOptions.reporter = process.env.MOCHA_REPORTER; + } + if (process.env.MOCHA_MULTI_REPORTERS) { + mochaOptions.reporterOptions = { + reporterEnabled: process.env.MOCHA_MULTI_REPORTERS, + }; + } + + const mocha = new Mocha({ + timeout: 10000, + ui: 'bdd', + color: true, + }); + + const testFiles = [path.join(__dirname, '..', '..', 'spec', 'devtron-install-spec.ts')]; + + if (testFiles.length === 0) { + console.error('No test files found.'); + mainTestDone = true; + maybeExit(); + return; + } + + testFiles.sort().forEach((file) => { + mocha.addFile(file); + }); + + mocha.run((failures) => { + mainFailures = failures; + if (failures > 0) { + console.error(`${fail} ${failures} main process test(s) failed.`); + } else { + console.log(`${pass} All main process tests passed.`); + } + + mainTestDone = true; + maybeExit(); + }); +} + +/* Start the Electron app */ +app.whenReady().then(async () => { + await cleanupTestSessions(); + createTestWindow(); + await runMainProcessTests(); +}); diff --git a/spec-electron-setup/electron/preload.js b/spec-electron-setup/electron/preload.js new file mode 100644 index 0000000..a696acf --- /dev/null +++ b/spec-electron-setup/electron/preload.js @@ -0,0 +1,23 @@ +const { ipcRenderer } = require('electron'); + +const listener = () => {}; + +ipcRenderer.on('test-renderer-on', () => {}); +ipcRenderer.addListener('test-renderer-addListener', () => {}); +ipcRenderer.once('test-renderer-once', () => {}); + +ipcRenderer.on('test-renderer-off', listener); +ipcRenderer.on('test-renderer-removeListener', listener); +ipcRenderer.on('test-renderer-removeAllListeners', listener); + +ipcRenderer.on('request-ipc-renderer-events', async () => { + ipcRenderer.off('test-renderer-off', listener); + ipcRenderer.removeListener('test-renderer-removeListener', listener); + ipcRenderer.removeAllListeners('test-renderer-removeAllListeners'); + + ipcRenderer.send('test-main-on', 'arg1', 'arg2'); + ipcRenderer.send('test-main-once', 'arg1', 'arg2'); + await ipcRenderer.invoke('test-main-handle', 'arg1', 'arg2'); + await ipcRenderer.invoke('test-main-handle-once', 'arg1', 'arg2'); + ipcRenderer.sendSync('test-main-sendSync', 'arg1', 'arg2'); +}); diff --git a/spec-electron-setup/scripts/mocha-cli.ts b/spec-electron-setup/scripts/mocha-cli.ts new file mode 100644 index 0000000..efe4d39 --- /dev/null +++ b/spec-electron-setup/scripts/mocha-cli.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * This test spawns a child process to run the Electron spec runner script. + * It ensures that the Electron tests complete successfully by checking + * the exit code of the child process. + */ +describe('Electron Spec Runner', function () { + this.timeout(1000 * 30); // 30 seconds + + it('should complete Electron tests successfully', (done) => { + const runnerPath = path.resolve(__dirname, './spec-runner.ts'); + const child = spawn('npx', ['tsx', `"${runnerPath}"`], { stdio: 'inherit', shell: true }); + + child.on('close', (code) => { + expect(code).to.equal(0, 'Electron exited with non-zero status'); + done(); + }); + + child.on('error', (err) => { + done(err); + }); + }); +}); diff --git a/spec-electron-setup/scripts/spec-runner.ts b/spec-electron-setup/scripts/spec-runner.ts new file mode 100644 index 0000000..c155ae5 --- /dev/null +++ b/spec-electron-setup/scripts/spec-runner.ts @@ -0,0 +1,27 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import electronPath from 'electron'; +import 'colors'; + +async function main(): Promise { + const runnerArgs = ['spec-electron-setup/electron/main.js']; + const cwd = path.resolve(__dirname, '..', '..'); + + const { status, signal } = spawnSync(electronPath as unknown as string, runnerArgs, { + cwd, + stdio: 'inherit', + }); + + if (status !== 0) { + console.error(`Electron exited with status ${status}, signal: ${signal}`); + process.exit(status ?? 1); + } +} + +main() + .then(() => { + console.log('Electron process completed'); + }) + .catch((error) => { + console.error('Error running Electron process:', error); + }); diff --git a/spec/devtron-install-spec.ts b/spec/devtron-install-spec.ts new file mode 100644 index 0000000..52021d1 --- /dev/null +++ b/spec/devtron-install-spec.ts @@ -0,0 +1,483 @@ +import type { ServiceWorkerMain } from 'electron'; +import type { IpcEventDataIndexed } from '../src/types/shared'; +import { BrowserWindow, ipcMain, session } from 'electron'; +import { devtron } from '../src/index'; +import { expect } from 'chai'; + +let devtronSW: ServiceWorkerMain | undefined; +let ipcEvents: IpcEventDataIndexed[] = []; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Fetches IPC events stored in Devtron's service worker. + */ +async function getEvents(): Promise { + if (!devtronSW) throw new Error('Devtron service worker is not registered yet.'); + + devtronSW.send('devtron-get-ipc-events'); + + return new Promise((resolve) => { + devtronSW?.ipc.once('devtron-ipc-events', (event, ipcEvents) => { + resolve(ipcEvents); + }); + }); +} + +describe('Devtron Installation', () => { + /* --------------- test on defaultSession --------------- */ + before(async () => { + if (!session.defaultSession) throw new Error('Default session is not available'); + + await devtron.install(); + }); + + it('should load the extension in defaultSession', () => { + expect( + session.defaultSession.extensions + .getAllExtensions() + .map((ext) => ext.name) + .includes('devtron'), + ).to.be.true; + }); + + it('should register the service worker preload script in defaultSession', () => { + expect( + session.defaultSession.getPreloadScripts().some((script) => { + return script.id === 'devtron-sw-preload' && script.type === 'service-worker'; + }), + ).to.be.true; + }); + + it('should register the renderer preload script in defaultSession', () => { + expect( + session.defaultSession.getPreloadScripts().some((script) => { + return script.id === 'devtron-renderer-preload' && script.type === 'frame'; + }), + ).to.be.true; + }); + + /* ----------- test on newly created sessions ----------- */ + let newSes: Electron.Session; + before(() => { + newSes = session.fromPartition('persist:devtron-test-session'); + if (!newSes) throw new Error('New session is not available'); + }); + + it('should load the extension in newly created sessions', () => { + expect( + newSes.extensions + .getAllExtensions() + .map((ext) => ext.name) + .includes('devtron'), + ).to.be.true; + }); + + it('should register the service worker preload script in newly created sessions', () => { + expect( + newSes.getPreloadScripts().some((script) => { + return script.id === 'devtron-sw-preload' && script.type === 'service-worker'; + }), + ).to.be.true; + }); + + it('should register the renderer preload script in newly created sessions', () => { + expect( + newSes.getPreloadScripts().some((script) => { + return script.id === 'devtron-renderer-preload' && script.type === 'frame'; + }), + ).to.be.true; + }); +}); + +function registerDevtronIpc() { + if (!devtronSW) { + let devtronId = ''; + session.defaultSession.extensions.getAllExtensions().map((ext) => { + if (ext.name === 'devtron') devtronId = ext.id; + }); + + const allRunning = session.defaultSession.serviceWorkers.getAllRunning(); + for (const vid in allRunning) { + const swInfo = allRunning[vid]; + if (devtronId && swInfo.scope.includes(devtronId)) { + devtronSW = session.defaultSession.serviceWorkers.getWorkerFromVersionID(Number(vid)); + } + } + } +} + +describe('Tracking IPC Events', () => { + const mainWindow = BrowserWindow.getAllWindows()[0]; + if (!mainWindow) throw new Error('Main window is not available'); + + before(async () => { + ipcMain.on('test-main-on', () => {}); + + ipcMain.on('test-main-sendSync', (event) => { + event.returnValue = 'pong'; + }); + + ipcMain.once('test-main-once', () => {}); + + ipcMain.handle('test-main-handle', async () => { + return 'handled'; + }); + + ipcMain.handleOnce('test-main-handle-once', async () => { + return 'handled'; + }); + + await delay(300); // If some test fails when it shouldn't, try increasing this delay + registerDevtronIpc(); + + mainWindow.webContents.send('test-renderer-on', 'arg1', 'arg2'); + mainWindow.webContents.send('test-renderer-addListener', 'arg1', 'arg2'); + mainWindow.webContents.send('test-renderer-once', 'arg1', 'arg2'); + mainWindow.webContents.send('request-ipc-renderer-events', 'arg1', 'arg2'); + + // test if the listener is actually removed or not by sending an event + mainWindow.webContents.send('test-renderer-off', 'arg1', 'arg2'); + mainWindow.webContents.send('test-renderer-removeListener', 'arg1', 'arg2'); + mainWindow.webContents.send('test-renderer-removeAllListeners', 'arg1', 'arg2'); + + // testing removal of ipcMain listeners + const listener = () => {}; + ipcMain.on('test-main-off', listener); + ipcMain.on('test-main-removeListener', listener); + ipcMain.on('test-main-removeAllListeners', listener); + ipcMain.handle('test-main-removeHandler', async () => { + return; + }); + ipcMain.off('test-main-off', listener); + ipcMain.removeListener('test-main-removeListener', listener); + ipcMain.removeAllListeners('test-main-removeAllListeners'); + ipcMain.removeHandler('test-main-removeHandler'); + + await delay(300); + + /** + * During testing, the `devtronSW` variable in "src/index.ts" + * is not the same instance as the one used here. As a result, + * some events are sent to that instance and some to this one. + * Therefore, to capture all events, we need to combine events + * from both instances. + * + * This behavior does not occur during normal usage of Devtron. + * It may be caused by directly importing `devtron` from "src/index.ts" here. + */ + ipcEvents = await getEvents(); + ipcEvents.push(...(await devtron.getEvents())); + }); + + it('should track ipcRenderer.on', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer-to-main' && + e.channel === 'test-main-on' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcRenderer.addListener', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-addListener' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'addListener' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcRenderer.once', () => { + const ev_on = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-once' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'on' && + typeof e.timestamp === 'number', + ); + + const ev_removeListener = ipcEvents.find( + (e) => + e.direction === 'renderer' && + e.channel === 'test-renderer-once' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeListener' && + typeof e.timestamp === 'number', + ); + + expect(ev_on, 'Expected an ipcRenderer.on event').to.exist; + expect(ev_removeListener, 'Expected an ipcRenderer.removeListener event').to.exist; + }); + + it('should track ipcRenderer.sendSync', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer-to-main' && + e.channel === 'test-main-sendSync' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track response received by ipcRenderer.sendSync', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-main-sendSync' && + Array.isArray(e.args) && + e.args[0] === 'pong' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number' && + e.method === 'sendSync (response)' && + typeof e.responseTime === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcRenderer.invoke', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer-to-main' && + e.channel === 'test-main-handle' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + // This also indirectly tests that ipcMain.handle is tracked properly + it('should track response received by ipcRenderer.invoke', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-main-handle' && + Array.isArray(e.args) && + e.args[0] === 'handled' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number' && + e.method === 'invoke (response)' && + typeof e.responseTime === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcRenderer.removeListener', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer' && + e.channel === 'test-renderer-removeListener' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeListener' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + + const ev_after = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-removeListener' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'on', + ); + expect(ev_after).to.not.exist; + }); + + it('should track ipcRenderer.off', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer' && + e.channel === 'test-renderer-off' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'off' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + + const ev_after = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-off' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'on', + ); + expect(ev_after).to.not.exist; + }); + + it('should track ipcRenderer.removeAllListeners', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'renderer' && + e.channel === 'test-renderer-removeAllListeners' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeAllListeners' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + + const ev_after = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-removeAllListeners' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'on', + ); + expect(ev_after).to.not.exist; + }); + + it('should track ipcMain.on', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-renderer-on' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + e.method === 'on', + ); + expect(ev).to.exist; + }); + + it('should track ipcMain.handleOnce', () => { + const ev_removeHandler = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-handle-once' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeHandler' && + typeof e.timestamp === 'number', + ); + const ev_invoke = ipcEvents.find( + (e) => + e.direction === 'renderer-to-main' && + e.channel === 'test-main-handle-once' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number', + ); + + const ev_response = ipcEvents.find( + (e) => + e.direction === 'main-to-renderer' && + e.channel === 'test-main-handle-once' && + Array.isArray(e.args) && + e.args[0] === 'handled' && + typeof e.uuid === 'string' && + typeof e.timestamp === 'number' && + e.method === 'invoke (response)' && + typeof e.responseTime === 'number', + ); + + expect(ev_removeHandler, 'Expected an ipcMain.removeHandler event').to.exist; + expect(ev_invoke, 'Expected an ipcRenderer.invoke event').to.exist; + expect(ev_response, 'Expected an ipcRenderer.invoke (response) event').to.exist; + }); + + it('should track ipcMain.once', () => { + const ev_removeListener = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-once' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeListener' && + typeof e.timestamp === 'number', + ); + const ev_on = ipcEvents.find( + (e) => + e.direction === 'renderer-to-main' && + e.channel === 'test-main-once' && + Array.isArray(e.args) && + e.args[0] === 'arg1' && + e.args[1] === 'arg2' && + typeof e.timestamp === 'number', + ); + + expect(ev_removeListener, 'Expected an ipcMain.removeListener event').to.exist; + expect(ev_on, 'Expected an ipcRenderer.on event').to.exist; + }); + + it('should track ipcMain.off', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-off' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'off' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcMain.removeListener', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-removeListener' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeListener' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcMain.removeAllListeners', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-removeAllListeners' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeAllListeners' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); + + it('should track ipcMain.removeHandler', () => { + const ev = ipcEvents.find( + (e) => + e.direction === 'main' && + e.channel === 'test-main-removeHandler' && + Array.isArray(e.args) && + e.args.length === 0 && + e.method === 'removeHandler' && + typeof e.timestamp === 'number', + ); + expect(ev).to.exist; + }); +}); diff --git a/tsconfig.json b/tsconfig.json index fdbd9a8..3aa502a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["DOM"], "module": "esnext", "moduleResolution": "node", - "types": ["chrome", "node", "webpack-env", "electron"], + "types": ["chrome", "node", "webpack-env", "electron", "mocha", "chai"], "declaration": false, "sourceMap": false, "outDir": "./dist",