From 998b81d0f8dcdea2b58410a82fbb80297e645710 Mon Sep 17 00:00:00 2001 From: Adarsh2692 Date: Sat, 13 Dec 2025 22:45:47 +0530 Subject: [PATCH 1/4] session comparison - details, capabilities, selenium logs and network logs --- package-lock.json | 537 +++++++++++++++++- package.json | 6 + src/channelHandlers/browserstack-api.ts | 34 ++ src/constants/ipc-channels.ts | 10 +- src/global.d.ts | 4 +- src/index.ts | 4 +- src/preload.ts | 6 +- src/renderer/index.css | 39 +- src/renderer/products.ts | 7 + .../routes/automate/tools/diffView.tsx | 313 ++++++++++ .../automate/tools/session-comparison.tsx | 252 ++++++++ temp.html | 120 ++++ 12 files changed, 1297 insertions(+), 35 deletions(-) create mode 100644 src/renderer/routes/automate/tools/diffView.tsx create mode 100644 src/renderer/routes/automate/tools/session-comparison.tsx create mode 100644 temp.html diff --git a/package-lock.json b/package-lock.json index a91d15c..3ab7367 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,17 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/postcss": "^4.1.15", "copy-webpack-plugin": "^13.0.1", "electron-squirrel-startup": "^1.0.1", + "monaco-editor": "^0.55.1", + "monaco-types": "^0.1.0", "pouchdb": "^9.0.0", "rc-field-form": "^2.7.0", "react": "^19.2.0", + "react-diff-view": "^3.3.2", + "react-diff-viewer": "^3.1.1", "react-dom": "^19.2.0", "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", @@ -31,6 +36,7 @@ "@electron-forge/plugin-fuses": "^7.10.2", "@electron-forge/plugin-webpack": "^7.10.2", "@electron/fuses": "^1.8.0", + "@types/diff": "^7.0.2", "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", @@ -69,7 +75,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -80,16 +85,77 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -99,6 +165,51 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@electron-forge/cli": { "version": "7.10.2", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz", @@ -950,6 +1061,79 @@ "node": ">=14.14" } }, + "node_modules/@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "license": "MIT", + "dependencies": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/serialize/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "license": "MIT" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1586,6 +1770,29 @@ "node": ">= 12.13.0" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2029,6 +2236,13 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2206,7 +2420,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, "license": "MIT" }, "node_modules/@types/pouchdb": { @@ -2517,6 +2730,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -3423,6 +3643,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3841,7 +4130,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3968,6 +4256,12 @@ "node": ">=6.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -4305,6 +4599,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -4421,6 +4721,18 @@ "node": ">=10" } }, + "node_modules/create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "license": "MIT", + "dependencies": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -4621,7 +4933,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4814,6 +5125,21 @@ "dev": true, "license": "MIT" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -4918,6 +5244,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -5591,6 +5926,16 @@ "node": ">= 4" } }, + "node_modules/emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "license": "MIT", + "dependencies": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5714,7 +6059,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6605,6 +6949,12 @@ "dev": true, "license": "MIT" }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6835,7 +7185,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7037,6 +7386,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gitdiff-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz", + "integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7309,7 +7664,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7704,7 +8058,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7839,7 +8192,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -7948,7 +8300,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8450,7 +8801,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -8466,6 +8816,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9074,7 +9436,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/listr2": { @@ -9172,7 +9533,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.get": { @@ -9256,6 +9616,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -9342,6 +9714,18 @@ "node": ">=6" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -9404,6 +9788,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -9648,11 +10038,29 @@ "node": ">=10" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz", + "integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/multicast-dns": { @@ -9946,6 +10354,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10351,7 +10768,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -10377,7 +10793,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -10447,7 +10862,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { @@ -10461,7 +10875,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10907,6 +11320,23 @@ "node": ">=10" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11090,6 +11520,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-diff-view": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.2.tgz", + "integrity": "sha512-wPVq4ktTcGOHbhnWKU/gHLtd3N2Xd+OZ/XQWcKA06dsxlSsESePAumQILwHtiak2nMCMiWcIfBpqZ5OiharUPA==", + "license": "MIT", + "dependencies": { + "classnames": "^2.3.2", + "diff-match-patch": "^1.0.5", + "gitdiff-parser": "^0.3.1", + "lodash": "^4.17.21", + "shallow-equal": "^3.1.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/react-diff-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz", + "integrity": "sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "create-emotion": "^10.0.14", + "diff": "^4.0.1", + "emotion": "^10.0.14", + "memoize-one": "^5.0.4", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0", + "react-dom": "^15.3.0 || ^16.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -11451,7 +11919,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -11479,7 +11946,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12021,6 +12487,12 @@ "node": ">=8" } }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12356,6 +12828,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -12627,7 +13105,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13567,6 +14044,15 @@ "integrity": "sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==", "license": "Apache-2.0" }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -14291,7 +14777,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 6" diff --git a/package.json b/package.json index 8e927e5..d5a72f1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@electron-forge/plugin-fuses": "^7.10.2", "@electron-forge/plugin-webpack": "^7.10.2", "@electron/fuses": "^1.8.0", + "@types/diff": "^7.0.2", "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", @@ -49,12 +50,17 @@ "typescript": "~4.5.4" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/postcss": "^4.1.15", "copy-webpack-plugin": "^13.0.1", "electron-squirrel-startup": "^1.0.1", + "monaco-editor": "^0.55.1", + "monaco-types": "^0.1.0", "pouchdb": "^9.0.0", "rc-field-form": "^2.7.0", "react": "^19.2.0", + "react-diff-view": "^3.3.2", + "react-diff-viewer": "^3.1.1", "react-dom": "^19.2.0", "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", diff --git a/src/channelHandlers/browserstack-api.ts b/src/channelHandlers/browserstack-api.ts index 6b1b39c..090c0f5 100644 --- a/src/channelHandlers/browserstack-api.ts +++ b/src/channelHandlers/browserstack-api.ts @@ -31,4 +31,38 @@ export const getParsedAutomateTextLogs = async (session:AutomateSessionResponse) const logs = await download(session.automation_session.logs); const result = parseAutomateTextLogs(logs.split('\n')) return result +} + +export const getSeleniumLogs = async (selenium_logs_url: string) => { + if (!selenium_logs_url) { + return 'No Selenium logs available for this session'; + } + try { + const response = await fetch(selenium_logs_url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.text(); + // For other URLs, use the existing download function with auth + return await download(selenium_logs_url); + } catch (error) { + console.error('Failed to fetch Selenium logs:', error); + return 'Failed to load Selenium logs'; + } +} + +export const getHarLogs = async (harLogsUrl: string) => { + if (!harLogsUrl) { + return 'No network logs available for this session'; + } + try { + const response = await fetch(harLogsUrl); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch HAR logs:', error); + return 'Failed to load network logs'; + } } \ No newline at end of file diff --git a/src/constants/ipc-channels.ts b/src/constants/ipc-channels.ts index 25745b8..039b7ec 100644 --- a/src/constants/ipc-channels.ts +++ b/src/constants/ipc-channels.ts @@ -1,9 +1,11 @@ const CHANNELS = { - POST_ADMIN_CREDENTIALS:'POST /credentials/admin', - GET_ADMIN_CREDENTIALS:'GET /credentials/admin', - GET_BROWSERSTACK_AUTOMATE_SESSION:'GET /automate/sessions', - GET_BROWSERSTACK_AUTOMATE_PARSED_TEXT_LOGS:'GET /automate/sessions/textLogs/parsed' + POST_ADMIN_CREDENTIALS: 'POST /credentials/admin', + GET_ADMIN_CREDENTIALS: 'GET /credentials/admin', + GET_BROWSERSTACK_AUTOMATE_SESSION: 'GET /automate/sessions', + GET_BROWSERSTACK_AUTOMATE_PARSED_TEXT_LOGS: 'GET /automate/sessions/textLogs/parsed', + GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS: 'GET /automate/sessions/seleniumLogs', + GET_BROWSERSTACK_AUTOMATE_HAR_LOGS: 'GET /automate/sessions/harLogs' } export default CHANNELS \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index 2c65dfe..46c6acd 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -7,7 +7,9 @@ declare global { type BrowserStackAPI = { getAutomateSessionDetails: (id: string) => Promise - getAutomateParsedTextLogs: (session:AutomateSessionResponse)=> Promise + getAutomateParsedTextLogs: (session:AutomateSessionResponse) => Promise + getSeleniumLogs: (selenium_logs_url: string) => Promise + getHarLogs: (harLogsUrl: string) => Promise } interface DBItem { diff --git a/src/index.ts b/src/index.ts index e0bdcff..5e2eb94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import StorageKeys from './constants/storage-keys'; import CONFIG from './constants/config'; import { mkdirSync } from 'fs' -import { getAutomateSessionDetails, getParsedAutomateTextLogs } from './channelHandlers/browserstack-api'; +import { getAutomateSessionDetails, getParsedAutomateTextLogs, getSeleniumLogs, getHarLogs } from './channelHandlers/browserstack-api'; // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on // whether you're running in development or production). @@ -87,6 +87,8 @@ app.whenReady().then(() => { ipcMain.handle(CHANNELS.GET_ADMIN_CREDENTIALS, getBrowserStackAdminCredentials); ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SESSION,(_,id)=>getAutomateSessionDetails(id)) ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_TEXT_LOGS,(_,session)=>getParsedAutomateTextLogs(session)) + ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS,(_, selenium_logs_url) => getSeleniumLogs(selenium_logs_url)); + ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, (_, har_logs_url) => getHarLogs(har_logs_url)); }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. diff --git a/src/preload.ts b/src/preload.ts index 6d735e5..aebc2cf 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -8,8 +8,10 @@ const credentialsAPI: CredentialsAPI = { } const browserstackAPI: BrowserStackAPI = { - getAutomateSessionDetails: (id:string)=> ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SESSION,id), - getAutomateParsedTextLogs: (session)=>ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_TEXT_LOGS,session) + getAutomateSessionDetails: (id:string) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SESSION, id), + getAutomateParsedTextLogs: (session) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_TEXT_LOGS, session), + getSeleniumLogs: (selenium_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS, selenium_logs_url), + getHarLogs: (har_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, har_logs_url) } contextBridge.exposeInMainWorld('credentialsAPI', credentialsAPI); diff --git a/src/renderer/index.css b/src/renderer/index.css index df2ff97..8760d7d 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -11,4 +11,41 @@ --sh-string: #00a99a; --sh-keyword: #f47067; --sh-comment: #a19595; -} \ No newline at end of file +} + +.diff-container .diff-viewer-code-column { + max-width: 600px; + overflow-wrap: anywhere; +} + +.diff-container .rdv-code-wrapper { + max-width: 25vw; + white-space: pre-wrap !important; + word-break: break-word !important; +} + +.diff-container .rdv-code { + white-space: pre-wrap !important; + word-break: break-word !important; +} + +/* Force wrapping in ReactDiffViewer */ +.react-diff-viewer, +.react-diff-viewer * { + white-space: pre-wrap !important; + word-break: break-word !important; +} + +/* Limit each side to half of the container */ +.react-diff-viewer .diff-viewer-container, +.react-diff-viewer .diff-viewer-split { + max-width: 48vw !important; +} + +/* Override left + right code column width */ +.react-diff-viewer .diff-code, +.react-diff-viewer .diff-line { + max-width: 48vw !important; + width: 48vw !important; + display: block !important; +} diff --git a/src/renderer/products.ts b/src/renderer/products.ts index b8afc63..af2e13d 100644 --- a/src/renderer/products.ts +++ b/src/renderer/products.ts @@ -1,5 +1,6 @@ import AutomatePage from "./routes/automate"; import ReplayTool from "./routes/automate/tools/replay-tool"; +import SessionComparison from "./routes/automate/tools/session-comparison"; const Products = [ { @@ -18,6 +19,12 @@ const Products = [ description:"Analyses time spend on different actions. Helpful to identify inside/outside time for a customer session.", path:'/automate/latency-analyser', component: null + }, + { + title:"Session Comparison", + description:"Compares logs across sessions and highlights differences", + path:'/automate/session-comparison', + component: SessionComparison } ] } diff --git a/src/renderer/routes/automate/tools/diffView.tsx b/src/renderer/routes/automate/tools/diffView.tsx new file mode 100644 index 0000000..dd6d27f --- /dev/null +++ b/src/renderer/routes/automate/tools/diffView.tsx @@ -0,0 +1,313 @@ +import { useState, useMemo } from 'react'; +import ReactDiffViewer from 'react-diff-viewer'; + +function createInfoString(session: any) { + if (!session?.automation_session) return "No Session Data"; + + const s = session.automation_session; + return `Project: ${s.project_name} +Build: ${s.build_name} +Status: ${s.status} +OS: ${s.os} ${s.os_version} +Browser: ${s.browser} ${s.browser_version} +Device: ${s.device || "N/A"} +Duration: ${s.duration}s +Created At: ${new Date(s.created_at).toLocaleString()}`.trim(); +} + +interface SessionDiffViewProps { + sessionA: any; + sessionB: any; + logsA: any; + logsB: any; + seleniumA: string | null; + seleniumB: string | null; + harA: string | null; + harB: string | null; + loadingA: boolean; + loadingB: boolean; + loadingHarA: boolean; + loadingHarB: boolean; +} + +export function SessionDiffView({ + sessionA, + sessionB, + logsA, + logsB, + seleniumA, + seleniumB, + harA, + harB, + loadingA, + loadingB, + loadingHarA, + loadingHarB +}: SessionDiffViewProps) { + const [activeTab, setActiveTab] = useState<'info' | 'capabilities' | 'selenium' | 'network'>('info'); + + const parseHarLogs = useMemo(() => (harData: string | null) => { + if (!harData) return null; + + try { + const har = JSON.parse(harData); + if (!har?.log?.entries?.length) return null; + + const groupedEntries: Record = {}; + har.log.entries.forEach((entry: any) => { + const key = `${entry.request.method}|||${entry.request.url}`; + if (!groupedEntries[key]) { + groupedEntries[key] = []; + } + groupedEntries[key].push(entry); + }); + + return groupedEntries; + } catch (e) { + console.error('Error parsing HAR data:', e); + return null; + } + }, []); + + const formatNetworkEntry = (key: string, entries: any[]): string => { + const [method, url] = key.split('|||'); + const firstEntry = entries[0]; + const response = firstEntry.response; + const timings = firstEntry.timings || {}; + + const statusCodes = entries.map((e: any) => e.response.status); + const uniqueStatuses = [...new Set(statusCodes)]; + const avgTime = entries.reduce((sum, e) => sum + (e.time || 0), 0) / entries.length; + const minTime = Math.min(...entries.map((e: any) => e.time || 0)); + const maxTime = Math.max(...entries.map((e: any) => e.time || 0)); + + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + + return `Count: ${entries.length} | Status: ${uniqueStatuses.join(', ')} +Time: ${avgTime.toFixed(1)}ms (${minTime}-${maxTime}ms) +Size: ${response.bodySize || 0}b | Type: ${response.content?.mimeType || 'unknown'} +Timings: ${Object.entries(timings) + .filter(([_, v]) => v !== -1) + .map(([k, v]) => `${k}:${v}ms`) + .join(' ')}`; + }; + + const networkComparison = useMemo(() => { + const harLogsA = parseHarLogs(harA); + const harLogsB = parseHarLogs(harB); + + if (!harLogsA && !harLogsB) { + return { matched: [], onlyInA: [], onlyInB: [] }; + } + + const matched: Array<{ key: string; dataA: any[]; dataB: any[] }> = []; + const onlyInA: Array<{ key: string; data: any[] }> = []; + const onlyInB: Array<{ key: string; data: any[] }> = []; + + const keysA = new Set(Object.keys(harLogsA || {})); + const keysB = new Set(Object.keys(harLogsB || {})); + + keysA.forEach(key => { + if (keysB.has(key)) { + matched.push({ + key, + dataA: harLogsA![key], + dataB: harLogsB![key] + }); + } else { + onlyInA.push({ + key, + data: harLogsA![key] + }); + } + }); + + keysB.forEach(key => { + if (!keysA.has(key)) { + onlyInB.push({ + key, + data: harLogsB![key] + }); + } + }); + + return { matched, onlyInA, onlyInB }; + }, [harA, harB, parseHarLogs]); + + const infoStrA = createInfoString(sessionA); + const infoStrB = createInfoString(sessionB); + const nameA = sessionA?.automation_session?.name || "Unnamed Session A"; + const nameB = sessionB?.automation_session?.name || "Unnamed Session B"; + + const capsStrA = JSON.stringify(logsA.capabilities[0], null, 2); + const capsStrB = JSON.stringify(logsB.capabilities[0], null, 2); + + const selLogsA = loadingA ? "Loading logs for A..." : (seleniumA || "No Selenium logs available"); + const selLogsB = loadingB ? "Loading logs for B..." : (seleniumB || "No Selenium logs available"); + + const tabs = [ + { id: 'info', label: 'Session Info' }, + { id: 'capabilities', label: 'Capabilities' }, + { id: 'selenium', label: 'Selenium Logs' }, + { id: 'network', label: 'Network Logs' } + ] as const; + + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {activeTab === 'info' && ( + + )} + + {activeTab === 'capabilities' && ( + + )} + + {activeTab === 'selenium' && ( + + )} + + {activeTab === 'network' && ( +
+ {loadingHarA || loadingHarB ? ( +
+ Loading network logs... +
+ ) : ( + <> + {networkComparison.matched.length > 0 && ( +
+

+ Matched Requests ({networkComparison.matched.length}) +

+ {networkComparison.matched.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + + return ( +
+
+ {method} {shortUrl} +
+
+
+
{nameA}
+
+                                {formatNetworkEntry(item.key, item.dataA)}
+                              
+
+
+
{nameB}
+
+                                {formatNetworkEntry(item.key, item.dataB)}
+                              
+
+
+
+ ); + })} +
+ )} + + {networkComparison.onlyInA.length > 0 && ( +
+

+ Only in {nameA} ({networkComparison.onlyInA.length}) +

+ {networkComparison.onlyInA.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + + return ( +
+
+ {method} {shortUrl} +
+
+                            {formatNetworkEntry(item.key, item.data)}
+                          
+
+ ); + })} +
+ )} + + {networkComparison.onlyInB.length > 0 && ( +
+

+ Only in {nameB} ({networkComparison.onlyInB.length}) +

+ {networkComparison.onlyInB.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + + return ( +
+
+ {method} {shortUrl} +
+
+                            {formatNetworkEntry(item.key, item.data)}
+                          
+
+ ); + })} +
+ )} + + {networkComparison.matched.length === 0 && + networkComparison.onlyInA.length === 0 && + networkComparison.onlyInB.length === 0 && ( +
+ No network logs available for either session +
+ )} + + )} +
+ )} +
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/session-comparison.tsx b/src/renderer/routes/automate/tools/session-comparison.tsx new file mode 100644 index 0000000..3c2b7a4 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison.tsx @@ -0,0 +1,252 @@ +import Form from "rc-field-form"; +import { usePromise } from "../../../hooks/use-promise"; +import { toast } from "react-toastify"; +import Editor from 'react-simple-code-editor'; +import { highlight } from 'sugar-high'; +import { useState } from "react"; +import { SessionDiffView } from './diffView'; // 👈 Import the new diff component + +const { Field } = Form; + +// --- UTILITY FUNCTION TO SANITIZE CAPABILITIES --- + +/** + * Removes the sensitive authToken from the capabilities object. + * @param {object} session - The session object containing capabilities. + * @returns {object} A new session object with the authToken replaced. + */ +const sanitizeCapabilities = (session: any) => { + if (!session) { + return session; + } + + // Create a deep copy to avoid modifying the original state object directly + const sanitizedSession = JSON.parse(JSON.stringify(session)); + + // Define the path to the auth token within the capabilities structure + const BSTACK_OPTIONS_PATH = 'bstack:options'; + const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; + const AUTH_TOKEN_KEY = 'authToken'; + + // 1. Sanitize the top-level 'bstack:options' + if (sanitizedSession[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + sanitizedSession[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + + // 2. Sanitize the W3C 'alwaysMatch' 'bstack:options' (if present) + const alwaysMatch = sanitizedSession.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + + // 3. Sanitize in any 'firstMatch' array entries (if present) + const firstMatch = sanitizedSession.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + }); + } + + return sanitizedSession; +}; + +/** + * Removes the sensitive authToken from the text logs result object. + * The textLogsResult has a capabilities array that also needs sanitization. + * @param {object} textLogsResult - The parsed text logs result containing capabilities array. + * @returns {object} A new textLogsResult object with authTokens redacted. + */ +const sanitizeTextLogs = (textLogsResult: any) => { + if (!textLogsResult || !textLogsResult.capabilities) { + return textLogsResult; + } + + // Create a deep copy + const sanitized = JSON.parse(JSON.stringify(textLogsResult)); + + // Define the path to the auth token + const BSTACK_OPTIONS_PATH = 'bstack:options'; + const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; + const AUTH_TOKEN_KEY = 'authToken'; + + // Sanitize each capabilities object in the array + if (Array.isArray(sanitized.capabilities)) { + sanitized.capabilities.forEach((cap: any) => { + // 1. Sanitize top-level bstack:options + if (cap?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + cap[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + + // 2. Sanitize W3C_capabilities.alwaysMatch + const alwaysMatch = cap.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + + // 3. Sanitize W3C_capabilities.firstMatch array + const firstMatch = cap.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; + } + }); + } + }); + } + + return sanitized; +}; + + +// The 'Info' and 'SessionView' components are no longer needed in this file +// as 'SessionDiffView' now handles the rendering. + +export default function SessionComparison() { + // --- STATE AND HOOKS (Unchanged) --- + const [fetchSessionDetails] = usePromise(window.browserstackAPI.getAutomateSessionDetails); + const [parseTextLogs] = usePromise(window.browserstackAPI.getAutomateParsedTextLogs); + + const [session, setSession] = useState(null); + const [textLogsResult, setTextLogsResult] = useState(null); + const [seleniumLogsA, setSeleniumLogsA] = useState(null); + const [loadingSeleniumA, setLoadingSeleniumA] = useState(false); + + const [session2, setSession2] = useState(null); + const [textLogsResult2, setTextLogsResult2] = useState(null); + const [seleniumLogsB, setSeleniumLogsB] = useState(null); + const [loadingSeleniumB, setLoadingSeleniumB] = useState(false); + + const [harLogsA, setHarLogsA] = useState(null); + const [harLogsB, setHarLogsB] = useState(null); + const [loadingHarA, setLoadingHarA] = useState(false); + const [loadingHarB, setLoadingHarB] = useState(false); + + // Handles loading all data for Session A + const loadSessionA = async (sessionId: string) => { + try { + const rawSessionA = await fetchSessionDetails(sessionId); + const sessionA = sanitizeCapabilities(rawSessionA); + const rawLogsA = await parseTextLogs(rawSessionA); + const logsA = sanitizeTextLogs(rawLogsA); + setSession(sessionA); + setTextLogsResult(logsA); + + // Load Selenium logs + setLoadingSeleniumA(true); + const selA = await window.browserstackAPI.getSeleniumLogs(rawSessionA.automation_session.selenium_logs_url); + setSeleniumLogsA(selA); + setLoadingSeleniumA(false); + + // Load HAR logs + setLoadingHarA(true); + const harA = await window.browserstackAPI.getHarLogs(rawSessionA.automation_session.har_logs_url); + setHarLogsA(harA); + setLoadingHarA(false); + } catch (err) { + console.error("Error in loadSessionA:", err); + setLoadingSeleniumA(false); + setLoadingHarA(false); + throw new Error("Failed to load Session A"); + } + }; + + const loadSessionB = async (sessionId: string) => { + try { + const rawSessionB = await fetchSessionDetails(sessionId); + const sessionB = sanitizeCapabilities(rawSessionB); + const rawLogsB = await parseTextLogs(rawSessionB); + const logsB = sanitizeTextLogs(rawLogsB); + setSession2(sessionB); + setTextLogsResult2(logsB); + + // Load Selenium logs + setLoadingSeleniumB(true); + const selB = await window.browserstackAPI.getSeleniumLogs(rawSessionB.automation_session.selenium_logs_url); + setSeleniumLogsB(selB); + setLoadingSeleniumB(false); + + // Load HAR logs + setLoadingHarB(true); + const harB = await window.browserstackAPI.getHarLogs(rawSessionB.automation_session.har_logs_url); + setHarLogsB(harB); + setLoadingHarB(false); + } catch (err) { + console.error("Error in loadSessionB:", err); + setLoadingSeleniumB(false); + setLoadingHarB(false); + throw new Error("Failed to load Session B"); + } + }; + + // Handles form submission (Unchanged) + const OpenSession = (input: any) => { + // Clear previous session data + setSession(null); + setTextLogsResult(null); + setSeleniumLogsA(null); + setSession2(null); + setTextLogsResult2(null); + setSeleniumLogsB(null); + + toast.promise( + Promise.all([ + loadSessionA(input.sessionIdA), + loadSessionB(input.sessionIdB) + ]), + { + pending: "Opening Sessions...", + success: "Sessions Loaded", + error: { + render({ data }) { + console.error(data); + return "Failed to load one or more sessions. Check console."; + }, + }, + } + ); + }; + + // --- RENDER FUNCTION (Passes Sanitized Sessions to SessionDiffView) --- + return ( +
+

Session Comparison

+ +
+ + + + + + + + + +
+ +
+ {/* Show the diff view only when all data for *both* sessions is loaded */} + {session && session2 && textLogsResult && textLogsResult2 && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/temp.html b/temp.html new file mode 100644 index 0000000..39d6ffe --- /dev/null +++ b/temp.html @@ -0,0 +1,120 @@ + + + + + Build Execution Report + + + + +
+ +

Build Execution Report

+ +

Build Information

+ + + + +
StatusPASSED
Triggered Bydemo_user@example.com
Triggered At2024-03-07T07:36:02.695Z
+ +

Execution Summary

+ + + + + + + +
Total Tests12
Passed8
Failed4
Running0
Queued0
Duration (sec)1200
+ +

Build Details

+ + + +
Build ID315d0cfdf7d77a42243ff87c0d6764cc8d3f9b09
Build NameSmoke Suite-1
+ +

Test Executions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameURLBrowserVersionTypeStatusDuration (sec)
Sample Test 02 | Bstackdemo add to carthttps://bstackdemo.comchrome102.0desktopPASSED300
Sample Test 02 | Bstackdemo add to carthttps://bstackdemo.comsafari16.5desktopPASSED300
+ +
+ + + From 222ccd42b0f312cb751fbb99c3d693f090c26eb5 Mon Sep 17 00:00:00 2001 From: Adarsh2692 Date: Thu, 18 Dec 2025 16:36:24 +0530 Subject: [PATCH 2/4] added custom logic for comparison --- package-lock.json | 44 ++ package.json | 4 + .../automate/tools/CustomDiffViewer.tsx | 491 ++++++++++++++++++ src/renderer/routes/automate/tools/diff.tsx | 137 +++++ .../routes/automate/tools/diffView.tsx | 179 ++++--- 5 files changed, 781 insertions(+), 74 deletions(-) create mode 100644 src/renderer/routes/automate/tools/CustomDiffViewer.tsx create mode 100644 src/renderer/routes/automate/tools/diff.tsx diff --git a/package-lock.json b/package-lock.json index 3ab7367..58b67b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,8 @@ "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", "react-toastify": "^11.0.5", + "react-virtualized-auto-sizer": "^1.0.26", + "react-window": "^2.2.3", "sugar-high": "^0.9.4" }, "devDependencies": { @@ -40,6 +42,8 @@ "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", + "@types/react-virtualized-auto-sizer": "^1.0.4", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", @@ -2653,6 +2657,26 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-virtualized-auto-sizer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz", + "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -11646,6 +11670,26 @@ "react-dom": "^18 || ^19" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-window": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.3.tgz", + "integrity": "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", diff --git a/package.json b/package.json index d5a72f1..b97fdf0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", + "@types/react-virtualized-auto-sizer": "^1.0.4", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", @@ -65,6 +67,8 @@ "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", "react-toastify": "^11.0.5", + "react-virtualized-auto-sizer": "^1.0.26", + "react-window": "^2.2.3", "sugar-high": "^0.9.4" } } diff --git a/src/renderer/routes/automate/tools/CustomDiffViewer.tsx b/src/renderer/routes/automate/tools/CustomDiffViewer.tsx new file mode 100644 index 0000000..02189f5 --- /dev/null +++ b/src/renderer/routes/automate/tools/CustomDiffViewer.tsx @@ -0,0 +1,491 @@ +import { useMemo, useState, useEffect } from 'react'; + +interface CharDiff { + type: 'common' | 'removed' | 'added'; + text: string; +} + +interface DiffLine { + type: 'unchanged' | 'removed' | 'added' | 'modified'; + leftLine: string | null; + rightLine: string | null; + leftNumber: number | null; + rightNumber: number | null; + charDiffs?: CharDiff[]; +} + +function generateCharDiffs(oldText: string, newText: string): CharDiff[] { + const diffs: CharDiff[] = []; + let i = 0, j = 0; + + while (i < oldText.length || j < newText.length) { + let commonStart = 0; + while (i + commonStart < oldText.length && + j + commonStart < newText.length && + oldText[i + commonStart] === newText[j + commonStart]) { + commonStart++; + } + + if (commonStart > 0) { + diffs.push({ type: 'common', text: oldText.slice(i, i + commonStart) }); + i += commonStart; + j += commonStart; + continue; + } + + let oldNext = i; + let newNext = j; + let found = false; + + for (let lookAhead = 1; lookAhead <= Math.min(50, Math.max(oldText.length - i, newText.length - j)); lookAhead++) { + if (i + lookAhead < oldText.length && j < newText.length && oldText[i + lookAhead] === newText[j]) { + oldNext = i + lookAhead; + newNext = j; + found = true; + break; + } + if (j + lookAhead < newText.length && i < oldText.length && newText[j + lookAhead] === oldText[i]) { + oldNext = i; + newNext = j + lookAhead; + found = true; + break; + } + } + + if (!found) { + oldNext = Math.min(i + 1, oldText.length); + newNext = Math.min(j + 1, newText.length); + } + + if (i < oldNext) { + diffs.push({ type: 'removed', text: oldText.slice(i, oldNext) }); + i = oldNext; + } + if (j < newNext) { + diffs.push({ type: 'added', text: newText.slice(j, newNext) }); + j = newNext; + } + } + + return diffs; +} + +function calculateSimilarity(str1: string, str2: string): number { + if (!str1 || !str2) return 0; + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + // Quick structural similarity check + const words1 = str1.split(/\s+/); + const words2 = str2.split(/\s+/); + const commonWords = words1.filter(w => words2.includes(w)).length; + const totalWords = Math.max(words1.length, words2.length); + const wordSimilarity = commonWords / totalWords; + + // Character-level similarity + const editDistance = levenshteinDistance(str1, str2); + const charSimilarity = (longer.length - editDistance) / longer.length; + + // Weighted average favoring word similarity + return wordSimilarity * 0.7 + charSimilarity * 0.3; +} + +function levenshteinDistance(str1: string, str2: string): number { + const matrix = []; + + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[str2.length][str1.length]; +} + +function generateDiff(oldValue: string, newValue: string): DiffLine[] { + const oldLines = oldValue.split('\n'); + const newLines = newValue.split('\n'); + const result: DiffLine[] = []; + + let leftNum = 1; + let rightNum = 1; + let i = 0; + let j = 0; + + // Precompute similarity matrix for lookahead + const getLookaheadMatch = (oldIdx: number, newIdx: number, range: number = 10) => { + let bestMatch = { oldIdx: -1, newIdx: -1, similarity: 0.3 }; + + // Look ahead in both directions + for (let oi = oldIdx; oi < Math.min(oldIdx + range, oldLines.length); oi++) { + for (let ni = newIdx; ni < Math.min(newIdx + range, newLines.length); ni++) { + if (oi === oldIdx && ni === newIdx) continue; + const sim = calculateSimilarity(oldLines[oi], newLines[ni]); + if (sim > bestMatch.similarity) { + bestMatch = { oldIdx: oi, newIdx: ni, similarity: sim }; + } + } + } + + return bestMatch; + }; + + while (i < oldLines.length || j < newLines.length) { + // Both exhausted + if (i >= oldLines.length && j >= newLines.length) break; + + // Only old lines remain + if (i < oldLines.length && j >= newLines.length) { + result.push({ + type: 'removed', + leftLine: oldLines[i], + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + continue; + } + + // Only new lines remain + if (j < newLines.length && i >= oldLines.length) { + result.push({ + type: 'added', + leftLine: null, + rightLine: newLines[j], + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + continue; + } + + // Both lines exist + const currentOld = oldLines[i]; + const currentNew = newLines[j]; + + // Check if lines are identical + if (currentOld === currentNew) { + result.push({ + type: 'unchanged', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++ + }); + i++; + j++; + continue; + } + + // Calculate similarity for current pair + const currentSimilarity = calculateSimilarity(currentOld, currentNew); + + // Look ahead to see if there are better matches + const lookahead = getLookaheadMatch(i, j, 10); + + // Decide whether to pair current lines or skip + const shouldPairCurrent = + currentSimilarity > 0.3 && + (lookahead.similarity - currentSimilarity < 0.2 || + (lookahead.oldIdx === i && lookahead.newIdx === j)); + + if (shouldPairCurrent) { + // Pair as modified + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else if (lookahead.oldIdx > i && lookahead.newIdx === j) { + // Better match found in old lines, current old should be removed + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else if (lookahead.newIdx > j && lookahead.oldIdx === i) { + // Better match found in new lines, current new should be added + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else { + // No clear winner, check which has a better forward match + let oldHasBetterMatch = false; + let newHasBetterMatch = false; + + // Check if current old line matches better with upcoming new lines + for (let k = j + 1; k < Math.min(j + 5, newLines.length); k++) { + if (calculateSimilarity(currentOld, newLines[k]) > currentSimilarity + 0.2) { + oldHasBetterMatch = true; + break; + } + } + + // Check if current new line matches better with upcoming old lines + for (let k = i + 1; k < Math.min(i + 5, oldLines.length); k++) { + if (calculateSimilarity(oldLines[k], currentNew) > currentSimilarity + 0.2) { + newHasBetterMatch = true; + break; + } + } + + if (newHasBetterMatch && !oldHasBetterMatch) { + // Skip new line (add it) + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else if (oldHasBetterMatch && !newHasBetterMatch) { + // Skip old line (remove it) + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else { + // When in doubt, pair them if they have ANY similarity + if (currentSimilarity > 0.2) { + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else { + // Completely different - remove old + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } + } + } + } + + return result; +} + +interface CustomDiffViewerProps { + oldValue: string; + newValue: string; + leftTitle: string; + rightTitle: string; + batchSize?: number; +} + +function LineContent({ line, side }: { line: DiffLine; side: 'left' | 'right' }) { + const content = side === 'left' ? line.leftLine : line.rightLine; + + if (content === null) return ; + if (content === '') return ; + + if (line.type === 'modified' && line.charDiffs) { + return ( + + {line.charDiffs.map((diff, idx) => { + if (diff.type === 'common') { + return {diff.text}; + } else if (diff.type === 'removed' && side === 'left') { + return ( + + {diff.text} + + ); + } else if (diff.type === 'added' && side === 'right') { + return ( + + {diff.text} + + ); + } + return null; + })} + + ); + } + + return {content}; +} + +export default function CustomDiffViewer({ + oldValue, + newValue, + leftTitle, + rightTitle, + batchSize = 200 +}: CustomDiffViewerProps) { + const allDiffLines = useMemo(() => { + return generateDiff(oldValue, newValue); + }, [oldValue, newValue]); + + const [renderLimit, setRenderLimit] = useState(batchSize); + + useEffect(() => { + setRenderLimit(batchSize); + }, [oldValue, newValue, batchSize]); + + const visibleLines = allDiffLines.slice(0, renderLimit); + const remainingLines = allDiffLines.length - renderLimit; + + const handleLoadMore = () => { + setRenderLimit((prev) => Math.min(prev + batchSize, allDiffLines.length)); + }; + + const handleLoadAll = () => { + setRenderLimit(allDiffLines.length); + }; + + const getLineStyle = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return 'bg-white'; + if (type === 'removed' && side === 'left') return 'bg-red-50'; + if (type === 'added' && side === 'right') return 'bg-green-50'; + if (type === 'modified') return side === 'left' ? 'bg-orange-50' : 'bg-blue-50'; + if (type === 'removed' && side === 'right') return 'bg-gray-50'; + if (type === 'added' && side === 'left') return 'bg-gray-50'; + return 'bg-white'; + }; + + const getIndicator = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return '•'; + if (type === 'removed' && side === 'left') return '−'; + if (type === 'added' && side === 'right') return '+'; + if (type === 'modified') return '~'; + return '•'; + }; + + return ( +
+
+
+ {leftTitle} +
+
+ {rightTitle} +
+
+ +
+ {visibleLines.map((line, idx) => ( +
+
+
+ {line.leftNumber || ''} +
+
+ {getIndicator(line.type, 'left')} +
+
+ +
+
+ +
+
+ {line.rightNumber || ''} +
+
+ {getIndicator(line.type, 'right')} +
+
+ +
+
+
+ ))} + + {remainingLines > 0 && ( +
+
+ Showing {renderLimit.toLocaleString()} of {allDiffLines.length.toLocaleString()} lines. +
+
+ + {remainingLines < 5000 && ( + + )} +
+
+ )} +
+ +
+
+ + Removed + + + ~ Modified + +
+
+ + + Added + + + ~ Modified + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/diff.tsx b/src/renderer/routes/automate/tools/diff.tsx new file mode 100644 index 0000000..283fd31 --- /dev/null +++ b/src/renderer/routes/automate/tools/diff.tsx @@ -0,0 +1,137 @@ +export interface DiffLine { + type: 'added' | 'removed' | 'modified' | 'unchanged'; + leftLine?: string; + rightLine?: string; + leftNumber?: number; + rightNumber?: number; + charDiffs?: Array<{ type: 'added' | 'removed' | 'common'; text: string }>; +} + +function getCharDiff(oldStr: string, newStr: string): Array<{ type: 'added' | 'removed' | 'common'; text: string }> { + const result: Array<{ type: 'added' | 'removed' | 'common'; text: string }> = []; + + const oldChars = oldStr.split(''); + const newChars = newStr.split(''); + + const matrix: number[][] = Array(oldChars.length + 1) + .fill(0) + .map(() => Array(newChars.length + 1).fill(0)); + + for (let i = 0; i <= oldChars.length; i++) { + for (let j = 0; j <= newChars.length; j++) { + if (i === 0 || j === 0) { + matrix[i][j] = 0; + } else if (oldChars[i - 1] === newChars[j - 1]) { + matrix[i][j] = matrix[i - 1][j - 1] + 1; + } else { + matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); + } + } + } + + let i = oldChars.length; + let j = newChars.length; + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && oldChars[i - 1] === newChars[j - 1]) { + result.unshift({ type: 'common', text: oldChars[i - 1] }); + i--; + j--; + } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { + result.unshift({ type: 'added', text: newChars[j - 1] }); + j--; + } else if (i > 0) { + result.unshift({ type: 'removed', text: oldChars[i - 1] }); + i--; + } + } + + const merged: Array<{ type: 'added' | 'removed' | 'common'; text: string }> = []; + for (const item of result) { + if (merged.length > 0 && merged[merged.length - 1].type === item.type) { + merged[merged.length - 1].text += item.text; + } else { + merged.push({ ...item }); + } + } + + return merged; +} + +export function generateDiff(oldText: string, newText: string): DiffLine[] { + const oldLines = oldText.split('\n'); + const newLines = newText.split('\n'); + + const result: DiffLine[] = []; + + const matrix: number[][] = Array(oldLines.length + 1) + .fill(0) + .map(() => Array(newLines.length + 1).fill(0)); + + for (let i = 0; i <= oldLines.length; i++) { + for (let j = 0; j <= newLines.length; j++) { + if (i === 0 || j === 0) { + matrix[i][j] = 0; + } else if (oldLines[i - 1] === newLines[j - 1]) { + matrix[i][j] = matrix[i - 1][j - 1] + 1; + } else { + matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); + } + } + } + + let i = oldLines.length; + let j = newLines.length; + let oldLineNum = oldLines.length; + let newLineNum = newLines.length; + + const tempResult: DiffLine[] = []; + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { + tempResult.unshift({ + type: 'unchanged', + leftLine: oldLines[i - 1], + rightLine: newLines[j - 1], + leftNumber: oldLineNum, + rightNumber: newLineNum + }); + i--; + j--; + oldLineNum--; + newLineNum--; + } else if (i > 0 && j > 0 && oldLines[i - 1] !== newLines[j - 1]) { + const charDiffs = getCharDiff(oldLines[i - 1], newLines[j - 1]); + tempResult.unshift({ + type: 'modified', + leftLine: oldLines[i - 1], + rightLine: newLines[j - 1], + leftNumber: oldLineNum, + rightNumber: newLineNum, + charDiffs + }); + i--; + j--; + oldLineNum--; + newLineNum--; + } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { + tempResult.unshift({ + type: 'added', + rightLine: newLines[j - 1], + rightNumber: newLineNum + }); + j--; + newLineNum--; + } else if (i > 0) { + tempResult.unshift({ + type: 'removed', + leftLine: oldLines[i - 1], + leftNumber: oldLineNum + }); + i--; + oldLineNum--; + } + } + + return tempResult; +} \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/diffView.tsx b/src/renderer/routes/automate/tools/diffView.tsx index dd6d27f..a456292 100644 --- a/src/renderer/routes/automate/tools/diffView.tsx +++ b/src/renderer/routes/automate/tools/diffView.tsx @@ -1,9 +1,8 @@ import { useState, useMemo } from 'react'; -import ReactDiffViewer from 'react-diff-viewer'; +import CustomDiffViewer from './CustomDiffViewer'; function createInfoString(session: any) { if (!session?.automation_session) return "No Session Data"; - const s = session.automation_session; return `Project: ${s.project_name} Build: ${s.build_name} @@ -45,23 +44,20 @@ export function SessionDiffView({ loadingHarB }: SessionDiffViewProps) { const [activeTab, setActiveTab] = useState<'info' | 'capabilities' | 'selenium' | 'network'>('info'); + const [seleniumBatch, setSeleniumBatch] = useState(0); + const LINES_PER_BATCH = 200; const parseHarLogs = useMemo(() => (harData: string | null) => { if (!harData) return null; - try { const har = JSON.parse(harData); if (!har?.log?.entries?.length) return null; - const groupedEntries: Record = {}; har.log.entries.forEach((entry: any) => { const key = `${entry.request.method}|||${entry.request.url}`; - if (!groupedEntries[key]) { - groupedEntries[key] = []; - } + if (!groupedEntries[key]) groupedEntries[key] = []; groupedEntries[key].push(entry); }); - return groupedEntries; } catch (e) { console.error('Error parsing HAR data:', e); @@ -74,16 +70,12 @@ export function SessionDiffView({ const firstEntry = entries[0]; const response = firstEntry.response; const timings = firstEntry.timings || {}; - const statusCodes = entries.map((e: any) => e.response.status); const uniqueStatuses = [...new Set(statusCodes)]; const avgTime = entries.reduce((sum, e) => sum + (e.time || 0), 0) / entries.length; const minTime = Math.min(...entries.map((e: any) => e.time || 0)); const maxTime = Math.max(...entries.map((e: any) => e.time || 0)); - - const urlObj = new URL(url); - const shortUrl = urlObj.pathname + urlObj.search; - + return `Count: ${entries.length} | Status: ${uniqueStatuses.join(', ')} Time: ${avgTime.toFixed(1)}ms (${minTime}-${maxTime}ms) Size: ${response.bodySize || 0}b | Type: ${response.content?.mimeType || 'unknown'} @@ -96,53 +88,58 @@ Timings: ${Object.entries(timings) const networkComparison = useMemo(() => { const harLogsA = parseHarLogs(harA); const harLogsB = parseHarLogs(harB); - - if (!harLogsA && !harLogsB) { - return { matched: [], onlyInA: [], onlyInB: [] }; - } - + if (!harLogsA && !harLogsB) return { matched: [], onlyInA: [], onlyInB: [] }; const matched: Array<{ key: string; dataA: any[]; dataB: any[] }> = []; const onlyInA: Array<{ key: string; data: any[] }> = []; const onlyInB: Array<{ key: string; data: any[] }> = []; - const keysA = new Set(Object.keys(harLogsA || {})); const keysB = new Set(Object.keys(harLogsB || {})); - keysA.forEach(key => { - if (keysB.has(key)) { - matched.push({ - key, - dataA: harLogsA![key], - dataB: harLogsB![key] - }); - } else { - onlyInA.push({ - key, - data: harLogsA![key] - }); - } + if (keysB.has(key)) matched.push({ key, dataA: harLogsA![key], dataB: harLogsB![key] }); + else onlyInA.push({ key, data: harLogsA![key] }); }); - keysB.forEach(key => { - if (!keysA.has(key)) { - onlyInB.push({ - key, - data: harLogsB![key] - }); - } + if (!keysA.has(key)) onlyInB.push({ key, data: harLogsB![key] }); }); - return { matched, onlyInA, onlyInB }; }, [harA, harB, parseHarLogs]); + // Selenium log batching logic + const seleniumLogStats = useMemo(() => { + const logsA = seleniumA || ""; + const logsB = seleniumB || ""; + const linesA = logsA.split('\n'); + const linesB = logsB.split('\n'); + const maxLines = Math.max(linesA.length, linesB.length); + const totalBatches = Math.ceil(maxLines / LINES_PER_BATCH); + + return { + linesA: linesA.length, + linesB: linesB.length, + maxLines, + totalBatches, + linesA_raw: linesA, + linesB_raw: linesB + }; + }, [seleniumA, seleniumB]); + + const currentSeleniumBatch = useMemo(() => { + const { linesA_raw, linesB_raw } = seleniumLogStats; + const startLine = seleniumBatch * LINES_PER_BATCH; + const endLine = startLine + LINES_PER_BATCH; + + const batchA = linesA_raw.slice(startLine, endLine).join('\n'); + const batchB = linesB_raw.slice(startLine, endLine).join('\n'); + + return { batchA, batchB, startLine: startLine + 1, endLine: Math.min(endLine, seleniumLogStats.maxLines) }; + }, [seleniumBatch, seleniumLogStats]); + const infoStrA = createInfoString(sessionA); const infoStrB = createInfoString(sessionB); const nameA = sessionA?.automation_session?.name || "Unnamed Session A"; const nameB = sessionB?.automation_session?.name || "Unnamed Session B"; - - const capsStrA = JSON.stringify(logsA.capabilities[0], null, 2); - const capsStrB = JSON.stringify(logsB.capabilities[0], null, 2); - + const capsStrA = JSON.stringify(logsA?.capabilities?.[0] || {}, null, 2); + const capsStrB = JSON.stringify(logsB?.capabilities?.[0] || {}, null, 2); const selLogsA = loadingA ? "Loading logs for A..." : (seleniumA || "No Selenium logs available"); const selLogsB = loadingB ? "Loading logs for B..." : (seleniumB || "No Selenium logs available"); @@ -153,13 +150,24 @@ Timings: ${Object.entries(timings) { id: 'network', label: 'Network Logs' } ] as const; + // Reset batch when switching tabs + const handleTabChange = (tabId: typeof activeTab) => { + setActiveTab(tabId); + if (tabId === 'selenium') { + setSeleniumBatch(0); + } + }; + return ( -
+
{tabs.map((tab) => ( ))}
- -
+ +
{activeTab === 'info' && ( - )} - {activeTab === 'capabilities' && ( - )} - {activeTab === 'selenium' && ( - +
+ {/* Batch navigation controls */} + {!loadingA && !loadingB && seleniumLogStats.totalBatches > 1 && ( +
+
+ + Lines {currentSeleniumBatch.startLine} - {currentSeleniumBatch.endLine} of {seleniumLogStats.maxLines} + + + (Batch {seleniumBatch + 1} of {seleniumLogStats.totalBatches}) + +
+
+ + +
+
+ )} + + {/* Diff viewer for current batch */} +
+ +
+
)} - {activeTab === 'network' && ( -
+
{loadingHarA || loadingHarB ? (
Loading network logs... @@ -221,8 +258,7 @@ Timings: ${Object.entries(timings) {networkComparison.matched.map((item, idx) => { const [method, url] = item.key.split('|||'); const urlObj = new URL(url); - const shortUrl = urlObj.pathname + urlObj.search; - + const shortUrl = urlObj.origin + urlObj.pathname + urlObj.search; return (
@@ -247,7 +283,6 @@ Timings: ${Object.entries(timings) })}
)} - {networkComparison.onlyInA.length > 0 && (

@@ -257,7 +292,6 @@ Timings: ${Object.entries(timings) const [method, url] = item.key.split('|||'); const urlObj = new URL(url); const shortUrl = urlObj.pathname + urlObj.search; - return (
@@ -271,7 +305,6 @@ Timings: ${Object.entries(timings) })}
)} - {networkComparison.onlyInB.length > 0 && (

@@ -281,7 +314,6 @@ Timings: ${Object.entries(timings) const [method, url] = item.key.split('|||'); const urlObj = new URL(url); const shortUrl = urlObj.pathname + urlObj.search; - return (
@@ -295,7 +327,6 @@ Timings: ${Object.entries(timings) })}
)} - {networkComparison.matched.length === 0 && networkComparison.onlyInA.length === 0 && networkComparison.onlyInB.length === 0 && ( @@ -310,4 +341,4 @@ Timings: ${Object.entries(timings)

); -} +} \ No newline at end of file From d4abd7552e34fbaa26e27878f5aed927aadcaa52 Mon Sep 17 00:00:00 2001 From: Adarsh2692 Date: Thu, 18 Dec 2025 17:23:17 +0530 Subject: [PATCH 3/4] code re-structuring for session comparison --- .../automate/tools/CustomDiffViewer.tsx | 491 ------------------ src/renderer/routes/automate/tools/diff.tsx | 137 ----- .../automate/tools/session-comparison.tsx | 253 +-------- .../components/DiffViewer.tsx | 178 +++++++ .../components/SessionDiffView.tsx} | 115 ++-- .../tools/session-comparison/index.tsx | 145 ++++++ .../tools/session-comparison/types.ts | 65 +++ .../session-comparison/utils/diffAlgorithm.ts | 278 ++++++++++ .../session-comparison/utils/networkParser.ts | 61 +++ .../session-comparison/utils/sanitization.ts | 64 +++ 10 files changed, 821 insertions(+), 966 deletions(-) delete mode 100644 src/renderer/routes/automate/tools/CustomDiffViewer.tsx delete mode 100644 src/renderer/routes/automate/tools/diff.tsx create mode 100644 src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx rename src/renderer/routes/automate/tools/{diffView.tsx => session-comparison/components/SessionDiffView.tsx} (78%) create mode 100644 src/renderer/routes/automate/tools/session-comparison/index.tsx create mode 100644 src/renderer/routes/automate/tools/session-comparison/types.ts create mode 100644 src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts create mode 100644 src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts create mode 100644 src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts diff --git a/src/renderer/routes/automate/tools/CustomDiffViewer.tsx b/src/renderer/routes/automate/tools/CustomDiffViewer.tsx deleted file mode 100644 index 02189f5..0000000 --- a/src/renderer/routes/automate/tools/CustomDiffViewer.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import { useMemo, useState, useEffect } from 'react'; - -interface CharDiff { - type: 'common' | 'removed' | 'added'; - text: string; -} - -interface DiffLine { - type: 'unchanged' | 'removed' | 'added' | 'modified'; - leftLine: string | null; - rightLine: string | null; - leftNumber: number | null; - rightNumber: number | null; - charDiffs?: CharDiff[]; -} - -function generateCharDiffs(oldText: string, newText: string): CharDiff[] { - const diffs: CharDiff[] = []; - let i = 0, j = 0; - - while (i < oldText.length || j < newText.length) { - let commonStart = 0; - while (i + commonStart < oldText.length && - j + commonStart < newText.length && - oldText[i + commonStart] === newText[j + commonStart]) { - commonStart++; - } - - if (commonStart > 0) { - diffs.push({ type: 'common', text: oldText.slice(i, i + commonStart) }); - i += commonStart; - j += commonStart; - continue; - } - - let oldNext = i; - let newNext = j; - let found = false; - - for (let lookAhead = 1; lookAhead <= Math.min(50, Math.max(oldText.length - i, newText.length - j)); lookAhead++) { - if (i + lookAhead < oldText.length && j < newText.length && oldText[i + lookAhead] === newText[j]) { - oldNext = i + lookAhead; - newNext = j; - found = true; - break; - } - if (j + lookAhead < newText.length && i < oldText.length && newText[j + lookAhead] === oldText[i]) { - oldNext = i; - newNext = j + lookAhead; - found = true; - break; - } - } - - if (!found) { - oldNext = Math.min(i + 1, oldText.length); - newNext = Math.min(j + 1, newText.length); - } - - if (i < oldNext) { - diffs.push({ type: 'removed', text: oldText.slice(i, oldNext) }); - i = oldNext; - } - if (j < newNext) { - diffs.push({ type: 'added', text: newText.slice(j, newNext) }); - j = newNext; - } - } - - return diffs; -} - -function calculateSimilarity(str1: string, str2: string): number { - if (!str1 || !str2) return 0; - - const longer = str1.length > str2.length ? str1 : str2; - const shorter = str1.length > str2.length ? str2 : str1; - - if (longer.length === 0) return 1.0; - - // Quick structural similarity check - const words1 = str1.split(/\s+/); - const words2 = str2.split(/\s+/); - const commonWords = words1.filter(w => words2.includes(w)).length; - const totalWords = Math.max(words1.length, words2.length); - const wordSimilarity = commonWords / totalWords; - - // Character-level similarity - const editDistance = levenshteinDistance(str1, str2); - const charSimilarity = (longer.length - editDistance) / longer.length; - - // Weighted average favoring word similarity - return wordSimilarity * 0.7 + charSimilarity * 0.3; -} - -function levenshteinDistance(str1: string, str2: string): number { - const matrix = []; - - for (let i = 0; i <= str2.length; i++) { - matrix[i] = [i]; - } - - for (let j = 0; j <= str1.length; j++) { - matrix[0][j] = j; - } - - for (let i = 1; i <= str2.length; i++) { - for (let j = 1; j <= str1.length; j++) { - if (str2.charAt(i - 1) === str1.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min( - matrix[i - 1][j - 1] + 1, - matrix[i][j - 1] + 1, - matrix[i - 1][j] + 1 - ); - } - } - } - - return matrix[str2.length][str1.length]; -} - -function generateDiff(oldValue: string, newValue: string): DiffLine[] { - const oldLines = oldValue.split('\n'); - const newLines = newValue.split('\n'); - const result: DiffLine[] = []; - - let leftNum = 1; - let rightNum = 1; - let i = 0; - let j = 0; - - // Precompute similarity matrix for lookahead - const getLookaheadMatch = (oldIdx: number, newIdx: number, range: number = 10) => { - let bestMatch = { oldIdx: -1, newIdx: -1, similarity: 0.3 }; - - // Look ahead in both directions - for (let oi = oldIdx; oi < Math.min(oldIdx + range, oldLines.length); oi++) { - for (let ni = newIdx; ni < Math.min(newIdx + range, newLines.length); ni++) { - if (oi === oldIdx && ni === newIdx) continue; - const sim = calculateSimilarity(oldLines[oi], newLines[ni]); - if (sim > bestMatch.similarity) { - bestMatch = { oldIdx: oi, newIdx: ni, similarity: sim }; - } - } - } - - return bestMatch; - }; - - while (i < oldLines.length || j < newLines.length) { - // Both exhausted - if (i >= oldLines.length && j >= newLines.length) break; - - // Only old lines remain - if (i < oldLines.length && j >= newLines.length) { - result.push({ - type: 'removed', - leftLine: oldLines[i], - rightLine: null, - leftNumber: leftNum++, - rightNumber: null - }); - i++; - continue; - } - - // Only new lines remain - if (j < newLines.length && i >= oldLines.length) { - result.push({ - type: 'added', - leftLine: null, - rightLine: newLines[j], - leftNumber: null, - rightNumber: rightNum++ - }); - j++; - continue; - } - - // Both lines exist - const currentOld = oldLines[i]; - const currentNew = newLines[j]; - - // Check if lines are identical - if (currentOld === currentNew) { - result.push({ - type: 'unchanged', - leftLine: currentOld, - rightLine: currentNew, - leftNumber: leftNum++, - rightNumber: rightNum++ - }); - i++; - j++; - continue; - } - - // Calculate similarity for current pair - const currentSimilarity = calculateSimilarity(currentOld, currentNew); - - // Look ahead to see if there are better matches - const lookahead = getLookaheadMatch(i, j, 10); - - // Decide whether to pair current lines or skip - const shouldPairCurrent = - currentSimilarity > 0.3 && - (lookahead.similarity - currentSimilarity < 0.2 || - (lookahead.oldIdx === i && lookahead.newIdx === j)); - - if (shouldPairCurrent) { - // Pair as modified - const charDiffs = generateCharDiffs(currentOld, currentNew); - result.push({ - type: 'modified', - leftLine: currentOld, - rightLine: currentNew, - leftNumber: leftNum++, - rightNumber: rightNum++, - charDiffs - }); - i++; - j++; - } else if (lookahead.oldIdx > i && lookahead.newIdx === j) { - // Better match found in old lines, current old should be removed - result.push({ - type: 'removed', - leftLine: currentOld, - rightLine: null, - leftNumber: leftNum++, - rightNumber: null - }); - i++; - } else if (lookahead.newIdx > j && lookahead.oldIdx === i) { - // Better match found in new lines, current new should be added - result.push({ - type: 'added', - leftLine: null, - rightLine: currentNew, - leftNumber: null, - rightNumber: rightNum++ - }); - j++; - } else { - // No clear winner, check which has a better forward match - let oldHasBetterMatch = false; - let newHasBetterMatch = false; - - // Check if current old line matches better with upcoming new lines - for (let k = j + 1; k < Math.min(j + 5, newLines.length); k++) { - if (calculateSimilarity(currentOld, newLines[k]) > currentSimilarity + 0.2) { - oldHasBetterMatch = true; - break; - } - } - - // Check if current new line matches better with upcoming old lines - for (let k = i + 1; k < Math.min(i + 5, oldLines.length); k++) { - if (calculateSimilarity(oldLines[k], currentNew) > currentSimilarity + 0.2) { - newHasBetterMatch = true; - break; - } - } - - if (newHasBetterMatch && !oldHasBetterMatch) { - // Skip new line (add it) - result.push({ - type: 'added', - leftLine: null, - rightLine: currentNew, - leftNumber: null, - rightNumber: rightNum++ - }); - j++; - } else if (oldHasBetterMatch && !newHasBetterMatch) { - // Skip old line (remove it) - result.push({ - type: 'removed', - leftLine: currentOld, - rightLine: null, - leftNumber: leftNum++, - rightNumber: null - }); - i++; - } else { - // When in doubt, pair them if they have ANY similarity - if (currentSimilarity > 0.2) { - const charDiffs = generateCharDiffs(currentOld, currentNew); - result.push({ - type: 'modified', - leftLine: currentOld, - rightLine: currentNew, - leftNumber: leftNum++, - rightNumber: rightNum++, - charDiffs - }); - i++; - j++; - } else { - // Completely different - remove old - result.push({ - type: 'removed', - leftLine: currentOld, - rightLine: null, - leftNumber: leftNum++, - rightNumber: null - }); - i++; - } - } - } - } - - return result; -} - -interface CustomDiffViewerProps { - oldValue: string; - newValue: string; - leftTitle: string; - rightTitle: string; - batchSize?: number; -} - -function LineContent({ line, side }: { line: DiffLine; side: 'left' | 'right' }) { - const content = side === 'left' ? line.leftLine : line.rightLine; - - if (content === null) return ; - if (content === '') return ; - - if (line.type === 'modified' && line.charDiffs) { - return ( - - {line.charDiffs.map((diff, idx) => { - if (diff.type === 'common') { - return {diff.text}; - } else if (diff.type === 'removed' && side === 'left') { - return ( - - {diff.text} - - ); - } else if (diff.type === 'added' && side === 'right') { - return ( - - {diff.text} - - ); - } - return null; - })} - - ); - } - - return {content}; -} - -export default function CustomDiffViewer({ - oldValue, - newValue, - leftTitle, - rightTitle, - batchSize = 200 -}: CustomDiffViewerProps) { - const allDiffLines = useMemo(() => { - return generateDiff(oldValue, newValue); - }, [oldValue, newValue]); - - const [renderLimit, setRenderLimit] = useState(batchSize); - - useEffect(() => { - setRenderLimit(batchSize); - }, [oldValue, newValue, batchSize]); - - const visibleLines = allDiffLines.slice(0, renderLimit); - const remainingLines = allDiffLines.length - renderLimit; - - const handleLoadMore = () => { - setRenderLimit((prev) => Math.min(prev + batchSize, allDiffLines.length)); - }; - - const handleLoadAll = () => { - setRenderLimit(allDiffLines.length); - }; - - const getLineStyle = (type: DiffLine['type'], side: 'left' | 'right') => { - if (type === 'unchanged') return 'bg-white'; - if (type === 'removed' && side === 'left') return 'bg-red-50'; - if (type === 'added' && side === 'right') return 'bg-green-50'; - if (type === 'modified') return side === 'left' ? 'bg-orange-50' : 'bg-blue-50'; - if (type === 'removed' && side === 'right') return 'bg-gray-50'; - if (type === 'added' && side === 'left') return 'bg-gray-50'; - return 'bg-white'; - }; - - const getIndicator = (type: DiffLine['type'], side: 'left' | 'right') => { - if (type === 'unchanged') return '•'; - if (type === 'removed' && side === 'left') return '−'; - if (type === 'added' && side === 'right') return '+'; - if (type === 'modified') return '~'; - return '•'; - }; - - return ( -
-
-
- {leftTitle} -
-
- {rightTitle} -
-
- -
- {visibleLines.map((line, idx) => ( -
-
-
- {line.leftNumber || ''} -
-
- {getIndicator(line.type, 'left')} -
-
- -
-
- -
-
- {line.rightNumber || ''} -
-
- {getIndicator(line.type, 'right')} -
-
- -
-
-
- ))} - - {remainingLines > 0 && ( -
-
- Showing {renderLimit.toLocaleString()} of {allDiffLines.length.toLocaleString()} lines. -
-
- - {remainingLines < 5000 && ( - - )} -
-
- )} -
- -
-
- - Removed - - - ~ Modified - -
-
- - + Added - - - ~ Modified - -
-
-
- ); -} \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/diff.tsx b/src/renderer/routes/automate/tools/diff.tsx deleted file mode 100644 index 283fd31..0000000 --- a/src/renderer/routes/automate/tools/diff.tsx +++ /dev/null @@ -1,137 +0,0 @@ -export interface DiffLine { - type: 'added' | 'removed' | 'modified' | 'unchanged'; - leftLine?: string; - rightLine?: string; - leftNumber?: number; - rightNumber?: number; - charDiffs?: Array<{ type: 'added' | 'removed' | 'common'; text: string }>; -} - -function getCharDiff(oldStr: string, newStr: string): Array<{ type: 'added' | 'removed' | 'common'; text: string }> { - const result: Array<{ type: 'added' | 'removed' | 'common'; text: string }> = []; - - const oldChars = oldStr.split(''); - const newChars = newStr.split(''); - - const matrix: number[][] = Array(oldChars.length + 1) - .fill(0) - .map(() => Array(newChars.length + 1).fill(0)); - - for (let i = 0; i <= oldChars.length; i++) { - for (let j = 0; j <= newChars.length; j++) { - if (i === 0 || j === 0) { - matrix[i][j] = 0; - } else if (oldChars[i - 1] === newChars[j - 1]) { - matrix[i][j] = matrix[i - 1][j - 1] + 1; - } else { - matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); - } - } - } - - let i = oldChars.length; - let j = newChars.length; - - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldChars[i - 1] === newChars[j - 1]) { - result.unshift({ type: 'common', text: oldChars[i - 1] }); - i--; - j--; - } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { - result.unshift({ type: 'added', text: newChars[j - 1] }); - j--; - } else if (i > 0) { - result.unshift({ type: 'removed', text: oldChars[i - 1] }); - i--; - } - } - - const merged: Array<{ type: 'added' | 'removed' | 'common'; text: string }> = []; - for (const item of result) { - if (merged.length > 0 && merged[merged.length - 1].type === item.type) { - merged[merged.length - 1].text += item.text; - } else { - merged.push({ ...item }); - } - } - - return merged; -} - -export function generateDiff(oldText: string, newText: string): DiffLine[] { - const oldLines = oldText.split('\n'); - const newLines = newText.split('\n'); - - const result: DiffLine[] = []; - - const matrix: number[][] = Array(oldLines.length + 1) - .fill(0) - .map(() => Array(newLines.length + 1).fill(0)); - - for (let i = 0; i <= oldLines.length; i++) { - for (let j = 0; j <= newLines.length; j++) { - if (i === 0 || j === 0) { - matrix[i][j] = 0; - } else if (oldLines[i - 1] === newLines[j - 1]) { - matrix[i][j] = matrix[i - 1][j - 1] + 1; - } else { - matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); - } - } - } - - let i = oldLines.length; - let j = newLines.length; - let oldLineNum = oldLines.length; - let newLineNum = newLines.length; - - const tempResult: DiffLine[] = []; - - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { - tempResult.unshift({ - type: 'unchanged', - leftLine: oldLines[i - 1], - rightLine: newLines[j - 1], - leftNumber: oldLineNum, - rightNumber: newLineNum - }); - i--; - j--; - oldLineNum--; - newLineNum--; - } else if (i > 0 && j > 0 && oldLines[i - 1] !== newLines[j - 1]) { - const charDiffs = getCharDiff(oldLines[i - 1], newLines[j - 1]); - tempResult.unshift({ - type: 'modified', - leftLine: oldLines[i - 1], - rightLine: newLines[j - 1], - leftNumber: oldLineNum, - rightNumber: newLineNum, - charDiffs - }); - i--; - j--; - oldLineNum--; - newLineNum--; - } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { - tempResult.unshift({ - type: 'added', - rightLine: newLines[j - 1], - rightNumber: newLineNum - }); - j--; - newLineNum--; - } else if (i > 0) { - tempResult.unshift({ - type: 'removed', - leftLine: oldLines[i - 1], - leftNumber: oldLineNum - }); - i--; - oldLineNum--; - } - } - - return tempResult; -} \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/session-comparison.tsx b/src/renderer/routes/automate/tools/session-comparison.tsx index 3c2b7a4..442b47c 100644 --- a/src/renderer/routes/automate/tools/session-comparison.tsx +++ b/src/renderer/routes/automate/tools/session-comparison.tsx @@ -1,252 +1 @@ -import Form from "rc-field-form"; -import { usePromise } from "../../../hooks/use-promise"; -import { toast } from "react-toastify"; -import Editor from 'react-simple-code-editor'; -import { highlight } from 'sugar-high'; -import { useState } from "react"; -import { SessionDiffView } from './diffView'; // 👈 Import the new diff component - -const { Field } = Form; - -// --- UTILITY FUNCTION TO SANITIZE CAPABILITIES --- - -/** - * Removes the sensitive authToken from the capabilities object. - * @param {object} session - The session object containing capabilities. - * @returns {object} A new session object with the authToken replaced. - */ -const sanitizeCapabilities = (session: any) => { - if (!session) { - return session; - } - - // Create a deep copy to avoid modifying the original state object directly - const sanitizedSession = JSON.parse(JSON.stringify(session)); - - // Define the path to the auth token within the capabilities structure - const BSTACK_OPTIONS_PATH = 'bstack:options'; - const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; - const AUTH_TOKEN_KEY = 'authToken'; - - // 1. Sanitize the top-level 'bstack:options' - if (sanitizedSession[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - sanitizedSession[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - - // 2. Sanitize the W3C 'alwaysMatch' 'bstack:options' (if present) - const alwaysMatch = sanitizedSession.W3C_capabilities?.alwaysMatch; - if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - - // 3. Sanitize in any 'firstMatch' array entries (if present) - const firstMatch = sanitizedSession.W3C_capabilities?.firstMatch; - if (Array.isArray(firstMatch)) { - firstMatch.forEach((match: any) => { - if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - }); - } - - return sanitizedSession; -}; - -/** - * Removes the sensitive authToken from the text logs result object. - * The textLogsResult has a capabilities array that also needs sanitization. - * @param {object} textLogsResult - The parsed text logs result containing capabilities array. - * @returns {object} A new textLogsResult object with authTokens redacted. - */ -const sanitizeTextLogs = (textLogsResult: any) => { - if (!textLogsResult || !textLogsResult.capabilities) { - return textLogsResult; - } - - // Create a deep copy - const sanitized = JSON.parse(JSON.stringify(textLogsResult)); - - // Define the path to the auth token - const BSTACK_OPTIONS_PATH = 'bstack:options'; - const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; - const AUTH_TOKEN_KEY = 'authToken'; - - // Sanitize each capabilities object in the array - if (Array.isArray(sanitized.capabilities)) { - sanitized.capabilities.forEach((cap: any) => { - // 1. Sanitize top-level bstack:options - if (cap?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - cap[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - - // 2. Sanitize W3C_capabilities.alwaysMatch - const alwaysMatch = cap.W3C_capabilities?.alwaysMatch; - if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - - // 3. Sanitize W3C_capabilities.firstMatch array - const firstMatch = cap.W3C_capabilities?.firstMatch; - if (Array.isArray(firstMatch)) { - firstMatch.forEach((match: any) => { - if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { - match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = '***REDACTED***'; - } - }); - } - }); - } - - return sanitized; -}; - - -// The 'Info' and 'SessionView' components are no longer needed in this file -// as 'SessionDiffView' now handles the rendering. - -export default function SessionComparison() { - // --- STATE AND HOOKS (Unchanged) --- - const [fetchSessionDetails] = usePromise(window.browserstackAPI.getAutomateSessionDetails); - const [parseTextLogs] = usePromise(window.browserstackAPI.getAutomateParsedTextLogs); - - const [session, setSession] = useState(null); - const [textLogsResult, setTextLogsResult] = useState(null); - const [seleniumLogsA, setSeleniumLogsA] = useState(null); - const [loadingSeleniumA, setLoadingSeleniumA] = useState(false); - - const [session2, setSession2] = useState(null); - const [textLogsResult2, setTextLogsResult2] = useState(null); - const [seleniumLogsB, setSeleniumLogsB] = useState(null); - const [loadingSeleniumB, setLoadingSeleniumB] = useState(false); - - const [harLogsA, setHarLogsA] = useState(null); - const [harLogsB, setHarLogsB] = useState(null); - const [loadingHarA, setLoadingHarA] = useState(false); - const [loadingHarB, setLoadingHarB] = useState(false); - - // Handles loading all data for Session A - const loadSessionA = async (sessionId: string) => { - try { - const rawSessionA = await fetchSessionDetails(sessionId); - const sessionA = sanitizeCapabilities(rawSessionA); - const rawLogsA = await parseTextLogs(rawSessionA); - const logsA = sanitizeTextLogs(rawLogsA); - setSession(sessionA); - setTextLogsResult(logsA); - - // Load Selenium logs - setLoadingSeleniumA(true); - const selA = await window.browserstackAPI.getSeleniumLogs(rawSessionA.automation_session.selenium_logs_url); - setSeleniumLogsA(selA); - setLoadingSeleniumA(false); - - // Load HAR logs - setLoadingHarA(true); - const harA = await window.browserstackAPI.getHarLogs(rawSessionA.automation_session.har_logs_url); - setHarLogsA(harA); - setLoadingHarA(false); - } catch (err) { - console.error("Error in loadSessionA:", err); - setLoadingSeleniumA(false); - setLoadingHarA(false); - throw new Error("Failed to load Session A"); - } - }; - - const loadSessionB = async (sessionId: string) => { - try { - const rawSessionB = await fetchSessionDetails(sessionId); - const sessionB = sanitizeCapabilities(rawSessionB); - const rawLogsB = await parseTextLogs(rawSessionB); - const logsB = sanitizeTextLogs(rawLogsB); - setSession2(sessionB); - setTextLogsResult2(logsB); - - // Load Selenium logs - setLoadingSeleniumB(true); - const selB = await window.browserstackAPI.getSeleniumLogs(rawSessionB.automation_session.selenium_logs_url); - setSeleniumLogsB(selB); - setLoadingSeleniumB(false); - - // Load HAR logs - setLoadingHarB(true); - const harB = await window.browserstackAPI.getHarLogs(rawSessionB.automation_session.har_logs_url); - setHarLogsB(harB); - setLoadingHarB(false); - } catch (err) { - console.error("Error in loadSessionB:", err); - setLoadingSeleniumB(false); - setLoadingHarB(false); - throw new Error("Failed to load Session B"); - } - }; - - // Handles form submission (Unchanged) - const OpenSession = (input: any) => { - // Clear previous session data - setSession(null); - setTextLogsResult(null); - setSeleniumLogsA(null); - setSession2(null); - setTextLogsResult2(null); - setSeleniumLogsB(null); - - toast.promise( - Promise.all([ - loadSessionA(input.sessionIdA), - loadSessionB(input.sessionIdB) - ]), - { - pending: "Opening Sessions...", - success: "Sessions Loaded", - error: { - render({ data }) { - console.error(data); - return "Failed to load one or more sessions. Check console."; - }, - }, - } - ); - }; - - // --- RENDER FUNCTION (Passes Sanitized Sessions to SessionDiffView) --- - return ( -
-

Session Comparison

- -
- - - - - - - - - -
- -
- {/* Show the diff view only when all data for *both* sessions is loaded */} - {session && session2 && textLogsResult && textLogsResult2 && ( - - )} -
-
- ); -} \ No newline at end of file +export { default } from './session-comparison/index'; \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx b/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx new file mode 100644 index 0000000..2a08017 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx @@ -0,0 +1,178 @@ +import { useMemo, useState, useEffect } from 'react'; +import { DiffLine } from '../types'; +import { generateDiff } from '../utils/diffAlgorithm'; + +interface DiffViewerProps { + oldValue: string; + newValue: string; + leftTitle: string; + rightTitle: string; + batchSize?: number; +} + +function LineContent({ line, side }: { line: DiffLine; side: 'left' | 'right' }) { + const content = side === 'left' ? line.leftLine : line.rightLine; + + if (content === null) return ; + if (content === '') return ; + + if (line.type === 'modified' && line.charDiffs) { + return ( + + {line.charDiffs.map((diff, idx) => { + if (diff.type === 'common') { + return {diff.text}; + } else if (diff.type === 'removed' && side === 'left') { + return ( + + {diff.text} + + ); + } else if (diff.type === 'added' && side === 'right') { + return ( + + {diff.text} + + ); + } + return null; + })} + + ); + } + + return {content}; +} + +export default function DiffViewer({ + oldValue, + newValue, + leftTitle, + rightTitle, + batchSize = 200 +}: DiffViewerProps) { + const allDiffLines = useMemo(() => { + return generateDiff(oldValue, newValue); + }, [oldValue, newValue]); + + const [renderLimit, setRenderLimit] = useState(batchSize); + + useEffect(() => { + setRenderLimit(batchSize); + }, [oldValue, newValue, batchSize]); + + const visibleLines = allDiffLines.slice(0, renderLimit); + const remainingLines = allDiffLines.length - renderLimit; + + const handleLoadMore = () => { + setRenderLimit((prev) => Math.min(prev + batchSize, allDiffLines.length)); + }; + + const handleLoadAll = () => { + setRenderLimit(allDiffLines.length); + }; + + const getLineStyle = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return 'bg-white'; + if (type === 'removed' && side === 'left') return 'bg-red-50'; + if (type === 'added' && side === 'right') return 'bg-green-50'; + if (type === 'modified') return side === 'left' ? 'bg-orange-50' : 'bg-blue-50'; + if (type === 'removed' && side === 'right') return 'bg-gray-50'; + if (type === 'added' && side === 'left') return 'bg-gray-50'; + return 'bg-white'; + }; + + const getIndicator = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return '•'; + if (type === 'removed' && side === 'left') return '−'; + if (type === 'added' && side === 'right') return '+'; + if (type === 'modified') return '~'; + return '•'; + }; + + return ( +
+
+
+ {leftTitle} +
+
+ {rightTitle} +
+
+ +
+ {visibleLines.map((line, idx) => ( +
+
+
+ {line.leftNumber || ''} +
+
+ {getIndicator(line.type, 'left')} +
+
+ +
+
+ +
+
+ {line.rightNumber || ''} +
+
+ {getIndicator(line.type, 'right')} +
+
+ +
+
+
+ ))} + + {remainingLines > 0 && ( +
+
+ Showing {renderLimit.toLocaleString()} of {allDiffLines.length.toLocaleString()} lines. +
+
+ + {remainingLines < 5000 && ( + + )} +
+
+ )} +
+ +
+
+ + Removed + + + ~ Modified + +
+
+ + + Added + + + ~ Modified + +
+
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/diffView.tsx b/src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx similarity index 78% rename from src/renderer/routes/automate/tools/diffView.tsx rename to src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx index a456292..69e62bc 100644 --- a/src/renderer/routes/automate/tools/diffView.tsx +++ b/src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx @@ -1,7 +1,24 @@ import { useState, useMemo } from 'react'; -import CustomDiffViewer from './CustomDiffViewer'; +import DiffViewer from './DiffViewer'; +import { SessionData, TextLogsResult } from '../types'; +import { compareNetworkLogs, formatNetworkEntry } from '../utils/networkParser'; -function createInfoString(session: any) { +interface SessionDiffViewProps { + sessionA: SessionData; + sessionB: SessionData; + logsA: TextLogsResult; + logsB: TextLogsResult; + seleniumA: string | null; + seleniumB: string | null; + harA: string | null; + harB: string | null; + loadingA: boolean; + loadingB: boolean; + loadingHarA: boolean; + loadingHarB: boolean; +} + +function createInfoString(session: SessionData) { if (!session?.automation_session) return "No Session Data"; const s = session.automation_session; return `Project: ${s.project_name} @@ -14,22 +31,7 @@ Duration: ${s.duration}s Created At: ${new Date(s.created_at).toLocaleString()}`.trim(); } -interface SessionDiffViewProps { - sessionA: any; - sessionB: any; - logsA: any; - logsB: any; - seleniumA: string | null; - seleniumB: string | null; - harA: string | null; - harB: string | null; - loadingA: boolean; - loadingB: boolean; - loadingHarA: boolean; - loadingHarB: boolean; -} - -export function SessionDiffView({ +export default function SessionDiffView({ sessionA, sessionB, logsA, @@ -47,64 +49,10 @@ export function SessionDiffView({ const [seleniumBatch, setSeleniumBatch] = useState(0); const LINES_PER_BATCH = 200; - const parseHarLogs = useMemo(() => (harData: string | null) => { - if (!harData) return null; - try { - const har = JSON.parse(harData); - if (!har?.log?.entries?.length) return null; - const groupedEntries: Record = {}; - har.log.entries.forEach((entry: any) => { - const key = `${entry.request.method}|||${entry.request.url}`; - if (!groupedEntries[key]) groupedEntries[key] = []; - groupedEntries[key].push(entry); - }); - return groupedEntries; - } catch (e) { - console.error('Error parsing HAR data:', e); - return null; - } - }, []); - - const formatNetworkEntry = (key: string, entries: any[]): string => { - const [method, url] = key.split('|||'); - const firstEntry = entries[0]; - const response = firstEntry.response; - const timings = firstEntry.timings || {}; - const statusCodes = entries.map((e: any) => e.response.status); - const uniqueStatuses = [...new Set(statusCodes)]; - const avgTime = entries.reduce((sum, e) => sum + (e.time || 0), 0) / entries.length; - const minTime = Math.min(...entries.map((e: any) => e.time || 0)); - const maxTime = Math.max(...entries.map((e: any) => e.time || 0)); - - return `Count: ${entries.length} | Status: ${uniqueStatuses.join(', ')} -Time: ${avgTime.toFixed(1)}ms (${minTime}-${maxTime}ms) -Size: ${response.bodySize || 0}b | Type: ${response.content?.mimeType || 'unknown'} -Timings: ${Object.entries(timings) - .filter(([_, v]) => v !== -1) - .map(([k, v]) => `${k}:${v}ms`) - .join(' ')}`; - }; - const networkComparison = useMemo(() => { - const harLogsA = parseHarLogs(harA); - const harLogsB = parseHarLogs(harB); - if (!harLogsA && !harLogsB) return { matched: [], onlyInA: [], onlyInB: [] }; - const matched: Array<{ key: string; dataA: any[]; dataB: any[] }> = []; - const onlyInA: Array<{ key: string; data: any[] }> = []; - const onlyInB: Array<{ key: string; data: any[] }> = []; - const keysA = new Set(Object.keys(harLogsA || {})); - const keysB = new Set(Object.keys(harLogsB || {})); - keysA.forEach(key => { - if (keysB.has(key)) matched.push({ key, dataA: harLogsA![key], dataB: harLogsB![key] }); - else onlyInA.push({ key, data: harLogsA![key] }); - }); - keysB.forEach(key => { - if (!keysA.has(key)) onlyInB.push({ key, data: harLogsB![key] }); - }); - return { matched, onlyInA, onlyInB }; - }, [harA, harB, parseHarLogs]); + return compareNetworkLogs(harA, harB); + }, [harA, harB]); - // Selenium log batching logic const seleniumLogStats = useMemo(() => { const logsA = seleniumA || ""; const logsB = seleniumB || ""; @@ -140,17 +88,14 @@ Timings: ${Object.entries(timings) const nameB = sessionB?.automation_session?.name || "Unnamed Session B"; const capsStrA = JSON.stringify(logsA?.capabilities?.[0] || {}, null, 2); const capsStrB = JSON.stringify(logsB?.capabilities?.[0] || {}, null, 2); - const selLogsA = loadingA ? "Loading logs for A..." : (seleniumA || "No Selenium logs available"); - const selLogsB = loadingB ? "Loading logs for B..." : (seleniumB || "No Selenium logs available"); const tabs = [ { id: 'info', label: 'Session Info' }, { id: 'capabilities', label: 'Capabilities' }, - { id: 'selenium', label: 'Selenium Logs' }, + // { id: 'selenium', label: 'Selenium Logs' }, { id: 'network', label: 'Network Logs' } ] as const; - // Reset batch when switching tabs const handleTabChange = (tabId: typeof activeTab) => { setActiveTab(tabId); if (tabId === 'selenium') { @@ -181,7 +126,7 @@ Timings: ${Object.entries(timings)
{activeTab === 'info' && ( - )} {activeTab === 'capabilities' && ( - )} - {activeTab === 'selenium' && ( + {/* {activeTab === 'selenium' && (
- {/* Batch navigation controls */} {!loadingA && !loadingB && seleniumLogStats.totalBatches > 1 && (
@@ -230,9 +174,8 @@ Timings: ${Object.entries(timings)
)} - {/* Diff viewer for current batch */}
-
- )} + )} */} {activeTab === 'network' && (
{loadingHarA || loadingHarB ? ( @@ -341,4 +284,4 @@ Timings: ${Object.entries(timings)
); -} \ No newline at end of file +} diff --git a/src/renderer/routes/automate/tools/session-comparison/index.tsx b/src/renderer/routes/automate/tools/session-comparison/index.tsx new file mode 100644 index 0000000..33a304f --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/index.tsx @@ -0,0 +1,145 @@ +import Form from "rc-field-form"; +import { usePromise } from "../../../../hooks/use-promise"; +import { toast } from "react-toastify"; +import { useState } from "react"; +import SessionDiffView from './components/SessionDiffView'; +import { sanitizeCapabilities, sanitizeTextLogs } from './utils/sanitization'; + +const { Field } = Form; + +export default function SessionComparison() { + const [fetchSessionDetails] = usePromise(window.browserstackAPI.getAutomateSessionDetails); + const [parseTextLogs] = usePromise(window.browserstackAPI.getAutomateParsedTextLogs); + + const [session, setSession] = useState(null); + const [textLogsResult, setTextLogsResult] = useState(null); + const [seleniumLogsA, setSeleniumLogsA] = useState(null); + const [loadingSeleniumA, setLoadingSeleniumA] = useState(false); + + const [session2, setSession2] = useState(null); + const [textLogsResult2, setTextLogsResult2] = useState(null); + const [seleniumLogsB, setSeleniumLogsB] = useState(null); + const [loadingSeleniumB, setLoadingSeleniumB] = useState(false); + + const [harLogsA, setHarLogsA] = useState(null); + const [harLogsB, setHarLogsB] = useState(null); + const [loadingHarA, setLoadingHarA] = useState(false); + const [loadingHarB, setLoadingHarB] = useState(false); + + const loadSessionA = async (sessionId: string) => { + try { + const rawSessionA = await fetchSessionDetails(sessionId); + const sessionA = sanitizeCapabilities(rawSessionA); + const rawLogsA = await parseTextLogs(rawSessionA); + const logsA = sanitizeTextLogs(rawLogsA); + setSession(sessionA); + setTextLogsResult(logsA); + + setLoadingSeleniumA(true); + const selA = await window.browserstackAPI.getSeleniumLogs(rawSessionA.automation_session.selenium_logs_url); + setSeleniumLogsA(selA); + setLoadingSeleniumA(false); + + setLoadingHarA(true); + const harA = await window.browserstackAPI.getHarLogs(rawSessionA.automation_session.har_logs_url); + setHarLogsA(harA); + setLoadingHarA(false); + } catch (err) { + console.error("Error in loadSessionA:", err); + setLoadingSeleniumA(false); + setLoadingHarA(false); + throw new Error("Failed to load Session A"); + } + }; + + const loadSessionB = async (sessionId: string) => { + try { + const rawSessionB = await fetchSessionDetails(sessionId); + const sessionB = sanitizeCapabilities(rawSessionB); + const rawLogsB = await parseTextLogs(rawSessionB); + const logsB = sanitizeTextLogs(rawLogsB); + setSession2(sessionB); + setTextLogsResult2(logsB); + + setLoadingSeleniumB(true); + const selB = await window.browserstackAPI.getSeleniumLogs(rawSessionB.automation_session.selenium_logs_url); + setSeleniumLogsB(selB); + setLoadingSeleniumB(false); + + setLoadingHarB(true); + const harB = await window.browserstackAPI.getHarLogs(rawSessionB.automation_session.har_logs_url); + setHarLogsB(harB); + setLoadingHarB(false); + } catch (err) { + console.error("Error in loadSessionB:", err); + setLoadingSeleniumB(false); + setLoadingHarB(false); + throw new Error("Failed to load Session B"); + } + }; + + const OpenSession = (input: any) => { + setSession(null); + setTextLogsResult(null); + setSeleniumLogsA(null); + setSession2(null); + setTextLogsResult2(null); + setSeleniumLogsB(null); + + toast.promise( + Promise.all([ + loadSessionA(input.sessionIdA), + loadSessionB(input.sessionIdB) + ]), + { + pending: "Opening Sessions...", + success: "Sessions Loaded", + error: { + render({ data }) { + console.error(data); + return "Failed to load one or more sessions. Check console."; + }, + }, + } + ); + }; + + return ( +
+

Session Comparison

+ +
+ + + + + + + + + +
+ +
+ {session && session2 && textLogsResult && textLogsResult2 && ( + + )} +
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/session-comparison/types.ts b/src/renderer/routes/automate/tools/session-comparison/types.ts new file mode 100644 index 0000000..5d1ceb1 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/types.ts @@ -0,0 +1,65 @@ +export interface CharDiff { + type: 'common' | 'removed' | 'added'; + text: string; +} + +export interface DiffLine { + type: 'unchanged' | 'removed' | 'added' | 'modified'; + leftLine: string | null; + rightLine: string | null; + leftNumber: number | null; + rightNumber: number | null; + charDiffs?: CharDiff[]; +} + +export interface SessionData { + automation_session: { + name?: string; + project_name: string; + build_name: string; + status: string; + os: string; + os_version: string; + browser: string; + browser_version: string; + device?: string; + duration: number; + created_at: string; + selenium_logs_url: string; + har_logs_url: string; + }; + [key: string]: any; +} + +export interface TextLogsResult { + capabilities: any[]; + [key: string]: any; +} + +export interface NetworkEntry { + request: { + method: string; + url: string; + }; + response: { + status: number; + bodySize?: number; + content?: { + mimeType?: string; + }; + }; + time?: number; + timings?: Record; +} + +export interface HarLog { + log: { + entries: NetworkEntry[]; + }; +} + +export interface NetworkComparison { + matched: Array<{ key: string; dataA: any[]; dataB: any[] }>; + onlyInA: Array<{ key: string; data: any[] }>; + onlyInB: Array<{ key: string; data: any[] }>; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts b/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts new file mode 100644 index 0000000..ebf6303 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts @@ -0,0 +1,278 @@ +import { CharDiff, DiffLine } from '../types'; + +export function generateCharDiffs(oldText: string, newText: string): CharDiff[] { + const diffs: CharDiff[] = []; + let i = 0, j = 0; + + while (i < oldText.length || j < newText.length) { + let commonStart = 0; + while (i + commonStart < oldText.length && + j + commonStart < newText.length && + oldText[i + commonStart] === newText[j + commonStart]) { + commonStart++; + } + + if (commonStart > 0) { + diffs.push({ type: 'common', text: oldText.slice(i, i + commonStart) }); + i += commonStart; + j += commonStart; + continue; + } + + let oldNext = i; + let newNext = j; + let found = false; + + for (let lookAhead = 1; lookAhead <= Math.min(50, Math.max(oldText.length - i, newText.length - j)); lookAhead++) { + if (i + lookAhead < oldText.length && j < newText.length && oldText[i + lookAhead] === newText[j]) { + oldNext = i + lookAhead; + newNext = j; + found = true; + break; + } + if (j + lookAhead < newText.length && i < oldText.length && newText[j + lookAhead] === oldText[i]) { + oldNext = i; + newNext = j + lookAhead; + found = true; + break; + } + } + + if (!found) { + oldNext = Math.min(i + 1, oldText.length); + newNext = Math.min(j + 1, newText.length); + } + + if (i < oldNext) { + diffs.push({ type: 'removed', text: oldText.slice(i, oldNext) }); + i = oldNext; + } + if (j < newNext) { + diffs.push({ type: 'added', text: newText.slice(j, newNext) }); + j = newNext; + } + } + + return diffs; +} + +function calculateSimilarity(str1: string, str2: string): number { + if (!str1 || !str2) return 0; + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const words1 = str1.split(/\s+/); + const words2 = str2.split(/\s+/); + const commonWords = words1.filter(w => words2.includes(w)).length; + const totalWords = Math.max(words1.length, words2.length); + const wordSimilarity = commonWords / totalWords; + + const editDistance = levenshteinDistance(str1, str2); + const charSimilarity = (longer.length - editDistance) / longer.length; + + return wordSimilarity * 0.7 + charSimilarity * 0.3; +} + +function levenshteinDistance(str1: string, str2: string): number { + const matrix = []; + + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[str2.length][str1.length]; +} + +export function generateDiff(oldValue: string, newValue: string): DiffLine[] { + const oldLines = oldValue.split('\n'); + const newLines = newValue.split('\n'); + const result: DiffLine[] = []; + + let leftNum = 1; + let rightNum = 1; + let i = 0; + let j = 0; + + const getLookaheadMatch = (oldIdx: number, newIdx: number, range: number = 10) => { + let bestMatch = { oldIdx: -1, newIdx: -1, similarity: 0.3 }; + + for (let oi = oldIdx; oi < Math.min(oldIdx + range, oldLines.length); oi++) { + for (let ni = newIdx; ni < Math.min(newIdx + range, newLines.length); ni++) { + if (oi === oldIdx && ni === newIdx) continue; + const sim = calculateSimilarity(oldLines[oi], newLines[ni]); + if (sim > bestMatch.similarity) { + bestMatch = { oldIdx: oi, newIdx: ni, similarity: sim }; + } + } + } + + return bestMatch; + }; + + while (i < oldLines.length || j < newLines.length) { + if (i >= oldLines.length && j >= newLines.length) break; + + if (i < oldLines.length && j >= newLines.length) { + result.push({ + type: 'removed', + leftLine: oldLines[i], + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + continue; + } + + if (j < newLines.length && i >= oldLines.length) { + result.push({ + type: 'added', + leftLine: null, + rightLine: newLines[j], + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + continue; + } + + const currentOld = oldLines[i]; + const currentNew = newLines[j]; + + if (currentOld === currentNew) { + result.push({ + type: 'unchanged', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++ + }); + i++; + j++; + continue; + } + + const currentSimilarity = calculateSimilarity(currentOld, currentNew); + const lookahead = getLookaheadMatch(i, j, 10); + + const shouldPairCurrent = + currentSimilarity > 0.3 && + (lookahead.similarity - currentSimilarity < 0.2 || + (lookahead.oldIdx === i && lookahead.newIdx === j)); + + if (shouldPairCurrent) { + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else if (lookahead.oldIdx > i && lookahead.newIdx === j) { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else if (lookahead.newIdx > j && lookahead.oldIdx === i) { + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else { + let oldHasBetterMatch = false; + let newHasBetterMatch = false; + + for (let k = j + 1; k < Math.min(j + 5, newLines.length); k++) { + if (calculateSimilarity(currentOld, newLines[k]) > currentSimilarity + 0.2) { + oldHasBetterMatch = true; + break; + } + } + + for (let k = i + 1; k < Math.min(i + 5, oldLines.length); k++) { + if (calculateSimilarity(oldLines[k], currentNew) > currentSimilarity + 0.2) { + newHasBetterMatch = true; + break; + } + } + + if (newHasBetterMatch && !oldHasBetterMatch) { + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else if (oldHasBetterMatch && !newHasBetterMatch) { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else { + if (currentSimilarity > 0.2) { + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } + } + } + } + + return result; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts b/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts new file mode 100644 index 0000000..c3c39f0 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts @@ -0,0 +1,61 @@ +import { NetworkComparison, NetworkEntry } from '../types'; + +export function parseHarLogs(harData: string | null): Record | null { + if (!harData) return null; + try { + const har = JSON.parse(harData); + if (!har?.log?.entries?.length) return null; + const groupedEntries: Record = {}; + har.log.entries.forEach((entry: any) => { + const key = `${entry.request.method}|||${entry.request.url}`; + if (!groupedEntries[key]) groupedEntries[key] = []; + groupedEntries[key].push(entry); + }); + return groupedEntries; + } catch (e) { + console.error('Error parsing HAR data:', e); + return null; + } +} + +export function formatNetworkEntry(key: string, entries: any[]): string { + const [method, url] = key.split('|||'); + const firstEntry = entries[0]; + const response = firstEntry.response; + const timings = firstEntry.timings || {}; + const statusCodes = entries.map((e: any) => e.response.status); + const uniqueStatuses = [...new Set(statusCodes)]; + const avgTime = entries.reduce((sum, e) => sum + (e.time || 0), 0) / entries.length; + const minTime = Math.min(...entries.map((e: any) => e.time || 0)); + const maxTime = Math.max(...entries.map((e: any) => e.time || 0)); + + return `Count: ${entries.length} | Status: ${uniqueStatuses.join(', ')} +Time: ${avgTime.toFixed(1)}ms (${minTime}-${maxTime}ms) +Size: ${response.bodySize || 0}b | Type: ${response.content?.mimeType || 'unknown'} +Timings: ${Object.entries(timings) + .filter(([_, v]) => v !== -1) + .map(([k, v]) => `${k}:${v}ms`) + .join(' ')}`; +} + +export function compareNetworkLogs(harA: string | null, harB: string | null): NetworkComparison { + const harLogsA = parseHarLogs(harA); + const harLogsB = parseHarLogs(harB); + if (!harLogsA && !harLogsB) return { matched: [], onlyInA: [], onlyInB: [] }; + + const matched: Array<{ key: string; dataA: any[]; dataB: any[] }> = []; + const onlyInA: Array<{ key: string; data: any[] }> = []; + const onlyInB: Array<{ key: string; data: any[] }> = []; + const keysA = new Set(Object.keys(harLogsA || {})); + const keysB = new Set(Object.keys(harLogsB || {})); + + keysA.forEach(key => { + if (keysB.has(key)) matched.push({ key, dataA: harLogsA![key], dataB: harLogsB![key] }); + else onlyInA.push({ key, data: harLogsA![key] }); + }); + keysB.forEach(key => { + if (!keysA.has(key)) onlyInB.push({ key, data: harLogsB![key] }); + }); + + return { matched, onlyInA, onlyInB }; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts b/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts new file mode 100644 index 0000000..b563c51 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts @@ -0,0 +1,64 @@ +const BSTACK_OPTIONS_PATH = 'bstack:options'; +const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; +const AUTH_TOKEN_KEY = 'authToken'; +const REDACTED_VALUE = '***REDACTED***'; + +export function sanitizeCapabilities(session: any) { + if (!session) { + return session; + } + + const sanitizedSession = JSON.parse(JSON.stringify(session)); + + if (sanitizedSession[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + sanitizedSession[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const alwaysMatch = sanitizedSession.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const firstMatch = sanitizedSession.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + }); + } + + return sanitizedSession; +} + +export function sanitizeTextLogs(textLogsResult: any) { + if (!textLogsResult || !textLogsResult.capabilities) { + return textLogsResult; + } + + const sanitized = JSON.parse(JSON.stringify(textLogsResult)); + + if (Array.isArray(sanitized.capabilities)) { + sanitized.capabilities.forEach((cap: any) => { + if (cap?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + cap[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const alwaysMatch = cap.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const firstMatch = cap.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + }); + } + }); + } + + return sanitized; +} From b91a9b22d9104486cfbe1b6c1a9804777ec7ae71 Mon Sep 17 00:00:00 2001 From: Rushabh Shroff Date: Fri, 19 Dec 2025 14:10:19 +0530 Subject: [PATCH 4/4] add comming soon banners --- src/global.d.ts | 1 + src/renderer/components/sidebar.tsx | 18 ------- src/renderer/products.ts | 71 ++++++++++++++++++++++++++ src/renderer/routes/automate/index.tsx | 34 +++++++++--- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/global.d.ts b/src/global.d.ts index 507f884..9138765 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -45,6 +45,7 @@ declare global { title: string description: string, path: string + component: React.ReactNode | null }[] } diff --git a/src/renderer/components/sidebar.tsx b/src/renderer/components/sidebar.tsx index 0272eb0..db43715 100644 --- a/src/renderer/components/sidebar.tsx +++ b/src/renderer/components/sidebar.tsx @@ -7,24 +7,6 @@ const TopMenu = [ path: "/", }, ]; -const ProductsMenu = [ - { - title: "Automate", - path: "/automate", - }, - { - title: "App Automate", - path: "/app-automate", - }, - { - title: "Percy", - path: "/percy", - }, - { - title: "Accessibility", - path: "/accessibility", - }, -]; export default function Sidebar() { const location = useLocation(); diff --git a/src/renderer/products.ts b/src/renderer/products.ts index b4dd76d..e8f6b35 100644 --- a/src/renderer/products.ts +++ b/src/renderer/products.ts @@ -30,6 +30,77 @@ const Products = [ } ], }, + { + name: "App Automate", + path: "/app-automate", + page: AutomatePage, + tools: [ + { + title: "Replay Toolkit", + description: "Replays the sessions on BrowserStack by parsing Raw Logs", + path: "/automate/replay-toolkit", + component: null, + }, + { + title: "Latency Analyser", + description: + "Analyses time spend on different actions. Helpful to identify inside/outside time for a customer session.", + path: "/automate/latency-analyser", + component: null, + }, + { + title: "Session Comparison", + description: "Compares logs across sessions and highlights differences", + path: '/automate/session-comparison', + component: null + } + ], + }, + { + name: "Percy", + path: "/percy", + page: AutomatePage, + tools: [ + { + title: "Snapshot Replay", + description: "Replay snapshots", + path: "/percy/snapshot-replay", + component: null, + }, + { + title: "CLI Logs Downloader", + description: "Download CLI logs using hash ID displayed in Customer's console", + path: "/percy/cli-log-downloader", + component: null, + }, + ], + }, + { + name: "Test Report & Analytics", + path: "/tra", + page: AutomatePage, + tools: [ + { + title: "SDK Logs Downloader", + description: "Download SDK logs from Backend", + path: "/tra/download-logs", + component: null, + }, + ], + }, + { + name: "Web Accessibility", + path: "/web-accessibility", + page: AutomatePage, + tools: [ + { + title: "Automate Session Finder", + description: "Find associated automate session for accessibility scanner run", + path: "/web-a11y/session-finder", + component: null, + }, + ], + }, ]; export default Products; diff --git a/src/renderer/routes/automate/index.tsx b/src/renderer/routes/automate/index.tsx index 57b1647..c4f14eb 100644 --- a/src/renderer/routes/automate/index.tsx +++ b/src/renderer/routes/automate/index.tsx @@ -2,22 +2,42 @@ import { NavLink } from "react-router-dom" export default function AutomatePage(props: ProductPageProps) { const { tools } = props + return (
{tools.map((tool) => { - return ( - -
-
-

{tool.title}

-

{tool.description}

+ const isComingSoon = tool.component === null + + const Card = ( +
+
+

+ {tool.title} +

+

{tool.description}

+
+ {isComingSoon && ( + + Coming soon + + )}
+
+ ) + + return isComingSoon ? ( +
+ {Card} +
+ ) : ( + + {Card} ) })}
) -} \ No newline at end of file +}