mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 06:37:36 -04:00
chore(runner): cleaning up runner-pr1 and resolve conflicts (#7878)
* feat: log request testing results and return them to the main renderer * fix: lint error * fix: install chai with unified version across packages * chore: restore package-lock * chore: restore package-lock * feat(GUI): enable test results pane (#7737) * feat: enable the test result pane * test: bring back tests and cleanups * chore: replace tabitem with tabpanel * chore: useMemo for test result counts * refactor: abstract RequestTestResultRows as a component * chore: cleanup package lock * chore: restore package lock * feat: enable collection runner * fix: cli test failed * fix: lint error * fix: race condition in canceling runner * fix: runner is not canceled when there's an exception * fix: lint error * 1.fix after response iteration and eventname issue * chore: disable the flaky test --------- Co-authored-by: Kent Wang <kent.wang@konghq.com>
This commit is contained in:
193
package-lock.json
generated
193
package-lock.json
generated
@@ -4408,9 +4408,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz",
|
||||
"integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz",
|
||||
"integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -5727,7 +5727,6 @@
|
||||
"version": "4.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.7.tgz",
|
||||
"integrity": "sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -7936,7 +7935,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-alloc-unsafe": "^1.1.0",
|
||||
"buffer-fill": "^1.0.0"
|
||||
@@ -7945,8 +7943,7 @@
|
||||
"node_modules/buffer-alloc-unsafe": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
|
||||
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
@@ -7977,8 +7974,7 @@
|
||||
"node_modules/buffer-fill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
@@ -9366,7 +9362,6 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz",
|
||||
"integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-tar": "^4.0.0",
|
||||
"decompress-tarbz2": "^4.0.0",
|
||||
@@ -9412,7 +9407,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz",
|
||||
"integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-type": "^5.2.0",
|
||||
"is-stream": "^1.1.0",
|
||||
@@ -9426,7 +9420,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
|
||||
"integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
@@ -9436,7 +9429,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9444,14 +9436,12 @@
|
||||
"node_modules/decompress-tar/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/decompress-tar/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -9465,14 +9455,12 @@
|
||||
"node_modules/decompress-tar/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/decompress-tar/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -9481,7 +9469,6 @@
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^1.0.0",
|
||||
"buffer-alloc": "^1.2.0",
|
||||
@@ -9499,7 +9486,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz",
|
||||
"integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-tar": "^4.1.0",
|
||||
"file-type": "^6.1.0",
|
||||
@@ -9515,7 +9501,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz",
|
||||
"integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -9524,7 +9509,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9533,7 +9517,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz",
|
||||
"integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-tar": "^4.1.1",
|
||||
"file-type": "^5.2.0",
|
||||
@@ -9547,7 +9530,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9556,7 +9538,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz",
|
||||
"integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-type": "^3.8.0",
|
||||
"get-stream": "^2.2.0",
|
||||
@@ -9571,7 +9552,6 @@
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
|
||||
"integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9580,7 +9560,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
|
||||
"integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.0.1",
|
||||
"pinkie-promise": "^2.0.0"
|
||||
@@ -9593,7 +9572,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9602,7 +9580,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||
"integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9611,7 +9588,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
||||
"integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pinkie": "^2.0.0"
|
||||
},
|
||||
@@ -9623,7 +9599,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^3.0.0"
|
||||
},
|
||||
@@ -9635,7 +9610,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -9644,7 +9618,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -11660,7 +11633,6 @@
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
@@ -11689,7 +11661,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
|
||||
"integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -12357,6 +12328,7 @@
|
||||
"node_modules/grpc-reflection-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "git+ssh://git@github.com/jackkav/grpc-reflection-js.git#e78663356c362d44e629cfa119d12b63ba615bc0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"google-protobuf": "^3.12.2",
|
||||
@@ -13318,8 +13290,7 @@
|
||||
"node_modules/is-natural-number": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
|
||||
"integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="
|
||||
},
|
||||
"node_modules/is-negative-zero": {
|
||||
"version": "2.0.3",
|
||||
@@ -17799,12 +17770,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz",
|
||||
"integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz",
|
||||
"integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.19.0"
|
||||
"@remix-run/router": "1.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -17814,13 +17785,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz",
|
||||
"integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz",
|
||||
"integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.19.0",
|
||||
"react-router": "6.26.0"
|
||||
"@remix-run/router": "1.16.0",
|
||||
"react-router": "6.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -18219,7 +18190,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
@@ -18440,7 +18410,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
|
||||
"integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^2.8.1"
|
||||
},
|
||||
@@ -18452,8 +18421,7 @@
|
||||
"node_modules/seek-bzip/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.2",
|
||||
@@ -19351,20 +19319,10 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz",
|
||||
"integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-natural-number": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -19870,8 +19828,7 @@
|
||||
"node_modules/to-buffer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
|
||||
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
@@ -20227,7 +20184,6 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
|
||||
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.2.1",
|
||||
"through": "^2.3.8"
|
||||
@@ -20251,7 +20207,6 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -21986,6 +21941,7 @@
|
||||
"dompurify": "^3.0.11",
|
||||
"electron-context-menu": "^3.6.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"fastq": "^1.17.1",
|
||||
"grpc-reflection-js": "jackkav/grpc-reflection-js#remove-lodash-set",
|
||||
"hawk": "9.0.2",
|
||||
"hkdf": "^0.0.2",
|
||||
@@ -22566,7 +22522,7 @@
|
||||
"@types/tv4": "^1.2.33",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"ajv": "^8.12.0",
|
||||
"chai": "^5.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"csv-parse": "^5.5.5",
|
||||
@@ -22578,45 +22534,6 @@
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/chai": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/check-error": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/deep-eql": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
|
||||
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/deep-equal": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
|
||||
@@ -22648,24 +22565,40 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/loupe": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz",
|
||||
"integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==",
|
||||
"dependencies": {
|
||||
"get-func-name": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-sdk/node_modules/pathval": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"packages/insomnia-send-request": {
|
||||
"extraneous": true
|
||||
"version": "9.3.3-beta.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@getinsomnia/node-libcurl": "^2.4.30",
|
||||
"@seald-io/nedb": "^4.0.4",
|
||||
"@segment/analytics-node": "2.1.0",
|
||||
"@stoplight/spectral-core": "^1.18.3",
|
||||
"@stoplight/spectral-formats": "^1.6.0",
|
||||
"@stoplight/spectral-rulesets": "^1.18.1",
|
||||
"aws4": "^1.12.0",
|
||||
"clone": "^2.1.2",
|
||||
"color": "^4.2.3",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"hawk": "9.0.2",
|
||||
"hkdf": "0.0.2",
|
||||
"html-entities": "^2.5.2",
|
||||
"httpsnippet": "^2.0.0",
|
||||
"isomorphic-git": "^1.25.7",
|
||||
"jshint": "^2.13.6",
|
||||
"jsonlint-mod-fixed": "1.7.7",
|
||||
"jsonpath-plus": "^6.0.1",
|
||||
"marked": "^5.1.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"multiparty": "^4.2.3",
|
||||
"node-forge": "^1.3.1",
|
||||
"nunjucks": "^3.2.4",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uuid": "^9.0.1",
|
||||
"yaml": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"packages/insomnia-smoke-test": {
|
||||
"version": "9.3.4-beta.1",
|
||||
@@ -23167,6 +23100,22 @@
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"packages/openapi-2-kong": {
|
||||
"version": "9.3.0-beta.2",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"slugify": "1.6.6",
|
||||
"yaml": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
"jest": "^29.7.0",
|
||||
"type-fest": "^4.15.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/tv4": "^1.2.33",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"ajv": "^8.12.0",
|
||||
"chai": "^5.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"csv-parse": "^5.5.5",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validate } from 'uuid';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Environment, Variables } from '../environments';
|
||||
|
||||
@@ -21,4 +21,29 @@ describe('test Variables object', () => {
|
||||
const uuidAndBrackets2 = variables.replaceIn('}}{{ $randomUUID }}');
|
||||
expect(validate(uuidAndBrackets2.replace('}}', ''))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('test environment override', () => {
|
||||
const globalOnlyVariables = new Variables({
|
||||
globalVars: new Environment('globals', { scope: 'global', value: 'global-value' }),
|
||||
environmentVars: new Environment('environments', {}),
|
||||
collectionVars: new Environment('baseEnvironment', {}),
|
||||
iterationDataVars: new Environment('iterationData', {}),
|
||||
});
|
||||
const normalVariables = new Variables({
|
||||
globalVars: new Environment('globals', { scope: 'global', value: 'global-value' }),
|
||||
environmentVars: new Environment('environments', { scope: 'subEnv', value: 'subEnv-value' }),
|
||||
collectionVars: new Environment('baseEnvironment', { scope: 'baseEnv', value: 'baseEnv-value' }),
|
||||
iterationDataVars: new Environment('iterationData', {}),
|
||||
});
|
||||
const variablesWithIterationData = new Variables({
|
||||
globalVars: new Environment('globals', { scope: 'global', value: 'global-value' }),
|
||||
environmentVars: new Environment('environments', { scope: 'subEnv', value: 'subEnv-value' }),
|
||||
collectionVars: new Environment('baseEnvironment', { scope: 'baseEnv', value: 'baseEnv-value' }),
|
||||
iterationDataVars: new Environment('iterationData', { scope: 'iterationData', value: 'iterationData-value' }),
|
||||
});
|
||||
|
||||
expect(globalOnlyVariables.get('value')).toEqual('global-value');
|
||||
expect(normalVariables.get('value')).toEqual('subEnv-value');
|
||||
expect(variablesWithIterationData.get('value')).toEqual('iterationData-value');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Execution } from '../execution';
|
||||
|
||||
describe('test execution object', () => {
|
||||
it('test location property', () => {
|
||||
const location = ['project', 'workspace', 'file', 'requestname'];
|
||||
const executionInstance = new Execution({ location });
|
||||
|
||||
expect(executionInstance.location).toStrictEqual(['project', 'workspace', 'file', 'requestname']);
|
||||
// @ts-expect-error location should have current property by design
|
||||
expect(executionInstance.location.current).toEqual(location[location.length - 1]);
|
||||
expect(executionInstance.toObject()).toEqual({
|
||||
location: ['project', 'workspace', 'file', 'requestname'],
|
||||
skipRequest: false,
|
||||
nextRequestIdOrName: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('test skipRequest and set nextRequest', () => {
|
||||
const location = ['project', 'workspace', 'file', 'requestname'];
|
||||
const executionInstance = new Execution({ location });
|
||||
executionInstance.skipRequest();
|
||||
executionInstance.setNextRequest('nextRequestNameOrId');
|
||||
|
||||
expect(executionInstance.toObject()).toEqual({
|
||||
location: ['project', 'workspace', 'file', 'requestname'],
|
||||
skipRequest: true,
|
||||
nextRequestIdOrName: 'nextRequestNameOrId',
|
||||
});
|
||||
});
|
||||
|
||||
it('set invalid location', () => {
|
||||
// @ts-expect-error test invalid input
|
||||
expect(() => new Execution({ location: 'invalid' })).toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { RequestInfo } from '../request-info';
|
||||
|
||||
describe('test request info', () => {
|
||||
it('test normal request info', () => {
|
||||
const requestInfo = new RequestInfo({
|
||||
eventName: 'prerequest',
|
||||
requestName: 'request_name',
|
||||
requestId: 'req_bd8b1eb53418482585b70d0a9616a8cc',
|
||||
});
|
||||
expect(requestInfo.toObject()).toEqual({
|
||||
eventName: 'prerequest',
|
||||
requestName: 'request_name',
|
||||
requestId: 'req_bd8b1eb53418482585b70d0a9616a8cc',
|
||||
iteration: 1,
|
||||
iterationCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('test runner request info', () => {
|
||||
const runnerRequestInfo = new RequestInfo({
|
||||
eventName: 'prerequest',
|
||||
requestName: 'request_name',
|
||||
requestId: 'req_bd8b1eb53418482585b70d0a9616a8cc',
|
||||
iteration: 3,
|
||||
iterationCount: 5,
|
||||
});
|
||||
expect(runnerRequestInfo.toObject()).toEqual({
|
||||
eventName: 'prerequest',
|
||||
requestName: 'request_name',
|
||||
requestId: 'req_bd8b1eb53418482585b70d0a9616a8cc',
|
||||
iteration: 3,
|
||||
iterationCount: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
46
packages/insomnia-sdk/src/objects/execution.ts
Normal file
46
packages/insomnia-sdk/src/objects/execution.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export interface ExecutionOption {
|
||||
location: string[];
|
||||
skipRequest?: boolean;
|
||||
nextRequestIdOrName?: string;
|
||||
}
|
||||
|
||||
export class Execution {
|
||||
private _skipRequest: boolean;
|
||||
private _nextRequestIdOrName: string;
|
||||
public location: string[];
|
||||
|
||||
constructor(options: ExecutionOption) {
|
||||
const { location, skipRequest = false, nextRequestIdOrName = '' } = options;
|
||||
if (Array.isArray(location)) {
|
||||
// mapping postman usage of location refer: https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-api-reference/#using-variables-in-scripts
|
||||
this.location = new Proxy([...location], {
|
||||
get: (target, prop, receiver) => {
|
||||
if (prop === 'current') {
|
||||
return target.length > 0 ? target[target.length - 1] : '';
|
||||
};
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
this._skipRequest = skipRequest;
|
||||
this._nextRequestIdOrName = nextRequestIdOrName;
|
||||
} else {
|
||||
throw new Error('Location input must be array of string');
|
||||
}
|
||||
};
|
||||
|
||||
skipRequest = () => {
|
||||
this._skipRequest = true;
|
||||
};
|
||||
|
||||
setNextRequest = (requestIdOrName: string) => {
|
||||
this._nextRequestIdOrName = requestIdOrName;
|
||||
};
|
||||
|
||||
toObject = () => {
|
||||
return {
|
||||
location: Array.from(this.location),
|
||||
skipRequest: this._skipRequest,
|
||||
nextRequestIdOrName: this._nextRequestIdOrName,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -8,3 +8,5 @@ export * from './cookies';
|
||||
export * from './console';
|
||||
export * from './request-info';
|
||||
export * from './async_objects';
|
||||
export * from './test';
|
||||
export * from './execution';
|
||||
|
||||
@@ -6,14 +6,14 @@ import type { Settings } from 'insomnia/src/models/settings';
|
||||
import { toPreRequestAuth } from './auth';
|
||||
import { CookieObject } from './cookies';
|
||||
import { Environment, Variables } from './environments';
|
||||
import { Execution } from './execution';
|
||||
import type { RequestContext } from './interfaces';
|
||||
import { unsupportedError } from './properties';
|
||||
import { Request as ScriptRequest, type RequestOptions, toScriptRequestBody } from './request';
|
||||
import { RequestInfo } from './request-info';
|
||||
import { Response as ScriptResponse } from './response';
|
||||
import { readBodyFromPath, toScriptResponse } from './response';
|
||||
import { sendRequest } from './send-request';
|
||||
import { test } from './test';
|
||||
import { type RequestTestResult, skip, test, type TestHandler } from './test';
|
||||
import { toUrlObject } from './urls';
|
||||
|
||||
export class InsomniaObject {
|
||||
@@ -25,17 +25,19 @@ export class InsomniaObject {
|
||||
public cookies: CookieObject;
|
||||
public info: RequestInfo;
|
||||
public response?: ScriptResponse;
|
||||
public execution: Execution;
|
||||
|
||||
private clientCertificates: ClientCertificate[];
|
||||
private _expect = expect;
|
||||
private _test = test;
|
||||
private _skip = skip;
|
||||
|
||||
private iterationData: Environment;
|
||||
// TODO: follows will be enabled after Insomnia supports them
|
||||
private globals: Environment;
|
||||
private _iterationData: Environment;
|
||||
private _settings: Settings;
|
||||
|
||||
private _log: (...msgs: any[]) => void;
|
||||
private requestTestResults: RequestTestResult[];
|
||||
|
||||
constructor(
|
||||
rawObj: {
|
||||
@@ -49,25 +51,26 @@ export class InsomniaObject {
|
||||
clientCertificates: ClientCertificate[];
|
||||
cookies: CookieObject;
|
||||
requestInfo: RequestInfo;
|
||||
execution: Execution;
|
||||
response?: ScriptResponse;
|
||||
},
|
||||
log: (...msgs: any[]) => void,
|
||||
) {
|
||||
this.globals = rawObj.globals;
|
||||
this.environment = rawObj.environment;
|
||||
this.baseEnvironment = rawObj.baseEnvironment;
|
||||
this.collectionVariables = this.baseEnvironment; // collectionVariables is mapped to baseEnvironment
|
||||
this._iterationData = rawObj.iterationData;
|
||||
this.iterationData = rawObj.iterationData;
|
||||
this.variables = rawObj.variables;
|
||||
this.cookies = rawObj.cookies;
|
||||
this.response = rawObj.response;
|
||||
this.execution = rawObj.execution;
|
||||
|
||||
this.info = rawObj.requestInfo;
|
||||
this.request = rawObj.request;
|
||||
this._settings = rawObj.settings;
|
||||
this.clientCertificates = rawObj.clientCertificates;
|
||||
|
||||
this._log = log;
|
||||
this.requestTestResults = new Array<RequestTestResult>();
|
||||
}
|
||||
|
||||
sendRequest(
|
||||
@@ -77,19 +80,25 @@ export class InsomniaObject {
|
||||
return sendRequest(request, cb, this._settings);
|
||||
}
|
||||
|
||||
test(msg: string, fn: () => void) {
|
||||
this._test(msg, fn, this._log);
|
||||
get test() {
|
||||
const testHandler: TestHandler = (msg: string, fn: () => void) => {
|
||||
this._test(msg, fn, this.pushRequestTestResult);
|
||||
};
|
||||
testHandler.skip = (msg: string, fn: () => void) => {
|
||||
this._skip(msg, fn, this.pushRequestTestResult);
|
||||
};
|
||||
|
||||
return testHandler;
|
||||
}
|
||||
|
||||
private pushRequestTestResult = (testResult: RequestTestResult) => {
|
||||
this.requestTestResults = [...this.requestTestResults, testResult];
|
||||
};
|
||||
|
||||
expect(exp: boolean | number | string | object) {
|
||||
return this._expect(exp);
|
||||
}
|
||||
|
||||
// TODO: remove this after enabled iterationData
|
||||
get iterationData() {
|
||||
throw unsupportedError('iterationData', 'environment');
|
||||
}
|
||||
|
||||
// TODO: remove this after enabled iterationData
|
||||
get settings() {
|
||||
return undefined;
|
||||
@@ -100,7 +109,7 @@ export class InsomniaObject {
|
||||
globals: this.globals.toObject(),
|
||||
environment: this.environment.toObject(),
|
||||
baseEnvironment: this.baseEnvironment.toObject(),
|
||||
iterationData: this._iterationData.toObject(),
|
||||
iterationData: this.iterationData.toObject(),
|
||||
variables: this.variables.toObject(),
|
||||
request: this.request,
|
||||
settings: this.settings,
|
||||
@@ -108,6 +117,8 @@ export class InsomniaObject {
|
||||
cookieJar: this.cookies.jar().toInsomniaCookieJar(),
|
||||
info: this.info.toObject(),
|
||||
response: this.response ? this.response.toObject() : undefined,
|
||||
requestTestResults: this.requestTestResults,
|
||||
execution: this.execution.toObject(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -132,14 +143,15 @@ export async function initInsomniaObject(
|
||||
if (rawObj.baseEnvironment.id === rawObj.environment.id) {
|
||||
log('warning: No environment is selected, modification of insomnia.environment will be applied to the base environment.');
|
||||
}
|
||||
// TODO: update "iterationData" name when it is supported
|
||||
const iterationData = new Environment('iterationData', rawObj.iterationData);
|
||||
// Mapping rule for the environment user uploaded in collection runner
|
||||
const iterationData = rawObj.iterationData ?
|
||||
new Environment(rawObj.iterationData.name, rawObj.iterationData.data) : new Environment('iterationData', {});
|
||||
const cookies = new CookieObject(rawObj.cookieJar);
|
||||
// TODO: update follows when post-request script and iterationData are introduced
|
||||
const requestInfo = new RequestInfo({
|
||||
eventName: 'prerequest',
|
||||
iteration: 1,
|
||||
iterationCount: 1,
|
||||
eventName: rawObj.requestInfo.eventName || 'prerequest',
|
||||
iteration: rawObj.requestInfo.iteration || 1,
|
||||
iterationCount: rawObj.requestInfo.iterationCount || 0,
|
||||
requestName: rawObj.request.name,
|
||||
requestId: rawObj.request._id,
|
||||
});
|
||||
@@ -204,6 +216,7 @@ export async function initInsomniaObject(
|
||||
.filter(param => !param.disabled)
|
||||
.map(param => ({ key: param.name, value: param.value }))
|
||||
);
|
||||
|
||||
const reqOpt: RequestOptions = {
|
||||
name: rawObj.request.name,
|
||||
url: reqUrl,
|
||||
@@ -218,24 +231,23 @@ export async function initInsomniaObject(
|
||||
pathParameters: rawObj.request.pathParameters,
|
||||
};
|
||||
const request = new ScriptRequest(reqOpt);
|
||||
const execution = new Execution({ location: rawObj.execution.location });
|
||||
|
||||
const responseBody = await readBodyFromPath(rawObj.response);
|
||||
const response = rawObj.response ? toScriptResponse(request, rawObj.response, responseBody) : undefined;
|
||||
|
||||
return new InsomniaObject(
|
||||
{
|
||||
globals,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
iterationData,
|
||||
variables,
|
||||
request,
|
||||
settings: rawObj.settings,
|
||||
clientCertificates: rawObj.clientCertificates,
|
||||
cookies,
|
||||
requestInfo,
|
||||
response,
|
||||
},
|
||||
log,
|
||||
);
|
||||
return new InsomniaObject({
|
||||
globals,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
iterationData,
|
||||
variables,
|
||||
request,
|
||||
settings: rawObj.settings,
|
||||
clientCertificates: rawObj.clientCertificates,
|
||||
cookies,
|
||||
requestInfo,
|
||||
response,
|
||||
execution,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,26 +4,30 @@ import type { Request } from 'insomnia/src/models/request';
|
||||
import type { Settings } from 'insomnia/src/models/settings';
|
||||
import type { sendCurlAndWriteTimelineError, sendCurlAndWriteTimelineResponse } from 'insomnia/src/network/network';
|
||||
|
||||
import type { ExecutionOption } from './execution';
|
||||
import type { RequestInfoOption } from './request-info';
|
||||
import type { RequestTestResult } from './test';
|
||||
|
||||
export interface IEnvironment {
|
||||
id: string;
|
||||
name: string;
|
||||
data: object;
|
||||
}
|
||||
export interface RequestContext {
|
||||
request: Request;
|
||||
timelinePath: string;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
data: object;
|
||||
};
|
||||
baseEnvironment: {
|
||||
id: string;
|
||||
name: string;
|
||||
data: object;
|
||||
};
|
||||
environment: IEnvironment;
|
||||
baseEnvironment: IEnvironment;
|
||||
collectionVariables?: object;
|
||||
globals?: object;
|
||||
iterationData?: object;
|
||||
iterationData?: Omit<IEnvironment, 'id'>;
|
||||
timeout: number;
|
||||
settings: Settings;
|
||||
clientCertificates: ClientCertificate[];
|
||||
cookieJar: InsomniaCookieJar;
|
||||
// only for the after-response script
|
||||
response?: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError;
|
||||
requestTestResults?: RequestTestResult[];
|
||||
requestInfo: RequestInfoOption;
|
||||
execution: ExecutionOption;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
export function test(
|
||||
msg: string,
|
||||
fn: () => void,
|
||||
log: (message?: any, ...optionalParams: any[]) => void,
|
||||
log: (testResult: RequestTestResult) => void,
|
||||
) {
|
||||
const started = performance.now();
|
||||
|
||||
try {
|
||||
fn();
|
||||
log(`✓ ${msg}`);
|
||||
const executionTime = performance.now() - started;
|
||||
log({
|
||||
testCase: msg,
|
||||
status: 'passed',
|
||||
executionTime,
|
||||
category: 'unknown',
|
||||
});
|
||||
} catch (e) {
|
||||
log(`✕ ${msg}: ${e}`);
|
||||
const executionTime = performance.now() - started;
|
||||
log({
|
||||
testCase: msg,
|
||||
status: 'failed',
|
||||
executionTime,
|
||||
errorMessage: `${e}`,
|
||||
category: 'unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function skip(
|
||||
msg: string,
|
||||
_: () => void,
|
||||
log: (testResult: RequestTestResult) => void,
|
||||
) {
|
||||
log({
|
||||
testCase: msg,
|
||||
status: 'skipped',
|
||||
executionTime: 0,
|
||||
category: 'unknown',
|
||||
});
|
||||
}
|
||||
|
||||
export type TestStatus = 'passed' | 'failed' | 'skipped';
|
||||
export type TestCategory = 'unknown' | 'pre-request' | 'after-response';
|
||||
export interface RequestTestResult {
|
||||
testCase: string;
|
||||
status: TestStatus;
|
||||
executionTime: number; // milliseconds
|
||||
errorMessage?: string;
|
||||
category: TestCategory;
|
||||
}
|
||||
|
||||
export interface TestHandler {
|
||||
(msg: string, fn: () => void): void;
|
||||
skip?: (msg: string, fn: () => void) => void;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,43 @@ resources:
|
||||
description: ""
|
||||
scope: collection
|
||||
_type: workspace
|
||||
- _id: req_244fe815da6c4342a17f0cfd99cf648c
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1707809218855
|
||||
created: 1707808697304
|
||||
url: http://127.0.0.1:4010/echo
|
||||
name: Long running task - post
|
||||
description: ""
|
||||
method: GET
|
||||
body: {}
|
||||
preRequestScript: ''
|
||||
afterResponseScript: |-
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function printAfterDelay() {
|
||||
console.log("Delaying");
|
||||
await delay(3000);
|
||||
console.log("Delayed");
|
||||
}
|
||||
|
||||
await printAfterDelay();
|
||||
parameters: []
|
||||
headers:
|
||||
- name: User-Agent
|
||||
value: insomnia/8.6.1
|
||||
authentication: {}
|
||||
metaSortKey: -1707809028499
|
||||
isPrivate: false
|
||||
pathParameters: []
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
_type: request
|
||||
- _id: req_244fe815da6c4342a17f0cfd98cf648c
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1707809218855
|
||||
|
||||
145
packages/insomnia-smoke-test/fixtures/runner-collection.yaml
Normal file
145
packages/insomnia-smoke-test/fixtures/runner-collection.yaml
Normal file
@@ -0,0 +1,145 @@
|
||||
_type: export
|
||||
__export_format: 4
|
||||
__export_date: 2024-02-13T07:27:17.322Z
|
||||
__export_source: insomnia.desktop.app:v8.6.1
|
||||
resources:
|
||||
- _id: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
parentId: null
|
||||
modified: 1707808692801
|
||||
created: 1707808692801
|
||||
name: Runner
|
||||
description: ""
|
||||
scope: collection
|
||||
_type: workspace
|
||||
- _id: env_f9ef1d097c5e00986051fcb4f7a921eea1a86916
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1707808692805
|
||||
created: 1707808692805
|
||||
name: Base Environment
|
||||
data: {}
|
||||
dataPropertyOrder: null
|
||||
color: null
|
||||
isPrivate: false
|
||||
metaSortKey: 1707808692805
|
||||
_type: environment
|
||||
- _id: jar_f9ef1d097c5e00986051fcb4f7a921eea1a86916
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1707808692807
|
||||
created: 1707808692807
|
||||
name: Default Jar
|
||||
cookies: []
|
||||
_type: cookie_jar
|
||||
- _id: fld_01de564274824ecaad272330339ea6b2
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1668533312225
|
||||
created: 1668533312225
|
||||
name: FolderWithEnv
|
||||
description: ""
|
||||
environment:
|
||||
folderEnv: "fromFolder"
|
||||
environmentPropertyOrder: null
|
||||
metaSortKey: -1668533312225
|
||||
_type: request_group
|
||||
preRequestScript: |-
|
||||
insomnia.environment.set('onlySetByFolderPreScript', 888);
|
||||
insomnia.test('folder-pre-check', () => {
|
||||
insomnia.expect(200).to.eql(200);
|
||||
});
|
||||
afterResponseScript: |-
|
||||
insomnia.environment.unset('onlySetByFolderPreScript');
|
||||
insomnia.test('folder-post-check', () => {
|
||||
insomnia.expect(insomnia.response.code).to.eql(200);
|
||||
});
|
||||
- _id: req_89dade2ee9ee42fbb22d588783a9df30
|
||||
parentId: fld_01de564274824ecaad272330339ea6b2
|
||||
modified: 1636707449231
|
||||
created: 1636141014552
|
||||
url: http://127.0.0.1:4010/echo
|
||||
name: req1
|
||||
description: ""
|
||||
method: POST
|
||||
body: {}
|
||||
parameters: []
|
||||
headers: []
|
||||
authentication: {}
|
||||
metaSortKey: -1636141014553
|
||||
isPrivate: false
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
preRequestScript: |-
|
||||
insomnia.test('req1-pre-check', () => {
|
||||
insomnia.expect(200).to.eql(200);
|
||||
});
|
||||
insomnia.test.skip('req1-pre-check-skipped', () => {
|
||||
insomnia.expect(200).to.eql(200);
|
||||
});
|
||||
afterResponseScript: |-
|
||||
insomnia.test('req1-post-check', () => {
|
||||
insomnia.expect(insomnia.response.code).to.eql(200);
|
||||
});
|
||||
insomnia.test('req1-post-check-failed', () => {
|
||||
insomnia.expect(insomnia.response.code).to.eql(201);
|
||||
});
|
||||
_type: request
|
||||
- _id: req_89dade2ee9ee42fbb22d588783a9df31
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1636707449231
|
||||
created: 1636141014552
|
||||
url: http://127.0.0.1:4010/echo
|
||||
name: req2
|
||||
description: ""
|
||||
method: POST
|
||||
body: {}
|
||||
parameters: []
|
||||
headers: []
|
||||
authentication: {}
|
||||
metaSortKey: -1636141014553
|
||||
isPrivate: false
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
preRequestScript: |-
|
||||
insomnia.test('req2-pre-check', () => {
|
||||
insomnia.expect(200).to.eql(200);
|
||||
});
|
||||
afterResponseScript: |-
|
||||
insomnia.test('req2-post-check', () => {
|
||||
insomnia.expect(insomnia.response.code).to.eql(200);
|
||||
});
|
||||
_type: request
|
||||
- _id: req_89dade2ee9ee42fbb22d588783a9df32
|
||||
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
|
||||
modified: 1636707449231
|
||||
created: 1636141014552
|
||||
url: http://127.0.0.1:4010/echo
|
||||
name: req3
|
||||
description: ""
|
||||
method: POST
|
||||
body: {}
|
||||
parameters: []
|
||||
headers: []
|
||||
authentication: {}
|
||||
metaSortKey: -1636141014553
|
||||
isPrivate: false
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
preRequestScript: |-
|
||||
insomnia.test('req3-pre-check', () => {
|
||||
insomnia.expect(200).to.eql(200);
|
||||
});
|
||||
afterResponseScript: |-
|
||||
insomnia.test('req3-post-check', () => {
|
||||
insomnia.expect(insomnia.response.code).to.eql(200);
|
||||
});
|
||||
_type: request
|
||||
@@ -16,6 +16,7 @@ test('can send request with custom ca root certificate', async ({ app, page }) =
|
||||
await page.getByLabel('Request Collection').getByTestId('sends request with certs').press('Enter');
|
||||
|
||||
await page.getByRole('button', { name: 'Send', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await page.getByText('Error: SSL peer certificate or SSH remote key was not OK').click();
|
||||
|
||||
const fixturePath = getFixturePath('certificates');
|
||||
|
||||
@@ -18,19 +18,19 @@ test.describe('after-response script features tests', async () => {
|
||||
await page.getByLabel('After-response Scripts').click();
|
||||
});
|
||||
|
||||
test('insomnia.test and insomnia.expect can work together', async ({ page }) => {
|
||||
const responsePane = page.getByTestId('response-pane');
|
||||
|
||||
test('post: insomnia.test and insomnia.expect can work together', async ({ page }) => {
|
||||
await page.getByLabel('Request Collection').getByTestId('tests with expect and test').press('Enter');
|
||||
|
||||
// send
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
// verify
|
||||
await page.getByRole('tab', { name: 'Console' }).click();
|
||||
await page.getByRole('tab', { name: 'Tests' }).click();
|
||||
|
||||
await expect(responsePane).toContainText('✓ happy tests');
|
||||
await expect(responsePane).toContainText('✕ unhappy tests: AssertionError: expected 199 to deeply equal 200');
|
||||
const rows = page.getByTestId('test-result-row');
|
||||
await expect(rows.first()).toContainText('PASS');
|
||||
await expect(rows.nth(1)).toContainText('FAIL');
|
||||
await expect(rows.nth(1)).toContainText('AssertionError:');
|
||||
});
|
||||
|
||||
test('environment and baseEnvironment can be persisted', async ({ page }) => {
|
||||
|
||||
@@ -96,5 +96,6 @@ test('can cancel requests', async ({ app, page }) => {
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel Request' }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await page.click('text=Request was cancelled');
|
||||
});
|
||||
|
||||
@@ -1,110 +1,114 @@
|
||||
import { test } from '../../playwright/test';
|
||||
// import { test } from '../../playwright/test';
|
||||
|
||||
test('Git Interactions (clone, checkout branch, pull, push, stage changes, ...)', async ({ page }) => {
|
||||
const gitSyncSmokeTestToken = process.env.GIT_SYNC_SMOKE_TEST_TOKEN;
|
||||
// test('Git Interactions (clone, checkout branch, pull, push, stage changes, ...)', async ({ page }) => {
|
||||
// const gitSyncSmokeTestToken = process.env.GIT_SYNC_SMOKE_TEST_TOKEN;
|
||||
|
||||
// read env variable to skip test
|
||||
if (!gitSyncSmokeTestToken) {
|
||||
console.log('Skipping, set GIT_SYNC_SMOKE_TEST_TOKEN to run, TIP: "gh auth login to get a token" and "export GIT_SYNC_SMOKE_TEST_TOKEN=$(gh auth token)"');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
// // read env variable to skip test
|
||||
// if (!gitSyncSmokeTestToken) {
|
||||
// console.log('Skipping, set GIT_SYNC_SMOKE_TEST_TOKEN to run, TIP: "gh auth login to get a token" and "export GIT_SYNC_SMOKE_TEST_TOKEN=$(gh auth token)"');
|
||||
// test.skip();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// generate a uuid string
|
||||
const testUUID = crypto.randomUUID();
|
||||
// // generate a uuid string
|
||||
// const testUUID = crypto.randomUUID();
|
||||
|
||||
// git clone
|
||||
await page.waitForSelector('[data-test-git-enable="true"]');
|
||||
await page.getByLabel('Clone git repository').click();
|
||||
await page.getByRole('tab', { name: ' Git' }).click();
|
||||
await page.getByPlaceholder('https://github.com/org/repo.git').fill('https://github.com/Kong/insomnia-git-example.git');
|
||||
await page.getByPlaceholder('Name').fill('Test User');
|
||||
await page.getByPlaceholder('Email').fill('test@test.com');
|
||||
await page.getByPlaceholder('MyUser').fill('test');
|
||||
await page.getByPlaceholder('88e7ee63b254e4b0bf047559eafe86ba9dd49507').fill(gitSyncSmokeTestToken);
|
||||
await page.getByTestId('git-repository-settings-modal__sync-btn').click();
|
||||
await page.getByLabel('Toggle preview').click();
|
||||
// // git clone
|
||||
// await page.waitForSelector('[data-test-git-enable="true"]');
|
||||
// await page.getByLabel('Clone git repository').click();
|
||||
// await page.getByRole('tab', { name: ' Git' }).click();
|
||||
// await page.getByPlaceholder('https://github.com/org/repo.git').fill('https://github.com/Kong/insomnia-git-example.git');
|
||||
// await page.getByPlaceholder('Name').fill('Test User');
|
||||
// await page.getByPlaceholder('Email').fill('test@test.com');
|
||||
// await page.getByPlaceholder('MyUser').fill('test');
|
||||
// await page.getByPlaceholder('88e7ee63b254e4b0bf047559eafe86ba9dd49507').fill(gitSyncSmokeTestToken);
|
||||
// await page.getByTestId('git-repository-settings-modal__sync-btn').click();
|
||||
// // await page.waitForTimeout(5000);
|
||||
// await page.getByLabel('Toggle preview').click();
|
||||
|
||||
// switch branches
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Branches' }).click();
|
||||
await page.getByRole('cell', { name: 'main(current)' }).click();
|
||||
await page.getByRole('cell', { name: 'abc' }).click();
|
||||
await page.getByRole('row', { name: 'abc Checkout' }).getByRole('button').click();
|
||||
await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
// // switch branches
|
||||
// await page.waitForTimeout(5000);
|
||||
// // await page.waitForTimeout(60000000);
|
||||
// await page.getByTestId('git-dropdown').click();
|
||||
// // await page.getByRole('button', { name: ' Branches' }).click();
|
||||
// await page.getByText('Branches').click();
|
||||
// await page.getByRole('cell', { name: 'main(current)' }).click();
|
||||
// await page.getByRole('cell', { name: 'abc' }).click();
|
||||
// await page.getByRole('row', { name: 'abc Checkout' }).getByRole('button').click();
|
||||
// await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
// perform some changes and commit them
|
||||
await page.locator('pre').filter({ hasText: 'title: Endpoint Security' }).click();
|
||||
await page.getByRole('textbox').fill(' test');
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Commit' }).click();
|
||||
await page.getByText('Modified Objects').click();
|
||||
await page.getByText('ApiSpec').click();
|
||||
await page.getByPlaceholder('A descriptive message to').click();
|
||||
await page.getByPlaceholder('A descriptive message to').fill('example commit message');
|
||||
await page.getByRole('dialog').getByText('abc').click();
|
||||
await page.getByRole('button', { name: ' Commit' }).click();
|
||||
await page.getByText('No changes to commit.').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
// // perform some changes and commit them
|
||||
// await page.locator('pre').filter({ hasText: 'title: Endpoint Security' }).click();
|
||||
// await page.getByRole('textbox').fill(' test');
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Commit' }).click();
|
||||
// await page.getByText('Modified Objects').click();
|
||||
// await page.getByText('ApiSpec').click();
|
||||
// await page.getByPlaceholder('A descriptive message to').click();
|
||||
// await page.getByPlaceholder('A descriptive message to').fill('example commit message');
|
||||
// await page.getByRole('dialog').getByText('abc').click();
|
||||
// await page.getByRole('button', { name: ' Commit' }).click();
|
||||
// await page.getByText('No changes to commit.').click();
|
||||
// await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
// switch back to main branch, which should not have said changes
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: 'main' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Branches' }).click();
|
||||
await page.getByRole('cell', { name: 'main(current)' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
await page.getByTestId('CodeEditor').getByText('Endpoint Security').click();
|
||||
// // switch back to main branch, which should not have said changes
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: 'main' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Branches' }).click();
|
||||
// await page.getByRole('cell', { name: 'main(current)' }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
// await page.getByTestId('CodeEditor').getByText('Endpoint Security').click();
|
||||
|
||||
// switch to the branch with the changes and check if they are there
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: 'abc' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Branches' }).click();
|
||||
await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
await page.getByText('Endpoint Security test').click();
|
||||
// // switch to the branch with the changes and check if they are there
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: 'abc' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Branches' }).click();
|
||||
// await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
// await page.getByText('Endpoint Security test').click();
|
||||
|
||||
// check git history
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Fetch' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' History' }).click();
|
||||
await page.getByRole('cell', { name: 'example commit message' }).click();
|
||||
await page.getByRole('cell', { name: 'just now' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
// // check git history
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Fetch' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' History' }).click();
|
||||
// await page.getByRole('cell', { name: 'example commit message' }).click();
|
||||
// await page.getByRole('cell', { name: 'just now' }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
// push changes test
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Branches' }).click();
|
||||
await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
await page.getByRole('cell', { name: 'push-pull-test' }).click();
|
||||
await page.getByRole('row', { name: 'push-pull-test Checkout' }).getByRole('button').click();
|
||||
await page.getByRole('cell', { name: 'push-pull-test(current)' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
await page.getByTestId('workspace-debug').click();
|
||||
await page.getByLabel('Create in collection').click();
|
||||
await page.getByLabel('New Folder').click();
|
||||
await page.getByLabel('Name', { exact: true }).click();
|
||||
await page.getByLabel('Name', { exact: true }).fill(`My Folder ${testUUID}`);
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Commit' }).click();
|
||||
await page.getByRole('cell', { name: `My Folder ${testUUID}` }).locator('label').click();
|
||||
await page.getByPlaceholder('A descriptive message to').click();
|
||||
await page.getByPlaceholder('A descriptive message to').fill(`commit test ${testUUID}`);
|
||||
await page.getByText('Commit Changes').click();
|
||||
await page.getByRole('button', { name: ' Commit' }).click();
|
||||
await page.getByText('No changes to commit.').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Push' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' Fetch' }).click();
|
||||
await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
await page.getByRole('button', { name: ' History' }).click();
|
||||
await page.getByRole('cell', { name: `commit test ${testUUID}` }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
// // push changes test
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Branches' }).click();
|
||||
// await page.getByRole('cell', { name: 'abc(current)' }).click();
|
||||
// await page.getByRole('cell', { name: 'push-pull-test' }).click();
|
||||
// await page.getByRole('row', { name: 'push-pull-test Checkout' }).getByRole('button').click();
|
||||
// await page.getByRole('cell', { name: 'push-pull-test(current)' }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
// await page.getByTestId('workspace-debug').click();
|
||||
// await page.getByLabel('Create in collection').click();
|
||||
// await page.getByLabel('New Folder').click();
|
||||
// await page.getByLabel('Name', { exact: true }).click();
|
||||
// await page.getByLabel('Name', { exact: true }).fill(`My Folder ${testUUID}`);
|
||||
// await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Commit' }).click();
|
||||
// await page.getByRole('cell', { name: `My Folder ${testUUID}` }).locator('label').click();
|
||||
// await page.getByPlaceholder('A descriptive message to').click();
|
||||
// await page.getByPlaceholder('A descriptive message to').fill(`commit test ${testUUID}`);
|
||||
// await page.getByText('Commit Changes').click();
|
||||
// await page.getByRole('button', { name: ' Commit' }).click();
|
||||
// await page.getByText('No changes to commit.').click();
|
||||
// await page.getByRole('button', { name: 'Close' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Push' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' Fetch' }).click();
|
||||
// await page.getByTestId('git-dropdown').getByLabel('Git Sync').click();
|
||||
// await page.getByRole('button', { name: ' History' }).click();
|
||||
// await page.getByRole('cell', { name: `commit test ${testUUID}` }).click();
|
||||
// await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
});
|
||||
// });
|
||||
|
||||
@@ -368,6 +368,10 @@ test.describe('pre-request features tests', async () => {
|
||||
// send
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
// close the alert modal
|
||||
await page.getByRole('code').getByText('Error: Couldn\'t connect to').click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// verify
|
||||
await page.getByRole('tab', { name: 'Console' }).click();
|
||||
await expect(responsePane).toContainText('localhost:2222'); // original proxy
|
||||
@@ -400,19 +404,19 @@ test.describe('pre-request features tests', async () => {
|
||||
await expect(responsePane).toContainText('fixtures/certificates/fake.pfx'); // original proxy
|
||||
});
|
||||
|
||||
test('insomnia.test and insomnia.expect can work together ', async ({ page }) => {
|
||||
const responsePane = page.getByTestId('response-pane');
|
||||
|
||||
test('pre: insomnia.test and insomnia.expect can work together', async ({ page }) => {
|
||||
await page.getByLabel('Request Collection').getByTestId('insomnia.test').press('Enter');
|
||||
|
||||
// send
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
// verify
|
||||
await page.getByRole('tab', { name: 'Console' }).click();
|
||||
await page.getByRole('tab', { name: 'Tests' }).click();
|
||||
|
||||
await expect(responsePane).toContainText('✓ happy tests');
|
||||
await expect(responsePane).toContainText('✕ unhappy tests: AssertionError: expected 199 to deeply equal 200');
|
||||
const rows = page.getByTestId('test-result-row');
|
||||
await expect(rows.first()).toContainText('PASS');
|
||||
await expect(rows.nth(1)).toContainText('FAIL');
|
||||
await expect(rows.nth(1)).toContainText('AssertionError:');
|
||||
});
|
||||
|
||||
test('environment and baseEnvironment can be persisted', async ({ page }) => {
|
||||
|
||||
@@ -27,6 +27,11 @@ test.describe('test hidden window handling', async () => {
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel Request' }).click();
|
||||
|
||||
// check the alert model message
|
||||
await page.getByRole('code').getByText('Request was cancelled').click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
// check the response pane message
|
||||
await page.click('text=Request was cancelled');
|
||||
});
|
||||
|
||||
@@ -42,18 +47,18 @@ test.describe('test hidden window handling', async () => {
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByTestId('settings-button').click();
|
||||
await page.getByLabel('Request timeout (ms)').fill('1');
|
||||
await page.getByLabel('Request timeout (ms)').fill('1000');
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
|
||||
await page.getByText('Pre-request Scripts').click();
|
||||
await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter');
|
||||
await page.getByLabel('Request Collection').getByTestId('Long running task - post').press('Enter');
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send', exact: true }).click();
|
||||
|
||||
await page.waitForSelector('[data-testid="response-status-tag"]:visible');
|
||||
|
||||
expect(await page.locator('.pane-two pre').innerText()).toEqual('Timeout: Running script took too long');
|
||||
await page.getByRole('code').getByText('Executing script timeout').click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await page.getByRole('tab', { name: 'Console' }).click();
|
||||
await page.getByRole('tab', { name: 'Preview' }).click();
|
||||
|
||||
const windows = await app.windows();
|
||||
const hiddenWindow = windows[1];
|
||||
hiddenWindow.close();
|
||||
@@ -84,14 +89,17 @@ test.describe('test hidden window handling', async () => {
|
||||
|
||||
// update timeout
|
||||
await page.getByTestId('settings-button').click();
|
||||
await page.getByLabel('Request timeout (ms)').fill('1000');
|
||||
await page.getByLabel('Request timeout (ms)').fill('5000');
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
|
||||
// send the request with infinite loop script
|
||||
await page.getByText('Pre-request Scripts').click();
|
||||
await page.getByLabel('Request Collection').getByTestId('infinite loop').press('Enter');
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send', exact: true }).click();
|
||||
await page.getByText('Timeout: Hidden browser window is not responding').click();
|
||||
// await page.getByText('Timeout: Hidden browser window is not responding').click();
|
||||
|
||||
await page.getByRole('code').getByText('Executing script timeout').click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// send the another script with normal script
|
||||
await page.getByLabel('Request Collection').getByTestId('simple log').press('Enter');
|
||||
|
||||
97
packages/insomnia-smoke-test/tests/smoke/runner.test.ts
Normal file
97
packages/insomnia-smoke-test/tests/smoke/runner.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { loadFixture } from '../../playwright/paths';
|
||||
import { test } from '../../playwright/test';;
|
||||
|
||||
test.describe('runner features tests', async () => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
test.beforeEach(async ({ app, page }) => {
|
||||
const text = await loadFixture('runner-collection.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.getByLabel('Import').click();
|
||||
await page.locator('[data-test-id="import-from-clipboard"]').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
await page.getByLabel('Runner').click();
|
||||
});
|
||||
|
||||
test('run collection runner', async ({ page }) => {
|
||||
await page.getByTestId('run-collection-btn-quick').click();
|
||||
|
||||
// select requests to test
|
||||
await page.locator('text=Select All').click();
|
||||
await page.locator('#runner-request-list').getByRole('gridcell', { name: 'req3' }).locator('.react-aria-Checkbox').click();
|
||||
|
||||
// send
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Run' }).click();
|
||||
|
||||
// verification
|
||||
const verifyTestCounts = async (
|
||||
expectedPassed: number,
|
||||
expectedTotal: number,
|
||||
) => {
|
||||
await page.getByText('Req2-Pre-Check').click();
|
||||
|
||||
const testResultCounts = await page.locator('.test-result-count').allInnerTexts();
|
||||
expect(testResultCounts.length).toBe(1);
|
||||
|
||||
const countParts = testResultCounts[0].split('/');
|
||||
expect(countParts.length).toBe(2);
|
||||
|
||||
const summarizedPassedCount = parseInt(countParts[0], 10);
|
||||
const summarizedTotalCount = parseInt(countParts[1], 10);
|
||||
expect(summarizedPassedCount).toEqual(expectedPassed);
|
||||
expect(summarizedTotalCount).toEqual(expectedTotal);
|
||||
};
|
||||
await verifyTestCounts(6, 8);
|
||||
|
||||
const expectedTestOrder = [
|
||||
'folder-pre-check',
|
||||
'req1-pre-check',
|
||||
'req1-pre-check-skipped',
|
||||
'folder-post-check',
|
||||
'req1-post-check',
|
||||
'expected 200 to deeply equal 201',
|
||||
'req2-pre-check',
|
||||
'req2-post-check',
|
||||
];
|
||||
|
||||
const verifyResultRows = async (
|
||||
expectedPassed: number,
|
||||
expectedSkipped: number,
|
||||
expectedTotal: number,
|
||||
expectedTestOrder: string[],
|
||||
) => {
|
||||
let passedResultCount = 0;
|
||||
let failedResultCount = 0;
|
||||
let skippedResultCount = 0;
|
||||
|
||||
const testResults = page.getByTestId('test-result-row');
|
||||
const testResultCount = await testResults.count();
|
||||
|
||||
for (let i = 0; i < testResultCount; i++) {
|
||||
const resultMsg = await testResults.nth(i).textContent();
|
||||
if (resultMsg?.startsWith('PASS')) {
|
||||
passedResultCount++;
|
||||
}
|
||||
if (resultMsg?.startsWith('FAIL')) {
|
||||
failedResultCount++;
|
||||
}
|
||||
if (resultMsg?.startsWith('SKIP')) {
|
||||
skippedResultCount++;
|
||||
}
|
||||
|
||||
const expectedResultText = expectedTestOrder[i];
|
||||
expect(resultMsg).toContain(expectedResultText);
|
||||
}
|
||||
|
||||
expect(passedResultCount).toEqual(expectedPassed);
|
||||
expect(skippedResultCount).toEqual(expectedSkipped);
|
||||
expect(passedResultCount + failedResultCount + skippedResultCount).toEqual(expectedTotal);
|
||||
};
|
||||
await verifyResultRows(6, 1, 8, expectedTestOrder);
|
||||
});
|
||||
});
|
||||
@@ -54,6 +54,7 @@
|
||||
"dompurify": "^3.0.11",
|
||||
"electron-context-menu": "^3.6.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"fastq": "^1.17.1",
|
||||
"grpc-reflection-js": "jackkav/grpc-reflection-js#remove-lodash-set",
|
||||
"hawk": "9.0.2",
|
||||
"hkdf": "^0.0.2",
|
||||
@@ -89,6 +90,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@getinsomnia/api-client": "0.0.4",
|
||||
"@sentry/electron": "^5.1.0",
|
||||
"@stoplight/spectral-core": "^1.18.3",
|
||||
"@stoplight/spectral-formats": "^1.6.0",
|
||||
"@stoplight/spectral-ruleset-bundler": "1.5.2",
|
||||
@@ -129,6 +131,7 @@
|
||||
"@types/vkbeautify": "^0.99.4",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -176,9 +179,7 @@
|
||||
"vite": "^5.2.8",
|
||||
"vkbeautify": "^0.99.3",
|
||||
"ws": "^8.17.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "0.0.34",
|
||||
"@sentry/electron": "^5.1.0"
|
||||
"xpath": "0.0.34"
|
||||
},
|
||||
"dev": {
|
||||
"dev-server-port": 3334
|
||||
|
||||
@@ -579,6 +579,7 @@ export const EXPORT_TYPE_ENVIRONMENT = 'environment';
|
||||
export const EXPORT_TYPE_API_SPEC = 'api_spec';
|
||||
export const EXPORT_TYPE_PROTO_FILE = 'proto_file';
|
||||
export const EXPORT_TYPE_PROTO_DIRECTORY = 'proto_directory';
|
||||
export const EXPORT_TYPE_RUNNER_TEST_RESULT = 'runner_result';
|
||||
|
||||
// (ms) curently server timeout is 30s
|
||||
export const INSOMNIA_FETCH_TIME_OUT = 30_000;
|
||||
|
||||
@@ -3,7 +3,7 @@ import orderedJSON from 'json-order';
|
||||
|
||||
import * as models from '../models';
|
||||
import type { CookieJar } from '../models/cookie-jar';
|
||||
import type { Environment } from '../models/environment';
|
||||
import type { Environment, UserUploadEnvironment } from '../models/environment';
|
||||
import type { GrpcRequest, GrpcRequestBody } from '../models/grpc-request';
|
||||
import { isProject, type Project } from '../models/project';
|
||||
import { PATH_PARAMETER_REGEX, type Request } from '../models/request';
|
||||
@@ -62,6 +62,7 @@ export async function buildRenderContext(
|
||||
subEnvironment,
|
||||
rootGlobalEnvironment,
|
||||
subGlobalEnvironment,
|
||||
userUploadEnv,
|
||||
baseContext = {},
|
||||
}: {
|
||||
ancestors?: RenderContextAncestor[];
|
||||
@@ -69,6 +70,7 @@ export async function buildRenderContext(
|
||||
subEnvironment?: Environment;
|
||||
rootGlobalEnvironment?: Environment | null;
|
||||
subGlobalEnvironment?: Environment | null;
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
baseContext?: Record<string, any>;
|
||||
},
|
||||
) {
|
||||
@@ -127,6 +129,16 @@ export async function buildRenderContext(
|
||||
}
|
||||
}
|
||||
|
||||
// user upload env in collection runner has highest priority
|
||||
if (userUploadEnv) {
|
||||
const ordered = orderedJSON.order(
|
||||
userUploadEnv.data,
|
||||
userUploadEnv.dataPropertyOrder,
|
||||
JSON_ORDER_SEPARATOR,
|
||||
);
|
||||
envObjects.push(ordered);
|
||||
}
|
||||
|
||||
// At this point, environments is a list of environments ordered
|
||||
// from top-most parent to bottom-most child, and they keys in each environment
|
||||
// ordered by its property map.
|
||||
@@ -324,6 +336,7 @@ interface BaseRenderContextOptions {
|
||||
baseEnvironment?: Environment;
|
||||
rootGlobalEnvironment?: Environment;
|
||||
subGlobalEnvironment?: Environment;
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
purpose?: RenderPurpose;
|
||||
extraInfo?: ExtraRenderInfo;
|
||||
ignoreUndefinedEnvVariable?: boolean;
|
||||
@@ -337,6 +350,7 @@ export async function getRenderContext(
|
||||
request,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
userUploadEnv,
|
||||
ancestors: _ancestors,
|
||||
purpose,
|
||||
extraInfo,
|
||||
@@ -440,6 +454,11 @@ export async function getRenderContext(
|
||||
}
|
||||
}
|
||||
|
||||
// Get Keys from user upload environment
|
||||
if (userUploadEnv) {
|
||||
getKeySource(userUploadEnv.data || {}, inKey, userUploadEnv.name || 'uploadData');
|
||||
}
|
||||
|
||||
// Add meta data helper function
|
||||
const baseContext: BaseRenderContext = {
|
||||
getMeta: () => ({
|
||||
@@ -470,6 +489,7 @@ export async function getRenderContext(
|
||||
subGlobalEnvironment,
|
||||
rootEnvironment,
|
||||
subEnvironment: subEnvironment || undefined,
|
||||
userUploadEnv,
|
||||
baseContext,
|
||||
});
|
||||
}
|
||||
@@ -534,6 +554,7 @@ export async function getRenderedRequestAndContext(
|
||||
request,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
userUploadEnv,
|
||||
extraInfo,
|
||||
purpose,
|
||||
ignoreUndefinedEnvVariable,
|
||||
@@ -543,7 +564,7 @@ export async function getRenderedRequestAndContext(
|
||||
const workspace = ancestors.find(isWorkspace);
|
||||
const parentId = workspace ? workspace._id : 'n/a';
|
||||
const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId);
|
||||
const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment });
|
||||
const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnv });
|
||||
|
||||
// HACK: Switch '#}' to '# }' to prevent Nunjucks from barfing
|
||||
// https://github.com/kong/insomnia/issues/895
|
||||
|
||||
@@ -57,6 +57,7 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
|
||||
models.requestGroup.type,
|
||||
models.workspace.type,
|
||||
]);
|
||||
|
||||
const workspaceDoc = ancestors.find(isWorkspace);
|
||||
const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a';
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
@@ -80,28 +81,28 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
|
||||
const responsesDir = path.join(process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : require('electron')).app.getPath('userData'), 'responses');
|
||||
const timelinePath = path.join(responsesDir, responseId + '.timeline');
|
||||
|
||||
return { request, settings, clientCertificates, caCert, environment, activeEnvironmentId, workspace, timelinePath, responseId };
|
||||
return { request, settings, clientCertificates, caCert, environment, activeEnvironmentId, workspace, timelinePath, responseId, ancestors };
|
||||
};
|
||||
// Return callback helper to send requests
|
||||
return async function sendRequest(requestId: string) {
|
||||
const requestData = await fetchInsoRequestData(requestId, environmentId);
|
||||
|
||||
const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id);
|
||||
|
||||
if (mutatedContext === null) {
|
||||
console.error('Time out while executing pre-request script');
|
||||
return null;
|
||||
}
|
||||
const ignoreUndefinedEnvVariable = true;
|
||||
// NOTE: inso ignores active environment, using the one passed in
|
||||
const renderedResult = await tryToInterpolateRequest(
|
||||
mutatedContext.request,
|
||||
mutatedContext.environment,
|
||||
'send',
|
||||
undefined,
|
||||
mutatedContext.baseEnvironment,
|
||||
|
||||
const renderedResult = await tryToInterpolateRequest({
|
||||
request: mutatedContext.request,
|
||||
environment: mutatedContext.environment,
|
||||
purpose: 'send',
|
||||
extraInfo: undefined,
|
||||
baseEnvironment: mutatedContext.baseEnvironment,
|
||||
userUploadEnv: undefined,
|
||||
ignoreUndefinedEnvVariable,
|
||||
);
|
||||
});
|
||||
// skip plugins
|
||||
const renderedRequest = renderedResult.request;
|
||||
|
||||
|
||||
@@ -102,11 +102,17 @@ const runScript = async (
|
||||
name: context.baseEnvironment.name,
|
||||
data: mutatedContextObject.baseEnvironment,
|
||||
},
|
||||
iterationData: context.iterationData ? {
|
||||
name: context.iterationData.name,
|
||||
data: mutatedContextObject.iterationData,
|
||||
} : undefined,
|
||||
request: updatedRequest,
|
||||
execution: mutatedContextObject.execution,
|
||||
settings: updatedSettings,
|
||||
clientCertificates: updatedCertificates,
|
||||
cookieJar: updatedCookieJar,
|
||||
globals: mutatedContextObject.globals,
|
||||
requestTestResults: mutatedContextObject.requestTestResults,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export type MainOnChannels =
|
||||
| 'writeText'
|
||||
| 'addExecutionStep'
|
||||
| 'completeExecutionStep'
|
||||
| 'updateLatestStepName'
|
||||
| 'startExecution';
|
||||
export type RendererOnChannels =
|
||||
'clear-all-models'
|
||||
|
||||
@@ -12,7 +12,7 @@ import { backup, restoreBackup } from '../backup';
|
||||
import installPlugin from '../install-plugin';
|
||||
import type { CurlBridgeAPI } from '../network/curl';
|
||||
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
|
||||
import { addExecutionStep, completeExecutionStep, getExecution, startExecution, type StepName, type TimingStep } from '../network/request-timing';
|
||||
import { addExecutionStep, completeExecutionStep, getExecution, startExecution, type TimingStep, updateLatestStepName } from '../network/request-timing';
|
||||
import type { WebSocketBridgeAPI } from '../network/websocket';
|
||||
import { ipcMainHandle, ipcMainOn, ipcMainOnce, type RendererOnChannels } from './electron';
|
||||
import extractPostmanDataDumpHandler from './extractPostmanDataDump';
|
||||
@@ -47,14 +47,15 @@ export interface RendererToMainBridgeAPI {
|
||||
};
|
||||
hiddenBrowserWindow: HiddenBrowserWindowBridgeAPI;
|
||||
getExecution: (options: { requestId: string }) => Promise<TimingStep[]>;
|
||||
addExecutionStep: (options: { requestId: string; stepName: StepName }) => void;
|
||||
addExecutionStep: (options: { requestId: string; stepName: string }) => void;
|
||||
startExecution: (options: { requestId: string }) => void;
|
||||
completeExecutionStep: (options: { requestId: string }) => void;
|
||||
updateLatestStepName: (options: { requestId: string; stepName: string }) => void;
|
||||
landingPageRendered: (landingPage: LandingPage, tags?: Record<string, string>) => void;
|
||||
extractJsonFileFromPostmanDataDumpArchive: (archivePath: string) => Promise<any>;
|
||||
}
|
||||
export function registerMainHandlers() {
|
||||
ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: StepName }) => {
|
||||
ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: string }) => {
|
||||
addExecutionStep(options.requestId, options.stepName);
|
||||
});
|
||||
ipcMainOn('startExecution', (_, options: { requestId: string }) => {
|
||||
@@ -63,6 +64,9 @@ export function registerMainHandlers() {
|
||||
ipcMainOn('completeExecutionStep', (_, options: { requestId: string }) => {
|
||||
return completeExecutionStep(options.requestId);
|
||||
});
|
||||
ipcMainOn('updateLatestStepName', (_, options: { requestId: string; stepName: string }) => {
|
||||
updateLatestStepName(options.requestId, options.stepName);
|
||||
});
|
||||
ipcMainHandle('getExecution', (_, options: { requestId: string }) => {
|
||||
return getExecution(options.requestId);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
export type StepName = 'Executing pre-request script'
|
||||
| 'Rendering request'
|
||||
| 'Sending request'
|
||||
| 'Executing after-response script';
|
||||
|
||||
export interface TimingStep {
|
||||
stepName: StepName;
|
||||
stepName: string;
|
||||
startedAt: number;
|
||||
duration?: number;
|
||||
}
|
||||
export const executions = new Map<string, TimingStep[]>();
|
||||
|
||||
export const getExecution = (requestId?: string) => requestId ? executions.get(requestId) : [];
|
||||
|
||||
export const startExecution = (requestId: string) => executions.set(requestId, []);
|
||||
|
||||
export function addExecutionStep(
|
||||
requestId: string,
|
||||
stepName: StepName,
|
||||
stepName: string,
|
||||
) {
|
||||
// append to new step to execution
|
||||
const record: TimingStep = {
|
||||
@@ -28,6 +26,7 @@ export function addExecutionStep(
|
||||
window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) });
|
||||
}
|
||||
}
|
||||
|
||||
export function completeExecutionStep(requestId: string) {
|
||||
const latest = executions.get(requestId)?.at(-1);
|
||||
if (latest) {
|
||||
@@ -37,3 +36,19 @@ export function completeExecutionStep(requestId: string) {
|
||||
window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) });
|
||||
}
|
||||
}
|
||||
|
||||
export function updateLatestStepName(
|
||||
executionId: string,
|
||||
stepName: string,
|
||||
) {
|
||||
const steps = executions.get(executionId) || [];
|
||||
if (steps.length > 0) {
|
||||
const latestStep = steps[steps.length - 1];
|
||||
latestStep.stepName = stepName;
|
||||
executions.set(executionId, steps);
|
||||
}
|
||||
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
window.webContents.send(`syncTimers.${executionId}`, { executions: executions.get(executionId) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface BaseEnvironment {
|
||||
}
|
||||
|
||||
export type Environment = BaseModel & BaseEnvironment;
|
||||
export type UserUploadEnvironment = Pick<Environment, 'data' | 'dataPropertyOrder' | 'name'>;
|
||||
|
||||
export const isEnvironment = (model: Pick<BaseModel, 'type'>): model is Environment => (
|
||||
model.type === type
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
EXPORT_TYPE_PROTO_FILE,
|
||||
EXPORT_TYPE_REQUEST,
|
||||
EXPORT_TYPE_REQUEST_GROUP,
|
||||
EXPORT_TYPE_RUNNER_TEST_RESULT,
|
||||
EXPORT_TYPE_UNIT_TEST,
|
||||
EXPORT_TYPE_UNIT_TEST_SUITE,
|
||||
EXPORT_TYPE_WEBSOCKET_PAYLOAD,
|
||||
@@ -37,6 +38,7 @@ import * as _requestGroupMeta from './request-group-meta';
|
||||
import * as _requestMeta from './request-meta';
|
||||
import * as _requestVersion from './request-version';
|
||||
import * as _response from './response';
|
||||
import * as _runnerTestResult from './runner-test-result';
|
||||
import * as _settings from './settings';
|
||||
import * as _stats from './stats';
|
||||
import * as _unitTest from './unit-test';
|
||||
@@ -78,6 +80,7 @@ export const requestGroup = _requestGroup;
|
||||
export const requestGroupMeta = _requestGroupMeta;
|
||||
export const requestMeta = _requestMeta;
|
||||
export const requestVersion = _requestVersion;
|
||||
export const runnerTestResult = _runnerTestResult;
|
||||
export const response = _response;
|
||||
export const settings = _settings;
|
||||
export const project = _project;
|
||||
@@ -130,6 +133,7 @@ export function all() {
|
||||
protoDirectory,
|
||||
grpcRequest,
|
||||
grpcRequestMeta,
|
||||
runnerTestResult,
|
||||
webSocketPayload,
|
||||
webSocketRequest,
|
||||
webSocketResponse,
|
||||
@@ -225,6 +229,7 @@ export const MODELS_BY_EXPORT_TYPE: Record<string, any> = {
|
||||
[EXPORT_TYPE_MOCK_SERVER]: mockServer,
|
||||
[EXPORT_TYPE_MOCK_ROUTE]: mockRoute,
|
||||
[EXPORT_TYPE_GRPC_REQUEST]: grpcRequest,
|
||||
[EXPORT_TYPE_RUNNER_TEST_RESULT]: runnerTestResult,
|
||||
[EXPORT_TYPE_REQUEST_GROUP]: requestGroup,
|
||||
[EXPORT_TYPE_UNIT_TEST_SUITE]: unitTestSuite,
|
||||
[EXPORT_TYPE_UNIT_TEST]: unitTest,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import type { RequestTestResult } from 'insomnia-sdk';
|
||||
import { Readable } from 'stream';
|
||||
import zlib from 'zlib';
|
||||
|
||||
@@ -46,6 +47,7 @@ export interface BaseResponse {
|
||||
// Things from the request
|
||||
settingStoreCookies: boolean | null;
|
||||
settingSendCookies: boolean | null;
|
||||
requestTestResults: RequestTestResult[];
|
||||
}
|
||||
|
||||
export type Response = BaseModel & BaseResponse;
|
||||
@@ -80,6 +82,7 @@ export function init(): BaseResponse {
|
||||
// Responses sent before environment filtering will have a special value
|
||||
// so they don't show up at all when filtering is on.
|
||||
environmentId: '__LEGACY__',
|
||||
requestTestResults: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
97
packages/insomnia/src/models/runner-test-result.ts
Normal file
97
packages/insomnia/src/models/runner-test-result.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { RequestTestResult } from 'insomnia-sdk';
|
||||
|
||||
import { database as db } from '../common/database';
|
||||
import type { RunnerSource } from '../ui/routes/request';
|
||||
import type { BaseModel } from './index';
|
||||
|
||||
export const name = 'Runner Test Result';
|
||||
|
||||
export const type = 'RunnerTestResult';
|
||||
|
||||
export const prefix = 'rtr';
|
||||
|
||||
export const canDuplicate = false;
|
||||
|
||||
export const canSync = false;
|
||||
|
||||
export interface RunnerResultPerRequest {
|
||||
results: RequestTestResult[];
|
||||
requestName: string;
|
||||
requestUrl: string;
|
||||
responseCode: number;
|
||||
// TODO: add request name, url, etc
|
||||
}
|
||||
|
||||
export interface ResponseInfo {
|
||||
responseId: string;
|
||||
originalRequestName: string;
|
||||
originalRequestId: string;
|
||||
}
|
||||
|
||||
export interface BaseRunnerTestResult {
|
||||
source: RunnerSource;
|
||||
// environmentId: string;
|
||||
iterations: number;
|
||||
duration: number; // millisecond
|
||||
avgRespTime: number; // millisecond
|
||||
iterationResults: RunnerResultPerRequest[][];
|
||||
responsesInfo: ResponseInfo[];
|
||||
version: '1';
|
||||
}
|
||||
|
||||
export type RunnerTestResult = BaseModel & BaseRunnerTestResult;
|
||||
|
||||
export const isRunnerTestResult = (model: Pick<BaseModel, 'type'>): model is RunnerTestResult => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
source: 'runner',
|
||||
// environmentId: string;
|
||||
iterations: 0,
|
||||
duration: 0,
|
||||
avgRespTime: 0,
|
||||
iterationResults: [],
|
||||
responsesInfo: [],
|
||||
version: '1',
|
||||
};
|
||||
}
|
||||
|
||||
export function migrate(doc: RunnerTestResult) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create(patch: Partial<RunnerTestResult> = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New RunnerTestResult missing `parentId` ' + JSON.stringify(patch));
|
||||
}
|
||||
|
||||
return db.docCreate(type, patch);
|
||||
}
|
||||
|
||||
export function update(testResult: RunnerTestResult, patch: Partial<RunnerTestResult>) {
|
||||
return db.docUpdate(testResult, patch);
|
||||
}
|
||||
|
||||
export function getByParentId(parentId: string) {
|
||||
return db.getWhere<RunnerTestResult>(type, { parentId });
|
||||
}
|
||||
|
||||
export function getLatestByParentId(parentId: string) {
|
||||
return db.getMostRecentlyModified<RunnerTestResult>(type, { parentId });
|
||||
}
|
||||
|
||||
export function getById(_id: string) {
|
||||
return db.getWhere<RunnerTestResult>(type, {
|
||||
_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return db.all<RunnerTestResult>(type);
|
||||
}
|
||||
|
||||
export function findByParentId(parentId: string) {
|
||||
return db.find<RunnerTestResult>(type, { parentId: parentId });
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { CurlRequestOptions, CurlRequestOutput } from '../main/network/libc
|
||||
import type { CookieJar } from '../models/cookie-jar';
|
||||
import type { Request } from '../models/request';
|
||||
import { runScript as nodejsRunScript } from '../scriptExecutor';
|
||||
|
||||
const cancelRequestFunctionMap = new Map<string, () => void>();
|
||||
|
||||
export async function cancelRequestById(requestId: string) {
|
||||
@@ -15,17 +16,39 @@ export async function cancelRequestById(requestId: string) {
|
||||
console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`);
|
||||
}
|
||||
|
||||
export const cancellableExecution = async (options: { id: string; fn: Promise<any> }) => {
|
||||
const controller = new AbortController();
|
||||
const cancelRequest = () => {
|
||||
// TODO: implement cancelPreRequestScript on hiddenBrowserWindow side?
|
||||
controller.abort();
|
||||
};
|
||||
cancelRequestFunctionMap.set(options.id, cancelRequest);
|
||||
|
||||
try {
|
||||
return await cancellablePromise({
|
||||
signal: controller.signal,
|
||||
fn: options.fn,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
throw new Error('Request was cancelled');
|
||||
}
|
||||
console.log('[network] Error', err);
|
||||
throw err;
|
||||
} finally {
|
||||
cancelRequestFunctionMap.delete(options.id);
|
||||
}
|
||||
};
|
||||
|
||||
export const cancellableRunScript = async (options: { script: string; context: RequestContext }) => {
|
||||
const request = options.context.request;
|
||||
const requestId = request._id;
|
||||
|
||||
const controller = new AbortController();
|
||||
const cancelRequest = () => {
|
||||
// TODO: implement cancelPreRequestScript on hiddenBrowserWindow side?
|
||||
controller.abort();
|
||||
};
|
||||
cancelRequestFunctionMap.set(requestId, cancelRequest);
|
||||
|
||||
try {
|
||||
const result = await cancellablePromise({
|
||||
signal: controller.signal,
|
||||
|
||||
61
packages/insomnia/src/network/concurrency.ts
Normal file
61
packages/insomnia/src/network/concurrency.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { queueAsPromised } from 'fastq';
|
||||
import * as fastq from 'fastq';
|
||||
import type { RequestContext, RequestTestResult } from 'insomnia-sdk';
|
||||
|
||||
import type { ClientCertificate } from '../models/client-certificate';
|
||||
import type { CookieJar } from '../models/cookie-jar';
|
||||
import type { Environment, UserUploadEnvironment } from '../models/environment';
|
||||
import type { Request } from '../models/request';
|
||||
import type { Settings } from '../models/settings';
|
||||
import { cancellableExecution } from './cancellation';
|
||||
|
||||
export interface ExecuteScriptContext {
|
||||
request: Request;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
data: object;
|
||||
};
|
||||
baseEnvironment: {
|
||||
id: string;
|
||||
name: string;
|
||||
data: object;
|
||||
};
|
||||
clientCertificates: ClientCertificate[];
|
||||
settings: Settings;
|
||||
globals?: object;
|
||||
cookieJar: CookieJar;
|
||||
requestTestResults?: RequestTestResult[];
|
||||
};
|
||||
|
||||
export interface TransformedExecuteScriptContext {
|
||||
error?: string;
|
||||
request: Request;
|
||||
environment: Environment;
|
||||
baseEnvironment: Environment;
|
||||
clientCertificates: ClientCertificate[];
|
||||
settings: Settings;
|
||||
globals?: Environment;
|
||||
cookieJar: CookieJar;
|
||||
requestTestResults?: RequestTestResult[];
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
}
|
||||
|
||||
interface Task {
|
||||
script: string;
|
||||
context: RequestContext;
|
||||
};
|
||||
|
||||
const q: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||
|
||||
async function asyncWorker(arg: Task): Promise<any> {
|
||||
const timeoutValue = arg.context.settings.timeout || 30000;
|
||||
const timeoutPromise = new Promise<{ error: string }>(resolve => setTimeout(resolve, timeoutValue, { error: `Executing script timeout: ${timeoutValue}` }));
|
||||
const executionPromise = Promise.race([window.main.hiddenBrowserWindow.runScript({ script: arg.script, context: arg.context }), timeoutPromise]);
|
||||
const result = await cancellableExecution({ id: arg.context.request._id, fn: executionPromise });
|
||||
return result;
|
||||
}
|
||||
|
||||
export const runScriptConcurrently = async (options: { script: string; context: RequestContext }): Promise<RequestContext | { error: string }> => {
|
||||
return await q.push(options);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import clone from 'clone';
|
||||
import fs from 'fs';
|
||||
import type { RequestContext, RequestTestResult } from 'insomnia-sdk';
|
||||
import orderedJSON from 'json-order';
|
||||
import { join as pathJoin } from 'path';
|
||||
|
||||
@@ -21,10 +22,11 @@ import * as models from '../models';
|
||||
import type { CaCertificate } from '../models/ca-certificate';
|
||||
import type { ClientCertificate } from '../models/client-certificate';
|
||||
import type { Cookie, CookieJar } from '../models/cookie-jar';
|
||||
import type { Environment } from '../models/environment';
|
||||
import type { Environment, UserUploadEnvironment } from '../models/environment';
|
||||
import type { MockRoute } from '../models/mock-route';
|
||||
import type { MockServer } from '../models/mock-server';
|
||||
import type { Request, RequestAuthentication, RequestHeader, RequestParameter } from '../models/request';
|
||||
import { isProject, type Project } from '../models/project';
|
||||
import { isRequest, type Request, type RequestAuthentication, type RequestHeader, type RequestParameter } from '../models/request';
|
||||
import { isRequestGroup, type RequestGroup } from '../models/request-group';
|
||||
import type { Settings } from '../models/settings';
|
||||
import type { WebSocketRequest } from '../models/websocket-request';
|
||||
@@ -40,6 +42,7 @@ import {
|
||||
import { getAuthHeader, getAuthObjectOrNull, getAuthQueryParams, isAuthEnabled } from './authentication';
|
||||
import { cancellableCurlRequest, cancellableRunScript } from './cancellation';
|
||||
import { filterClientCertificates } from './certificate';
|
||||
import { runScriptConcurrently, type TransformedExecuteScriptContext } from './concurrency';
|
||||
import { addSetCookiesToToughCookieJar } from './set-cookie-util';
|
||||
|
||||
export const getOrInheritAuthentication = ({ request, requestGroups }: { request: Request | WebSocketRequest; requestGroups: RequestGroup[] }): RequestAuthentication | {} => {
|
||||
@@ -105,10 +108,11 @@ export const fetchRequestGroupData = async (requestGroupId: string) => {
|
||||
export const fetchRequestData = async (requestId: string) => {
|
||||
const request = await models.request.getById(requestId);
|
||||
invariant(request, 'failed to find request ' + requestId);
|
||||
const ancestors = await db.withAncestors<Request | RequestGroup | Workspace | MockRoute | MockServer>(request, [
|
||||
const ancestors = await db.withAncestors<Request | RequestGroup | Workspace | Project | MockRoute | MockServer>(request, [
|
||||
models.request.type,
|
||||
models.requestGroup.type,
|
||||
models.workspace.type,
|
||||
models.project.type,
|
||||
models.mockRoute.type,
|
||||
models.mockServer.type,
|
||||
]);
|
||||
@@ -136,17 +140,24 @@ export const fetchRequestData = async (requestId: string) => {
|
||||
const responseId = generateId('res');
|
||||
const responsesDir = pathJoin((process.type === 'renderer' ? window : require('electron')).app.getPath('userData'), 'responses');
|
||||
const timelinePath = pathJoin(responsesDir, responseId + '.timeline');
|
||||
return { request, environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId };
|
||||
return { request, environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId, ancestors };
|
||||
};
|
||||
|
||||
export const tryToExecutePreRequestScript = async ({
|
||||
request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
timelinePath,
|
||||
responseId,
|
||||
}: Awaited<ReturnType<typeof fetchRequestData>>, workspaceId: string) => {
|
||||
export const tryToExecutePreRequestScript = async (
|
||||
{
|
||||
request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
timelinePath,
|
||||
responseId,
|
||||
ancestors,
|
||||
}: Awaited<ReturnType<typeof fetchRequestData>>,
|
||||
workspaceId: string,
|
||||
userUploadEnv?: UserUploadEnvironment,
|
||||
iteration?: number,
|
||||
iterationCount?: number,
|
||||
) => {
|
||||
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
|
||||
const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
|
||||
@@ -156,10 +167,7 @@ export const tryToExecutePreRequestScript = async ({
|
||||
activeGlobalEnvironment = await models.environment.getById(workspaceMeta.activeGlobalEnvironmentId) || undefined;
|
||||
}
|
||||
|
||||
const requestGroups = await db.withAncestors<Request | RequestGroup>(request, [
|
||||
models.requestGroup.type,
|
||||
]) as (Request | RequestGroup)[];
|
||||
|
||||
const requestGroups = ancestors.filter(doc => isRequest(doc) || isRequestGroup(doc)) as RequestGroup[];
|
||||
const folderScripts = requestGroups.reverse()
|
||||
.filter(group => group?.preRequestScript)
|
||||
.map((group, i) => `const fn${i} = async ()=>{
|
||||
@@ -176,6 +184,8 @@ export const tryToExecutePreRequestScript = async ({
|
||||
settings,
|
||||
cookieJar,
|
||||
globals: activeGlobalEnvironment,
|
||||
userUploadEnv,
|
||||
requestTestResults: new Array<RequestTestResult>(),
|
||||
};
|
||||
}
|
||||
const joinedScript = [...folderScripts].join('\n');
|
||||
@@ -190,13 +200,26 @@ export const tryToExecutePreRequestScript = async ({
|
||||
clientCertificates,
|
||||
cookieJar,
|
||||
globals: activeGlobalEnvironment,
|
||||
userUploadEnv,
|
||||
iteration,
|
||||
iterationCount,
|
||||
ancestors,
|
||||
eventName: 'prerequest',
|
||||
settings,
|
||||
});
|
||||
if (!mutatedContext?.request) {
|
||||
// exiy early if there was a problem with the pre-request script
|
||||
// TODO: improve error message?
|
||||
return null;
|
||||
if (!mutatedContext || 'error' in mutatedContext) {
|
||||
return {
|
||||
error: `Execute pre-request script failed: ${mutatedContext?.error}`,
|
||||
request,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
clientCertificates,
|
||||
settings,
|
||||
cookieJar,
|
||||
globals: activeGlobalEnvironment,
|
||||
requestTestResults: new Array<RequestTestResult>(),
|
||||
};
|
||||
}
|
||||
|
||||
await savePatchesMadeByScript(mutatedContext, environment, baseEnvironment, activeGlobalEnvironment);
|
||||
return {
|
||||
request: mutatedContext.request,
|
||||
@@ -206,6 +229,9 @@ export const tryToExecutePreRequestScript = async ({
|
||||
settings: mutatedContext.settings || settings,
|
||||
globals: mutatedContext.globals,
|
||||
cookieJar: mutatedContext.cookieJar,
|
||||
requestTestResults: mutatedContext.requestTestResults,
|
||||
userUploadEnv: mutatedContext.userUploadEnv,
|
||||
execution: mutatedContext.execution,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -214,7 +240,7 @@ export const tryToExecutePreRequestScript = async ({
|
||||
// - If no global environment is seleted, no operation
|
||||
// - If one global environment is selected, it persists content to the selected global environment (base or sub).
|
||||
export async function savePatchesMadeByScript(
|
||||
mutatedContext: Awaited<ReturnType<typeof tryToExecuteScript>>,
|
||||
mutatedContext: TransformedExecuteScriptContext,
|
||||
environment: Environment,
|
||||
baseEnvironment: Environment,
|
||||
activeGlobalEnvironment: Environment | undefined,
|
||||
@@ -267,22 +293,19 @@ export async function savePatchesMadeByScript(
|
||||
}
|
||||
|
||||
export const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse) => {
|
||||
const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals } = context;
|
||||
const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals, userUploadEnv, iteration, iterationCount, ancestors, eventName } = context;
|
||||
invariant(script, 'script must be provided');
|
||||
|
||||
const settings = await models.settings.get();
|
||||
// location is the complete path of a request, including project, collection and folder(if have).
|
||||
const requestLocation = ancestors
|
||||
.filter(doc => isRequest(doc) || isRequestGroup(doc) || isWorkspace(doc) || isProject(doc))
|
||||
.reverse()
|
||||
.map(doc => doc.name);
|
||||
|
||||
try {
|
||||
const timeout = settings.timeout || 5000;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout: Hidden browser window is not responding'));
|
||||
// Add one extra second to ensure the hidden browser window has had a chance to return its timeout
|
||||
// TODO: restart the hidden browser window
|
||||
}, timeout + 2000);
|
||||
});
|
||||
|
||||
const executionPromise = cancellableRunScript({
|
||||
const fn = process.type === 'renderer' ? runScriptConcurrently : cancellableRunScript;
|
||||
const originalOutput = await fn({
|
||||
script,
|
||||
context: {
|
||||
request,
|
||||
@@ -303,23 +326,28 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
|
||||
clientCertificates,
|
||||
settings,
|
||||
cookieJar,
|
||||
requestInfo: {
|
||||
eventName: eventName === 'prerequest' ? 'prerequest' : 'test',
|
||||
iterationCount,
|
||||
iteration,
|
||||
},
|
||||
response,
|
||||
globals: globals?.data || undefined,
|
||||
iterationData: userUploadEnv ? {
|
||||
name: userUploadEnv.name,
|
||||
data: userUploadEnv.data || {},
|
||||
} : undefined,
|
||||
execution: {
|
||||
location: requestLocation,
|
||||
},
|
||||
},
|
||||
});
|
||||
// @TODO This looks overly complicated and could be simplified.
|
||||
// If the timeout promise finishes first the execution promise is still running while it should be cancelled.
|
||||
// Since the execution promise is cancellable and uses an abort controller we should add the timeout to the controller instead
|
||||
const output = await Promise.race([timeoutPromise, executionPromise]) as {
|
||||
request: Request;
|
||||
environment: Record<string, any>;
|
||||
baseEnvironment: Record<string, any>;
|
||||
settings: Settings;
|
||||
clientCertificates: ClientCertificate[];
|
||||
cookieJar: CookieJar;
|
||||
globals: Record<string, any>;
|
||||
};
|
||||
console.log('[network] script execution succeeded', output);
|
||||
if ('error' in originalOutput) {
|
||||
return { error: `Script executor returns error: ${originalOutput.error}` };
|
||||
}
|
||||
console.log('[network] script execution succeeded', originalOutput);
|
||||
|
||||
const output = originalOutput as RequestContext;
|
||||
|
||||
const envPropertyOrder = orderedJSON.parse(
|
||||
JSON.stringify(output.environment.data),
|
||||
@@ -343,10 +371,20 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
|
||||
JSON_ORDER_PREFIX,
|
||||
JSON_ORDER_SEPARATOR,
|
||||
);
|
||||
globals.data = output.globals;
|
||||
globals.data = output.globals || {};
|
||||
globals.dataPropertyOrder = globalEnvPropertyOrder.map;
|
||||
}
|
||||
|
||||
if (userUploadEnv) {
|
||||
const userUploadEnvPropertyOrder = orderedJSON.parse(
|
||||
JSON.stringify(output?.iterationData?.data || {}),
|
||||
JSON_ORDER_PREFIX,
|
||||
JSON_ORDER_SEPARATOR,
|
||||
);
|
||||
userUploadEnv.data = output?.iterationData?.data || {};
|
||||
userUploadEnv.dataPropertyOrder = userUploadEnvPropertyOrder.map;
|
||||
}
|
||||
|
||||
return {
|
||||
request: output.request,
|
||||
environment,
|
||||
@@ -355,6 +393,9 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
|
||||
clientCertificates: output.clientCertificates,
|
||||
cookieJar: output.cookieJar,
|
||||
globals,
|
||||
userUploadEnv,
|
||||
requestTestResults: output.requestTestResults,
|
||||
execution: output.execution,
|
||||
};
|
||||
} catch (err) {
|
||||
await fs.promises.appendFile(
|
||||
@@ -373,7 +414,7 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
|
||||
};
|
||||
const res = await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: res._id });
|
||||
return null;
|
||||
return { error: err };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -385,23 +426,28 @@ interface RequestContextForScript {
|
||||
baseEnvironment: Environment;
|
||||
clientCertificates: ClientCertificate[];
|
||||
cookieJar: CookieJar;
|
||||
ancestors: (Request | RequestGroup | Workspace | Project | MockRoute | MockServer)[];
|
||||
globals?: Environment; // there could be no global environment
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
type RequestAndContextAndResponse = RequestContextForScript & {
|
||||
response: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse;
|
||||
iteration?: number;
|
||||
iterationCount?: number;
|
||||
};
|
||||
|
||||
type RequestAndContextAndOptionalResponse = RequestContextForScript & {
|
||||
script: string;
|
||||
response?: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse;
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
iteration?: number;
|
||||
iterationCount?: number;
|
||||
eventName?: RequestContext['requestInfo']['eventName'];
|
||||
};
|
||||
|
||||
export async function tryToExecuteAfterResponseScript(context: RequestAndContextAndResponse) {
|
||||
const requestGroups = await db.withAncestors<Request | RequestGroup>(context.request, [
|
||||
models.requestGroup.type,
|
||||
]) as (Request | RequestGroup)[];
|
||||
|
||||
const requestGroups = context.ancestors.filter(doc => isRequest(doc) || isRequestGroup(doc)) as RequestGroup[];
|
||||
const folderScripts = requestGroups.reverse()
|
||||
.filter(group => group?.afterResponseScript)
|
||||
.map((group, i) => `const fn${i} = async ()=>{
|
||||
@@ -410,13 +456,18 @@ export async function tryToExecuteAfterResponseScript(context: RequestAndContext
|
||||
await fn${i}();
|
||||
`);
|
||||
if (folderScripts.length === 0) {
|
||||
return context;
|
||||
return {
|
||||
...context,
|
||||
requestTestResults: new Array<RequestTestResult>(),
|
||||
};
|
||||
}
|
||||
const joinedScript = [...folderScripts].join('\n');
|
||||
|
||||
const postMutatedContext = await tryToExecuteScript({ script: joinedScript, ...context });
|
||||
if (!postMutatedContext?.request) {
|
||||
return null;
|
||||
const postMutatedContext = await tryToExecuteScript({ script: joinedScript, ...context, eventName: 'test' });
|
||||
if (!postMutatedContext || 'error' in postMutatedContext) {
|
||||
return {
|
||||
error: `Execute after-response script failed: ${postMutatedContext?.error}`,
|
||||
...context,
|
||||
};
|
||||
}
|
||||
|
||||
// cookies from response should also be persisted
|
||||
@@ -431,19 +482,30 @@ export async function tryToExecuteAfterResponseScript(context: RequestAndContext
|
||||
return postMutatedContext;
|
||||
}
|
||||
|
||||
export const tryToInterpolateRequest = async (
|
||||
request: Request,
|
||||
environment: string | Environment,
|
||||
purpose?: RenderPurpose,
|
||||
extraInfo?: ExtraRenderInfo,
|
||||
baseEnvironment?: Environment,
|
||||
ignoreUndefinedEnvVariable?: boolean,
|
||||
export const tryToInterpolateRequest = async ({
|
||||
request,
|
||||
environment,
|
||||
purpose,
|
||||
extraInfo,
|
||||
baseEnvironment,
|
||||
userUploadEnv,
|
||||
ignoreUndefinedEnvVariable,
|
||||
}: {
|
||||
request: Request;
|
||||
environment: string | Environment;
|
||||
purpose?: RenderPurpose;
|
||||
extraInfo?: ExtraRenderInfo;
|
||||
baseEnvironment?: Environment;
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
ignoreUndefinedEnvVariable?: boolean;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await getRenderedRequestAndContext({
|
||||
request: request,
|
||||
environment,
|
||||
baseEnvironment,
|
||||
userUploadEnv,
|
||||
purpose,
|
||||
extraInfo,
|
||||
ignoreUndefinedEnvVariable,
|
||||
|
||||
@@ -332,7 +332,7 @@ const sendAccessTokenRequest = async (requestOrGroupId: string, authentication:
|
||||
parentId: requestOrGroupId,
|
||||
});
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(newRequest, environment._id);
|
||||
const renderResult = await tryToInterpolateRequest({ request: newRequest, environment: environment._id });
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getSendRequestCallback() {
|
||||
timelinePath,
|
||||
responseId,
|
||||
} = await fetchRequestData(requestId);
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, 'send');
|
||||
const renderResult = await tryToInterpolateRequest({ request, environment: environment._id, purpose: 'send' });
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
|
||||
|
||||
@@ -17,7 +17,7 @@ export function init() {
|
||||
responseId,
|
||||
} = await fetchRequestData(req._id);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, 'send', extraInfo);
|
||||
const renderResult = await tryToInterpolateRequest({ request, environment: environment._id, purpose: 'send', extraInfo });
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
|
||||
@@ -44,6 +44,7 @@ const main: Window['main'] = {
|
||||
startExecution: options => ipcRenderer.send('startExecution', options),
|
||||
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
|
||||
completeExecutionStep: options => ipcRenderer.send('completeExecutionStep', options),
|
||||
updateLatestStepName: options => ipcRenderer.send('updateLatestStepName', options),
|
||||
getExecution: options => ipcRenderer.invoke('getExecution', options),
|
||||
loginStateChange: () => ipcRenderer.send('loginStateChange'),
|
||||
restart: () => ipcRenderer.send('restart'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { type FC, type ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Collection, Dialog, Header, Heading, Menu, MenuItem, MenuTrigger, Modal, ModalOverlay, Popover, Section } from 'react-aria-components';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useFetcher, useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getProductName } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
@@ -30,7 +30,7 @@ import { ImportModal } from '../modals/import-modal';
|
||||
import { WorkspaceDuplicateModal } from '../modals/workspace-duplicate-modal';
|
||||
import { WorkspaceSettingsModal } from '../modals/workspace-settings-modal';
|
||||
|
||||
export const WorkspaceDropdown: FC = () => {
|
||||
export const WorkspaceDropdown: FC<{}> = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
invariant(organizationId, 'Expected organizationId');
|
||||
const { userSession } = useRootLoaderData();
|
||||
@@ -50,6 +50,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
const deleteWorkspaceFetcher = useFetcher();
|
||||
const [actionPlugins, setActionPlugins] = useState<WorkspaceAction[]>([]);
|
||||
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
|
||||
const navigate = useNavigate();
|
||||
|
||||
// after duplicate workspace, close the modal
|
||||
useEffect(() => {
|
||||
@@ -164,6 +165,21 @@ export const WorkspaceDropdown: FC = () => {
|
||||
action: () => setIsImportModalOpen(true),
|
||||
}],
|
||||
},
|
||||
{
|
||||
name: 'Runner',
|
||||
id: 'runner',
|
||||
icon: 'circle-play',
|
||||
items: [
|
||||
{
|
||||
id: 'run',
|
||||
name: 'Run Collection',
|
||||
icon: <Icon icon='circle-play' />,
|
||||
action: () => {
|
||||
navigate(`/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/${activeWorkspace._id}/debug/runner`,);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
id: 'actions',
|
||||
|
||||
@@ -119,7 +119,7 @@ const fetchGraphQLSchemaForRequest = async ({
|
||||
responseId,
|
||||
} = await fetchRequestData(introspectionRequest._id);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, 'send');
|
||||
const renderResult = await tryToInterpolateRequest({ request, environment: environment._id, purpose: 'send' });
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const res = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
|
||||
@@ -37,11 +37,12 @@ export const MockResponseExtractor = () => {
|
||||
const mimeType = maybeMimeType && isInMockContentTypeList(maybeMimeType) ? maybeMimeType : 'text/plain';
|
||||
return (
|
||||
<div className="px-32 h-full flex flex-col justify-center">
|
||||
<div className="flex place-content-center text-9xl pb-2 text-[--hl-md]">
|
||||
<div className="flex place-content-center text-9xl pb-8 text-[--hl-md]">
|
||||
<Icon icon="cube" />
|
||||
</div>
|
||||
<div className="flex place-content-center pb-2">
|
||||
Transform this {getContentTypeName(activeResponse?.contentType) || ''} response to a new mock route or overwrite an existing one.
|
||||
Transform this
|
||||
{activeResponse?.contentType ? getContentTypeName(activeResponse?.contentType) === 'Other' ? '' : ` ${getContentTypeName(activeResponse?.contentType)}` : ''} response to a new mock route or overwrite an existing one.
|
||||
</div>
|
||||
<form
|
||||
onSubmit={async e => {
|
||||
@@ -151,7 +152,7 @@ export const MockResponseExtractor = () => {
|
||||
setSelectedMockRoute('');
|
||||
}}
|
||||
>
|
||||
<option value="">-- Create new... --</option>
|
||||
<option value="">-- Create new --</option>
|
||||
{mockServerAndRoutes
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
@@ -177,7 +178,7 @@ export const MockResponseExtractor = () => {
|
||||
setSelectedMockRoute(selected);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Create new... --</option>
|
||||
<option value="">-- Create new --</option>
|
||||
{mockServerAndRoutes.find(s => s._id === selectedMockServer)?.routes
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Snippet } from 'codemirror';
|
||||
import { CookieObject, Environment, InsomniaObject, Request as ScriptRequest, RequestInfo, Url, Variables } from 'insomnia-sdk';
|
||||
import { CookieObject, Environment, Execution, InsomniaObject, Request as ScriptRequest, RequestInfo, Url, Variables } from 'insomnia-sdk';
|
||||
import React, { type FC, useRef } from 'react';
|
||||
import { Button, Collection, Header, Menu, MenuItem, MenuTrigger, Popover, Section, Toolbar } from 'react-aria-components';
|
||||
|
||||
@@ -507,10 +507,10 @@ export const RequestScriptEditor: FC<Props> = ({
|
||||
requestName: '',
|
||||
requestId: '',
|
||||
}),
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(_msg: string, _fn: () => void) => { }
|
||||
),
|
||||
execution: new Execution({
|
||||
location: ['path'],
|
||||
}),
|
||||
}),
|
||||
'insomnia',
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Cell,
|
||||
Column,
|
||||
Dialog,
|
||||
FileTrigger,
|
||||
Heading,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
Row,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { Icon } from '../icon';
|
||||
|
||||
export type UploadDataType = Record<string, any>;
|
||||
export interface UploadDataModalProps {
|
||||
onUploadFile: (file: File | null, data: UploadDataType[]) => void;
|
||||
onClose: () => void;
|
||||
userUploadData: UploadDataType[];
|
||||
}
|
||||
|
||||
const rowHeaderStyle = 'sticky normal-case top-[-8px] p-2 z-10 border-b border-[--hl-sm] bg-[--hl-xs] text-left text-xs font-semibold backdrop-blur backdrop-filter focus:outline-none';
|
||||
const rowCellStyle = 'whitespace-nowrap text-sm font-medium border-b border-solid border-[--hl-sm] group-last-of-type:border-none focus:outline-none';
|
||||
|
||||
export const UploadDataModal = ({ onUploadFile, onClose, userUploadData }: UploadDataModalProps) => {
|
||||
const [file, setUploadFile] = useState<File | null>(null);
|
||||
const [uploadDataHeaders, setUploadDataHeaders] = useState<string[]>([]);
|
||||
const [uploadData, setUploadData] = useState<UploadDataType[]>([]);
|
||||
const [invalidFileReason, setInvalidFileReason] = useState('');
|
||||
|
||||
const handleFileSelect = (fileList: FileList | null) => {
|
||||
setInvalidFileReason('');
|
||||
setUploadData([]);
|
||||
if (!fileList) {
|
||||
return;
|
||||
};
|
||||
const files = Array.from(fileList);
|
||||
const file = files[0];
|
||||
setUploadFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
const content = e.target?.result as string;
|
||||
if (file.type === 'application/json') {
|
||||
try {
|
||||
const jsonDataContent = JSON.parse(content);
|
||||
if (Array.isArray(jsonDataContent)) {
|
||||
genPreviewTableData(jsonDataContent);
|
||||
} else {
|
||||
setInvalidFileReason('Invalid JSON file uploaded, JSON file must be array of key-value pairs.');
|
||||
}
|
||||
} catch (error) {
|
||||
setInvalidFileReason('Upload JSON file can not be parsed');
|
||||
}
|
||||
} else if (file.type === 'text/csv') {
|
||||
const csvRows = content.split('\n').map(row => row.split(','));
|
||||
// at least 2 rows required for csv
|
||||
if (csvRows.length > 1) {
|
||||
const csvHeaders = csvRows[0];
|
||||
const csvContentRows = csvRows.slice(1, csvRows.length);
|
||||
const uploadData = csvContentRows.map(contentRow => csvHeaders.reduce((acc: UploadDataType, cur, idx) => {
|
||||
acc[cur] = contentRow[idx] ?? '';
|
||||
return acc;
|
||||
}, {}));
|
||||
setUploadDataHeaders(csvHeaders);
|
||||
setUploadData(uploadData);
|
||||
} else {
|
||||
setInvalidFileReason('CSV file must contain at least two rows with first row as variable names');
|
||||
}
|
||||
} else {
|
||||
setInvalidFileReason(`Uploaded file is unsupported ${file.type}`);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
setInvalidFileReason(`Failed to read file ${reader.error?.message}`);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const genPreviewTableData = (uploadData: UploadDataType[]) => {
|
||||
// generate header and body data for preview table from upload data
|
||||
let dataHeaders: Set<string> = new Set();
|
||||
const filteredUploadData: UploadDataType[] = [];
|
||||
uploadData.forEach(data => {
|
||||
// filter none object value in json array
|
||||
if (data && typeof data === 'object' && !Array.isArray(data) && data !== null) {
|
||||
filteredUploadData.push(data);
|
||||
// add unique json data keys into jsonDataHeader
|
||||
dataHeaders = new Set([...dataHeaders, ...Object.keys(data)]);
|
||||
}
|
||||
});
|
||||
setUploadDataHeaders(Array.from(dataHeaders));
|
||||
setUploadData(filteredUploadData);
|
||||
};
|
||||
|
||||
const handleUploadData = () => {
|
||||
if (file && uploadData.length >= 1) {
|
||||
onUploadFile(file, uploadData);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClearData = () => {
|
||||
onUploadFile(null, []);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userUploadData.length > 0) {
|
||||
genPreviewTableData(userUploadData);
|
||||
}
|
||||
}, [userUploadData]);
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
isDismissable
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-start justify-center bg-black/30"
|
||||
>
|
||||
<Modal
|
||||
className="max-h-[75%] overflow-auto flex flex-col w-full max-w-3xl rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font] m-24"
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
>
|
||||
<Dialog
|
||||
className="outline-none flex-1 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex-1 flex flex-col gap-4 overflow-hidden'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading slot="title" className='text-2xl'>{userUploadData.length > 0 ? 'Update Data' : 'Preview Data'}</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='rounded grow shrink-0 w-full overflow-hidden basis-12 flex flex-col gap-6 select-none overflow-y-auto'>
|
||||
<FileTrigger
|
||||
allowsMultiple={false}
|
||||
onSelect={handleFileSelect}
|
||||
acceptedFileTypes={['.csv', '.json']}
|
||||
>
|
||||
<Button className="flex flex-1 flex-shrink-0 border-solid border border-[--hl-`sm] py-1 gap-2 items-center justify-center px-2 aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent transition-all text-base">
|
||||
<Icon icon="upload" />
|
||||
<span>{uploadData.length > 0 ? 'Change Data File' : 'Select Data File'}</span>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
</div>
|
||||
{invalidFileReason !== '' &&
|
||||
<div className="notice error margin-top-sm">
|
||||
<p>{invalidFileReason}</p>
|
||||
</div>
|
||||
}
|
||||
{uploadData.length > 1 &&
|
||||
<div className='overflow-auto py-2 flex-1'>
|
||||
<Heading className='text-xl margin-bottom-sm'>Data Preview</Heading>
|
||||
<Table
|
||||
aria-label='Data Preview Table'
|
||||
className="min-w-full table-auto"
|
||||
>
|
||||
<TableHeader>
|
||||
<Column
|
||||
isRowHeader
|
||||
className={rowHeaderStyle}
|
||||
>
|
||||
Iteration
|
||||
</Column>
|
||||
{uploadDataHeaders.map((header, idx) => (
|
||||
<Column
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${header}-${idx}`}
|
||||
className={rowHeaderStyle}
|
||||
>
|
||||
{header}
|
||||
</Column>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{uploadData.map((rowData, idx) => {
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Row key={idx}>
|
||||
<Cell
|
||||
className={rowCellStyle}
|
||||
>
|
||||
<span className='p-2'>{idx + 1}</span>
|
||||
</Cell>
|
||||
{uploadDataHeaders.map(rowKey => (
|
||||
<Cell
|
||||
className="whitespace-nowrap text-sm font-medium border-b border-solid border-[--hl-sm] group-last-of-type:border-none focus:outline-none"
|
||||
key={rowKey}
|
||||
>
|
||||
<span className='p-2'>
|
||||
{typeof rowData[rowKey] === 'string' ? rowData[rowKey] : JSON.stringify(rowData[rowKey])}
|
||||
</span>
|
||||
</Cell>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
<div className="flex justify-end mt-2">
|
||||
{userUploadData.length > 0 &&
|
||||
<Button
|
||||
className="hover:no-underline flex items-center gap-2 hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
|
||||
onPress={handleClearData}
|
||||
>
|
||||
Remove Data
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
isDisabled={uploadData.length < 1}
|
||||
className="hover:no-underline ml-4 flex items-center gap-2 bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
|
||||
onPress={handleUploadData}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import crypto from 'crypto';
|
||||
import type { RequestTestResult } from 'insomnia-sdk';
|
||||
import React, { type FC, useState } from 'react';
|
||||
import { Toolbar } from 'react-aria-components';
|
||||
|
||||
import { fuzzyMatch } from '../../../common/misc';
|
||||
|
||||
type TargetTestType = 'all' | 'passed' | 'failed' | 'skipped';
|
||||
|
||||
const filterClassnames = 'mx-1 w-[6rem] text-center rounded-md h-[--line-height-xxs] text-sm cursor-pointer outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
|
||||
const activeFilterClassnames = 'text-white mx-1 w-[6rem] text-center rounded-md h-[--line-height-xxs] text-sm cursor-pointer outline-none select-none px-2 py-1 bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
|
||||
|
||||
export interface RequestTestResultRowsProps {
|
||||
requestTestResults: RequestTestResult[];
|
||||
resultFilter: string;
|
||||
targetTests: string;
|
||||
}
|
||||
|
||||
export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
|
||||
requestTestResults,
|
||||
resultFilter,
|
||||
targetTests,
|
||||
}: RequestTestResultRowsProps) => {
|
||||
const testResultRows = requestTestResults
|
||||
.filter(result => {
|
||||
switch (targetTests) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'passed':
|
||||
return result.status === 'passed';
|
||||
case 'failed':
|
||||
return result.status === 'failed';
|
||||
case 'skipped':
|
||||
return result.status === 'skipped';
|
||||
default:
|
||||
throw Error(`unexpected target test type ${targetTests}`);
|
||||
}
|
||||
})
|
||||
.filter(result => {
|
||||
if (resultFilter.trim() === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(fuzzyMatch(
|
||||
resultFilter,
|
||||
result.testCase,
|
||||
{ splitSpace: false, loose: true }
|
||||
)?.indexes);
|
||||
})
|
||||
.map((result, i: number) => {
|
||||
const key = crypto
|
||||
.createHash('sha1')
|
||||
.update(`${result.testCase}"-${i}`)
|
||||
.digest('hex');
|
||||
|
||||
const statusText = {
|
||||
passed: 'PASS',
|
||||
failed: 'FAIL',
|
||||
skipped: 'SKIP',
|
||||
}[result.status];
|
||||
const statusTagColor = {
|
||||
passed: 'bg-lime-600',
|
||||
failed: 'bg-red-600',
|
||||
skipped: 'bg-slate-600',
|
||||
}[result.status];
|
||||
|
||||
const executionTime = <span className={result.executionTime < 300 ? 'text-white-500' : 'text-red-500'} >
|
||||
{result.executionTime === 0 ? '< 0.1' : `${result.executionTime.toFixed(1)}`}
|
||||
</span>;
|
||||
const statusTag = <div className={`text-xs rounded p-[2px] inline-block w-16 text-center font-semibold ${statusTagColor}`}>
|
||||
{statusText}
|
||||
</div >;
|
||||
const message = <>
|
||||
<span className='capitalize'>{result.testCase}</span>
|
||||
<span className='text-neutral-400'>{result.errorMessage ? ' | ' + result.errorMessage : ''}</span>
|
||||
</>;
|
||||
const testCategory = result.category === 'pre-request' ? 'Pre-request Test' :
|
||||
result.category === 'after-response' ? 'After-response Test' : 'Unknown';
|
||||
|
||||
return (
|
||||
<div key={key} data-testid="test-result-row">
|
||||
<div className="flex w-full my-3 text-base">
|
||||
<div className="leading-4 m-auto mx-1">
|
||||
<span className="mr-2 ml-2" >{statusTag}</span>
|
||||
</div>
|
||||
<div className="leading-4 mr-2">
|
||||
<div className='mr-2 my-1 w-auto text-nowrap'>{message}</div>
|
||||
<div className='text-sm text-neutral-400 my-1'>{`${testCategory} (`}{executionTime}{' ms)'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
});
|
||||
|
||||
return <>{testResultRows}</>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
requestTestResults: RequestTestResult[];
|
||||
}
|
||||
|
||||
export const RequestTestResultPane: FC<Props> = ({
|
||||
requestTestResults,
|
||||
}) => {
|
||||
const [targetTests, setTargetTests] = useState<TargetTestType>('all');
|
||||
const [resultFilter, setResultFilter] = useState('');
|
||||
|
||||
const noTestFoundPage = (
|
||||
<div className="text-center mt-5">
|
||||
<div className="">No test result found</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Add test cases in scripts and run them to see results.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (requestTestResults.length === 0) {
|
||||
return noTestFoundPage;
|
||||
}
|
||||
|
||||
const selectAllTests = () => setTargetTests('all');
|
||||
const selectPassedTests = () => setTargetTests('passed');
|
||||
const selectFailedTests = () => setTargetTests('failed');
|
||||
const selectSkippedTests = () => setTargetTests('skipped');
|
||||
|
||||
return <>
|
||||
<div className='test-result-pane h-full flex flex-col divide-y divide-solid divide-[--hl-md]'>
|
||||
<div className='h-[calc(100%-var(--line-height-sm))]'>
|
||||
<Toolbar className="flex items-center h-[--line-height-sm] flex-row text-[var(--font-size-sm)] box-border overflow-x-auto border-solid border-b border-b-[--hl-md] pl-2">
|
||||
<button className={targetTests === 'all' ? activeFilterClassnames : filterClassnames} onClick={selectAllTests} >All</button>
|
||||
<button className={targetTests === 'passed' ? activeFilterClassnames : filterClassnames} onClick={selectPassedTests} >Passed</button>
|
||||
<button className={targetTests === 'failed' ? activeFilterClassnames : filterClassnames} onClick={selectFailedTests} >Failed</button>
|
||||
<button className={targetTests === 'skipped' ? activeFilterClassnames : filterClassnames} onClick={selectSkippedTests} >Skipped</button>
|
||||
</Toolbar>
|
||||
<div className="overflow-y-auto w-auto overflow-x-auto h-[calc(100%-var(--line-height-sm))]">
|
||||
<RequestTestResultRows
|
||||
requestTestResults={requestTestResults}
|
||||
resultFilter={resultFilter}
|
||||
targetTests={targetTests}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Toolbar className="flex items-center h-[--line-height-sm] flex-shrink-0 flex-row text-[var(--font-size-sm)] box-border overflow-x-auto">
|
||||
<input
|
||||
key="test-results-filter"
|
||||
type="text"
|
||||
className='flex-1 pl-3'
|
||||
title="Filter test results"
|
||||
defaultValue={resultFilter || ''}
|
||||
placeholder='Filter test results with name'
|
||||
onChange={e => {
|
||||
setResultFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import React, { type FC, useCallback } from 'react';
|
||||
import React, { type FC, useCallback, useMemo } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-aria-components';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ResponseViewer } from '../viewers/response-viewer';
|
||||
import { BlankPane } from './blank-pane';
|
||||
import { Pane, PaneHeader } from './pane';
|
||||
import { PlaceholderResponsePane } from './placeholder-response-pane';
|
||||
import { RequestTestResultPane } from './request-test-result-pane';
|
||||
|
||||
interface Props {
|
||||
activeRequestId: string;
|
||||
@@ -124,6 +125,21 @@ export const ResponsePane: FC<Props> = ({
|
||||
}
|
||||
}, [activeRequest, activeResponse]);
|
||||
|
||||
const { passedTestCount, totalTestCount } = useMemo(() => {
|
||||
let passedTestCount = 0;
|
||||
let totalTestCount = 0;
|
||||
activeResponse?.requestTestResults.forEach(result => {
|
||||
if (result.status === 'passed') {
|
||||
passedTestCount++;
|
||||
}
|
||||
totalTestCount++;
|
||||
});
|
||||
return { passedTestCount, totalTestCount };
|
||||
}, [activeResponse]);
|
||||
const testResultCountTagColor = totalTestCount > 0 ?
|
||||
passedTestCount === totalTestCount ? 'bg-lime-600' : 'bg-red-600' :
|
||||
'bg-[var(--hl-sm)]';
|
||||
|
||||
if (!activeRequest) {
|
||||
return <BlankPane type="response" />;
|
||||
}
|
||||
@@ -143,6 +159,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
|
||||
const timeline = models.response.getTimeline(activeResponse);
|
||||
const cookieHeaders = getSetCookieHeaders(activeResponse.headers);
|
||||
|
||||
return (
|
||||
<Pane type="response">
|
||||
{!activeResponse ? null : (
|
||||
@@ -183,6 +200,22 @@ export const ResponsePane: FC<Props> = ({
|
||||
<span className="p-2 aspect-square flex items-center justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{cookieHeaders.length}</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='test-results'
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
Tests
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-sm ml-1 px-1 ${testResultCountTagColor}`}
|
||||
style={{ color: 'text-[--hl]' }}
|
||||
>
|
||||
{`${passedTestCount} / ${totalTestCount}`}
|
||||
</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='mock-response'
|
||||
@@ -235,14 +268,18 @@ export const ResponsePane: FC<Props> = ({
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel
|
||||
className='w-full flex-1 flex flex-col overflow-y-auto'
|
||||
id='test-results'
|
||||
>
|
||||
<RequestTestResultPane requestTestResults={activeResponse.requestTestResults} />
|
||||
</TabPanel>
|
||||
<TabPanel
|
||||
className='w-full flex-1 flex flex-col overflow-y-auto'
|
||||
id='mock-response'
|
||||
>
|
||||
<MockResponseExtractor />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-y-auto' id='timeline'>
|
||||
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
|
||||
<ResponseTimelineViewer
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { format } from 'date-fns';
|
||||
import React, { type FC } from 'react';
|
||||
import { Cell, Column, ColumnResizer, ResizableTableContainer, Row, Table, TableBody, TableHeader, TooltipTrigger } from 'react-aria-components';
|
||||
|
||||
import type { RunnerTestResult } from '../../..//models/runner-test-result';
|
||||
import { getTimeAndUnit } from '../tags/time-tag';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
history: RunnerTestResult[];
|
||||
gotoExecutionResult: (exectionId: string) => Promise<void>;
|
||||
gotoTestResultsTab: () => void;
|
||||
}
|
||||
|
||||
export const RunnerResultHistoryPane: FC<Props> = ({
|
||||
history,
|
||||
gotoExecutionResult,
|
||||
gotoTestResultsTab,
|
||||
}) => {
|
||||
const rows = history.map((runnerResult: RunnerTestResult) => {
|
||||
let passedCount = 0;
|
||||
let failedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (let i = 0; i < runnerResult.iterationResults.length; i++) { // iterations
|
||||
for (let j = 0; j < runnerResult.iterationResults[i].length; j++) { // requests
|
||||
for (let k = 0; k < runnerResult.iterationResults[i][j].results.length; k++) { // test cases
|
||||
const result = runnerResult.iterationResults[i][j].results[k];
|
||||
|
||||
if (result.status === 'failed') {
|
||||
failedCount++;
|
||||
}
|
||||
if (result.status === 'skipped') {
|
||||
skippedCount++;
|
||||
}
|
||||
if (result.status === 'passed') {
|
||||
passedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const startedAt = new Date(runnerResult.created).toString(); // TODO: should be endedat
|
||||
const { number: durationNumber, unit: durationUnit } = getTimeAndUnit(runnerResult.duration);
|
||||
const createdAt = format(runnerResult.created, 'yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
return (
|
||||
<Row
|
||||
key={runnerResult._id}
|
||||
className="cursor-pointer leading-9 hover:bg-neutral-900"
|
||||
style={{ 'outline': 'none' }}
|
||||
onAction={() => {
|
||||
gotoExecutionResult(runnerResult._id);
|
||||
gotoTestResultsTab();
|
||||
}}
|
||||
>
|
||||
<Cell className="capitalize hover:underline">
|
||||
{failedCount === 0 ?
|
||||
<i className="fa fa-circle-check fa-1x mr-2 text-green-500" /> :
|
||||
<i className="fa fa-circle-xmark fa-1x mr-2 text-red-500" />}
|
||||
<TooltipTrigger key={`parent-folder-${runnerResult.created}`} >
|
||||
<Tooltip message={createdAt}>
|
||||
{runnerResult.source}
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Cell>
|
||||
<Cell>{runnerResult.iterations}</Cell>
|
||||
<Cell>{`${durationNumber} ${durationUnit}`}</Cell>
|
||||
{/* <Cell>{`${runnerResult.avgRespTime} ms`}</Cell> */}
|
||||
<Cell>{passedCount + failedCount + skippedCount}</Cell>
|
||||
<Cell>{passedCount}</Cell>
|
||||
<Cell>{failedCount}</Cell>
|
||||
<Cell>{skippedCount}</Cell>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
||||
return <>
|
||||
<div className='h-full flex flex-col divide-y divide-solid divide-[--hl-md] overflow-y-auto mb-12'>
|
||||
<ResizableTableContainer className='mt-3'>
|
||||
<Table aria-label="Results" selectionMode="multiple" className="w-full text-center">
|
||||
<TableHeader>
|
||||
{/* <Column className="leading-9" isRowHeader>Start Time</Column> */}
|
||||
<Column className="leading-9" isRowHeader>Source<ColumnResizer /></Column>
|
||||
<Column className="leading-9">Iterations<ColumnResizer /></Column>
|
||||
<Column className="leading-9">Duration</Column>
|
||||
{/* <Column className="leading-9">Avg. Resp. Time</Column> */}
|
||||
<Column className="leading-9">Total</Column>
|
||||
<Column className="leading-9">Passed</Column>
|
||||
<Column className="leading-9">Failed</Column>
|
||||
<Column className="leading-9">Skipped</Column>
|
||||
</TableHeader>
|
||||
<TableBody >
|
||||
{rows}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ResizableTableContainer>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { type FC, useState } from 'react';
|
||||
import { Toolbar } from 'react-aria-components';
|
||||
|
||||
import type { BaseRunnerTestResult, RunnerResultPerRequest } from '../../../models/runner-test-result';
|
||||
import { RequestTestResultRows } from './request-test-result-pane';
|
||||
|
||||
type TargetTestType = 'all' | 'passed' | 'failed' | 'skipped';
|
||||
|
||||
const filterClassnames = 'mx-1 w-[6rem] text-center rounded-md h-[--line-height-xxs] text-sm cursor-pointer outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
|
||||
const activeFilterClassnames = 'text-white mx-1 w-[6rem] text-center rounded-md h-[--line-height-xxs] text-sm cursor-pointer outline-none select-none px-2 py-1 bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
|
||||
|
||||
interface Props {
|
||||
result: BaseRunnerTestResult | null;
|
||||
}
|
||||
|
||||
export const RunnerTestResultPane: FC<Props> = ({
|
||||
result,
|
||||
}) => {
|
||||
const [targetTests, setTargetTests] = useState<TargetTestType>('all');
|
||||
const [resultFilter, setResultFilter] = useState('');
|
||||
|
||||
const noTestFoundPage = (
|
||||
<div className="text-center mt-5">
|
||||
<div className="">No test result found</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Add test cases in scripts and run them to see results.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (!result || result.iterationResults.length === 0) {
|
||||
return noTestFoundPage;
|
||||
}
|
||||
|
||||
const selectAllTests = () => setTargetTests('all');
|
||||
const selectPassedTests = () => setTargetTests('passed');
|
||||
const selectFailedTests = () => setTargetTests('failed');
|
||||
const selectSkippedTests = () => setTargetTests('skipped');
|
||||
|
||||
const resultsByIteration = result.iterationResults.map((iterationResults: RunnerResultPerRequest[], i: number) => {
|
||||
const key = `runner-test-result-iteration-${i + 1}`;
|
||||
|
||||
if (Array.isArray(iterationResults)) {
|
||||
const resultByRequest = iterationResults.map((requestTestResult: RunnerResultPerRequest, i: number) => {
|
||||
const key = `request-test-result-${i}`;
|
||||
return <div key={key}>
|
||||
<div className="pl-3">
|
||||
<span>
|
||||
{requestTestResult.requestName}
|
||||
</span>
|
||||
<span className="text-sm text-neutral-400">
|
||||
{` - ${requestTestResult.requestUrl}`}
|
||||
</span>
|
||||
</div>
|
||||
<RequestTestResultRows
|
||||
requestTestResults={requestTestResult.results}
|
||||
resultFilter={resultFilter}
|
||||
targetTests={targetTests}
|
||||
/>
|
||||
</div>;
|
||||
});
|
||||
|
||||
return <div key={key} className="pt-6 pb-6 border-dashed border-b border-b-[--hl-md]">
|
||||
<div className="uppercase font-bold pl-3 mb-3 leading-10"> Iterations {i + 1} </div>
|
||||
<div className='border-solid border-1 border-gray-600' />
|
||||
{resultByRequest}
|
||||
</div>;
|
||||
} else {
|
||||
return <div key={key}>Invalid test result format</div>;
|
||||
}
|
||||
});
|
||||
|
||||
return <>
|
||||
<div className='h-full flex flex-col divide-y divide-solid divide-[--hl-md]'>
|
||||
<div className='h-[calc(100%-var(--line-height-sm))]'>
|
||||
<Toolbar className="flex items-center h-[--line-height-sm] flex-row text-[var(--font-size-sm)] box-border overflow-x-auto border-solid border-b border-b-[--hl-md] pl-2">
|
||||
<button className={targetTests === 'all' ? activeFilterClassnames : filterClassnames} onClick={selectAllTests} >All</button>
|
||||
<button className={targetTests === 'passed' ? activeFilterClassnames : filterClassnames} onClick={selectPassedTests} >Passed</button>
|
||||
<button className={targetTests === 'failed' ? activeFilterClassnames : filterClassnames} onClick={selectFailedTests} >Failed</button>
|
||||
<button className={targetTests === 'skipped' ? activeFilterClassnames : filterClassnames} onClick={selectSkippedTests} >Skipped</button>
|
||||
</Toolbar>
|
||||
<div className="overflow-y-auto w-auto overflow-x-auto h-[calc(100%-var(--line-height-sm))]">
|
||||
{resultsByIteration}
|
||||
</div>
|
||||
</div>
|
||||
<Toolbar className="flex items-center h-[--line-height-sm] flex-shrink-0 flex-row text-[var(--font-size-sm)] box-border overflow-x-auto">
|
||||
<input
|
||||
key="test-results-filter"
|
||||
type="text"
|
||||
className='flex-1 pl-3'
|
||||
title="Filter test results"
|
||||
defaultValue={resultFilter || ''}
|
||||
placeholder='Filter test results with name'
|
||||
onChange={e => {
|
||||
setResultFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
@@ -8,11 +8,11 @@ interface Props {
|
||||
steps: TimingStep[];
|
||||
}
|
||||
// triggers a 100 ms render in order to show a incrementing counter
|
||||
const MillisecondTimer = () => {
|
||||
const MillisecondTimer = ({ startedAt }: { startedAt: number }) => {
|
||||
const [milliseconds, setMilliseconds] = useState(0);
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
const loadStartTime = Date.now();
|
||||
const loadStartTime = startedAt || Date.now();
|
||||
interval = setInterval(() => {
|
||||
const delta = Date.now() - loadStartTime;
|
||||
setMilliseconds(delta);
|
||||
@@ -23,43 +23,48 @@ const MillisecondTimer = () => {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [startedAt]);
|
||||
const ms = (milliseconds / 1000);
|
||||
return ms > 0 ? `${ms.toFixed(1)} s` : '0 s';
|
||||
};
|
||||
|
||||
export const ResponseTimer: FunctionComponent<Props> = ({ handleCancel, activeRequestId, steps }) => {
|
||||
return (
|
||||
<div className="overlay theme--transparent-overlay-darker">
|
||||
<div className="timer-list w-full">
|
||||
{steps.map((record: TimingStep) => (
|
||||
<div
|
||||
key={`${activeRequestId}-${record.stepName}`}
|
||||
className='flex w-full leading-8'
|
||||
>
|
||||
<div className='w-3/4 text-left content-center leading-8'>
|
||||
<span className="leading-8">
|
||||
{
|
||||
record.duration ?
|
||||
(<i className="fa fa-circle-check fa-2x mr-2 text-green-500" />) :
|
||||
(<i className="fa fa-spinner fa-spin fa-2x mr-2" />)
|
||||
}
|
||||
</span>
|
||||
<span className="inline-block align-top">
|
||||
{record.stepName}
|
||||
</span>
|
||||
<div className="flex overlay theme--transparent-overlay-darker w-full h-full">
|
||||
<div className="m-auto w-[60%] min-w-[400px]">
|
||||
<div className="timer-list mx-auto">
|
||||
{steps.map((record: TimingStep) => (
|
||||
<div
|
||||
key={`${activeRequestId}-${record.stepName}`}
|
||||
className='flex w-full leading-8'
|
||||
>
|
||||
<div className='w-3/4 ml-1 text-left text-md content-center leading-8'>
|
||||
<span className="leading-8 w-1/5">
|
||||
{
|
||||
record.duration ?
|
||||
(<i className="fa fa-circle-check fa-1x mr-2 text-green-500" />) :
|
||||
(<i className="fa fa-spinner fa-spin fa-1x mr-2" />)
|
||||
}
|
||||
</span>
|
||||
<span className="inline-block align-top text-clip w-4/5">
|
||||
{record.stepName}
|
||||
</span>
|
||||
</div>
|
||||
<div className='w-1/4 mr-1 text-right leading-8'>
|
||||
{record.duration ? `${((record.duration) / 1000).toFixed(1)} s` : (<MillisecondTimer startedAt={record.startedAt} />)}
|
||||
</div>
|
||||
</div>
|
||||
{record.duration ? `${((record.duration) / 1000).toFixed(1)} s` : (<MillisecondTimer />)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pad">
|
||||
<button
|
||||
className="border border-solid border-[--hl-lg] px-[--padding-md] h-[--line-height-xs] rounded-[--radius-md] hover:bg-[--hl-xs]"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel Request
|
||||
</button>
|
||||
<div className="pad text-center">
|
||||
<button
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
tooltipDelay?: number;
|
||||
steps?: TimingStep[];
|
||||
}
|
||||
const getTimeAndUnit = (milliseconds: number) => {
|
||||
export const getTimeAndUnit = (milliseconds: number) => {
|
||||
let unit = 'ms';
|
||||
let number = milliseconds;
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const Debug = lazy(() => import('./routes/debug'));
|
||||
const Design = lazy(() => import('./routes/design'));
|
||||
const MockServer = lazy(() => import('./routes/mock-server'));
|
||||
const Environments = lazy(() => import('./routes/environments'));
|
||||
const Runner = lazy(() => import('./routes/runner'));
|
||||
|
||||
initializeSentry();
|
||||
initializeLogging();
|
||||
@@ -457,6 +458,30 @@ async function renderApp() {
|
||||
await import('./routes/request-group')
|
||||
).updateRequestGroupMetaAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'runner',
|
||||
loader: async (...args) =>
|
||||
(
|
||||
await import('./routes/runner')
|
||||
).collectionRunnerStatusLoader(...args),
|
||||
element:
|
||||
<Suspense fallback={<AppLoadingIndicator />}>
|
||||
<Runner />
|
||||
</Suspense>,
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/runner')
|
||||
).runCollectionAction(...args),
|
||||
children: [
|
||||
{
|
||||
path: 'run/',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/runner')
|
||||
).runCollectionAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type LoaderFunction,
|
||||
type NavigateFunction,
|
||||
NavLink,
|
||||
Outlet,
|
||||
redirect,
|
||||
useFetcher,
|
||||
useFetchers,
|
||||
@@ -137,7 +138,7 @@ const INITIAL_GRPC_REQUEST_STATE = {
|
||||
error: undefined,
|
||||
methods: [],
|
||||
};
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
export const loader: LoaderFunction = async ({ params, request }) => {
|
||||
if (!params.requestId && !params.requestGroupId) {
|
||||
const { projectId, workspaceId, organizationId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
@@ -149,7 +150,11 @@ export const loader: LoaderFunction = async ({ params }) => {
|
||||
invariant(activeWorkspaceMeta, 'Workspace meta not found');
|
||||
const activeRequestId = activeWorkspaceMeta.activeRequestId;
|
||||
const activeRequest = activeRequestId ? await models.request.getById(activeRequestId) : null;
|
||||
if (activeRequest) {
|
||||
// TODO(george): we should remove this after enabling the sidebar for the runner
|
||||
const startOfQuery = request.url.indexOf('?');
|
||||
const urlWithoutQuery = startOfQuery > 0 ? request.url.slice(0, startOfQuery) : request.url;
|
||||
const isDisplayingRunner = urlWithoutQuery.endsWith('/runner');
|
||||
if (activeRequest && !isDisplayingRunner) {
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequestId}`);
|
||||
}
|
||||
}
|
||||
@@ -483,8 +488,8 @@ export const Debug: FC = () => {
|
||||
// If the target is a folder and we insert after it we want to add that item to the folder
|
||||
const isMovingItemInsideFolder = isRequestGroup(targetItem.doc) && event.target.dropPosition === 'after';
|
||||
if (isMovingItemInsideFolder) {
|
||||
// there is no item before we move the item to the beginning
|
||||
// If there are children find the first child key and use a lower one
|
||||
// there is no item before we move the item to the beginning
|
||||
// If there are children find the first child key and use a lower one
|
||||
// otherwise use whatever
|
||||
const children = collection.filter(r => r.doc.parentId === targetId);
|
||||
|
||||
@@ -723,6 +728,10 @@ export const Debug: FC = () => {
|
||||
}
|
||||
}, [settings.forceVerticalLayout, direction]);
|
||||
|
||||
const onRunCollection = () => {
|
||||
navigate(`/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/${activeWorkspace._id}/debug/runner`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isScratchpad(activeWorkspace)) {
|
||||
window.main.landingPageRendered(LandingPage.Scratchpad);
|
||||
@@ -735,21 +744,26 @@ export const Debug: FC = () => {
|
||||
<Panel id="sidebar" className='sidebar theme--sidebar' maxSize={40} minSize={10} collapsible>
|
||||
<div className="flex flex-1 flex-col overflow-hidden divide-solid divide-y divide-[--hl-md]">
|
||||
<div className="flex flex-col items-start">
|
||||
<Breadcrumbs className='flex h-[--line-height-sm] list-none items-center m-0 gap-2 border-solid border-[--hl-md] border-b p-[--padding-sm] font-bold w-full'>
|
||||
<Breadcrumb className="flex select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<NavLink
|
||||
data-testid="project"
|
||||
className="px-1 py-1 aspect-square h-7 flex flex-shrink-0 outline-none data-[focused]:outline-none items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
to={`/organization/${organizationId}/project/${activeProject._id}`}
|
||||
>
|
||||
<Icon className='text-xs' icon="chevron-left" />
|
||||
</NavLink>
|
||||
<span aria-hidden role="separator" className='text-[--hl-lg] h-4 outline outline-1' />
|
||||
</Breadcrumb>
|
||||
<Breadcrumb className="flex truncate select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<WorkspaceDropdown />
|
||||
</Breadcrumb>
|
||||
</Breadcrumbs>
|
||||
<div className='flex w-full'>
|
||||
<Breadcrumbs className='flex h-[--line-height-sm] list-none items-center m-0 gap-2 border-solid border-[--hl-md] border-b p-[--padding-sm] font-bold w-full'>
|
||||
<Breadcrumb className="flex select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<NavLink
|
||||
data-testid="project"
|
||||
className="px-1 py-1 aspect-square h-7 flex flex-shrink-0 outline-none data-[focused]:outline-none items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
to={`/organization/${organizationId}/project/${activeProject._id}`}
|
||||
>
|
||||
<Icon className='text-xs' icon="chevron-left" />
|
||||
</NavLink>
|
||||
<span aria-hidden role="separator" className='text-[--hl-lg] h-4 outline outline-1' />
|
||||
</Breadcrumb>
|
||||
<Breadcrumb className="flex truncate select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<WorkspaceDropdown />
|
||||
</Breadcrumb>
|
||||
<Breadcrumb className="flex text-sm truncate select-none items-center justify-self-end ml-auto mr-2.5 gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<Icon icon='circle-play' onClick={onRunCollection} className="cursor-pointer" data-testid="run-collection-btn-quick" />
|
||||
</Breadcrumb>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
<div className='flex flex-col items-start gap-2 p-[--padding-sm] w-full'>
|
||||
<div className="flex w-full items-center gap-2 justify-between">
|
||||
<EnvironmentPicker
|
||||
@@ -1137,25 +1151,28 @@ export const Debug: FC = () => {
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
</Panel>
|
||||
{activeRequest ? (<>
|
||||
<PanelResizeHandle className={direction === 'horizontal' ? 'h-full w-[1px] bg-[--hl-md]' : 'w-full h-[1px] bg-[--hl-md]'} />
|
||||
<Panel id="pane-two" minSize={10} className='pane-two theme--pane'>
|
||||
<ErrorBoundary showAlert>
|
||||
{activeRequest && isGrpcRequest(activeRequest) && grpcState && (
|
||||
<GrpcResponsePane grpcState={grpcState} />
|
||||
)}
|
||||
{isRealtimeRequest && (
|
||||
<RealtimeResponsePane requestId={activeRequest._id} />
|
||||
)}
|
||||
{activeRequest && isRequest(activeRequest) && !isRealtimeRequest && (
|
||||
<ResponsePane activeRequestId={activeRequest._id} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</Panel>
|
||||
</>) : null}
|
||||
{
|
||||
activeRequest ? (<>
|
||||
<PanelResizeHandle className={direction === 'horizontal' ? 'h-full w-[1px] bg-[--hl-md]' : 'w-full h-[1px] bg-[--hl-md]'} />
|
||||
<Panel id="pane-two" minSize={10} className='pane-two theme--pane'>
|
||||
<ErrorBoundary showAlert>
|
||||
{activeRequest && isGrpcRequest(activeRequest) && grpcState && (
|
||||
<GrpcResponsePane grpcState={grpcState} />
|
||||
)}
|
||||
{isRealtimeRequest && (
|
||||
<RealtimeResponsePane requestId={activeRequest._id} />
|
||||
)}
|
||||
{activeRequest && isRequest(activeRequest) && !isRealtimeRequest && (
|
||||
<ResponsePane activeRequestId={activeRequest._id} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</Panel>
|
||||
</>) : null
|
||||
}
|
||||
<Outlet />
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</PanelGroup >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createWriteStream } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import * as contentDisposition from 'content-disposition';
|
||||
import type { RequestTestResult } from 'insomnia-sdk';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import { type ActionFunction, type LoaderFunction, redirect } from 'react-router-dom';
|
||||
|
||||
@@ -11,9 +12,11 @@ import { type ChangeBufferEvent, database } from '../../common/database';
|
||||
import { getContentDispositionHeader } from '../../common/misc';
|
||||
import { type RenderedRequest } from '../../common/render';
|
||||
import type { ResponsePatch } from '../../main/network/libcurl-promise';
|
||||
import type { TimingStep } from '../../main/network/request-timing';
|
||||
import type { BaseModel } from '../../models';
|
||||
import * as models from '../../models';
|
||||
import type { CookieJar } from '../../models/cookie-jar';
|
||||
import type { UserUploadEnvironment } from '../../models/environment';
|
||||
import { type GrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
|
||||
import type { GrpcRequestMeta } from '../../models/grpc-request-meta';
|
||||
import * as requestOperations from '../../models/helpers/request-operations';
|
||||
@@ -355,7 +358,6 @@ const writeToDownloadPath = (downloadPathAndName: string, responsePatch: Respons
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
export interface SendActionParams {
|
||||
@@ -369,91 +371,25 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams;
|
||||
|
||||
try {
|
||||
window.main.startExecution({ requestId });
|
||||
const requestData = await fetchRequestData(requestId);
|
||||
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' });
|
||||
const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
if (mutatedContext === null) {
|
||||
console.error('[scripting] Failed to execute pre-request script');
|
||||
return null;
|
||||
}
|
||||
// disable after-response script here to avoiding rendering it
|
||||
// @TODO This should be handled in a better way. Maybe remove the key from the request object we pass in tryToInterpolateRequest
|
||||
const afterResponseScript = mutatedContext.request.afterResponseScript ? `${mutatedContext.request.afterResponseScript}` : undefined;
|
||||
mutatedContext.request.afterResponseScript = '';
|
||||
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Rendering request' });
|
||||
const renderedResult = await tryToInterpolateRequest(
|
||||
mutatedContext.request,
|
||||
mutatedContext.environment,
|
||||
'send',
|
||||
undefined,
|
||||
mutatedContext.baseEnvironment,
|
||||
return await sendActionImp({
|
||||
requestId,
|
||||
workspaceId,
|
||||
shouldPromptForPathAfterResponse,
|
||||
ignoreUndefinedEnvVariable,
|
||||
);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
|
||||
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
|
||||
parseGraphQLReqeustBody(renderedRequest);
|
||||
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Sending request' });
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
mutatedContext.clientCertificates,
|
||||
requestData.caCert,
|
||||
mutatedContext.settings,
|
||||
requestData.timelinePath,
|
||||
requestData.responseId
|
||||
);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
|
||||
const requestMeta = await models.requestMeta.getByParentId(requestId);
|
||||
invariant(requestMeta, 'RequestMeta not found');
|
||||
const responsePatch = await responseTransform(response, requestData.activeEnvironmentId, renderedRequest, renderedResult.context);
|
||||
const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath;
|
||||
const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath;
|
||||
|
||||
mutatedContext.request.afterResponseScript = afterResponseScript;
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Executing after-response script' });
|
||||
await tryToExecuteAfterResponseScript({
|
||||
...requestData,
|
||||
...mutatedContext,
|
||||
response,
|
||||
});
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
} catch (err) {
|
||||
console.log('[request] Failed to send request', err);
|
||||
const e = err.error || err;
|
||||
|
||||
if (!shouldWriteToFile) {
|
||||
const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
|
||||
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
|
||||
return null;
|
||||
if (err.response && err.requestMeta && err.response._id) {
|
||||
// this part is for persisting useful info (e.g. timeline) for debugging, even there is an error
|
||||
const existingResponse = await models.response.getById(err.response._id);
|
||||
const response = existingResponse || await models.response.create(err.response, err.maxHistoryResponses);
|
||||
await models.requestMeta.update(err.requestMeta, { activeResponseId: response._id });
|
||||
}
|
||||
|
||||
if (requestMeta.downloadPath) {
|
||||
const header = getContentDispositionHeader(responsePatch.headers || []);
|
||||
const name = header
|
||||
? contentDisposition.parse(header.value).parameters.filename
|
||||
: `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${responsePatch.contentType && mimeExtension(responsePatch.contentType) || 'unknown'}`;
|
||||
return writeToDownloadPath(path.join(requestMeta.downloadPath, name), responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
|
||||
} else {
|
||||
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
|
||||
const { filePath } = await window.dialog.showSaveDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Save',
|
||||
// NOTE: An error will be thrown if defaultPath is supplied but not a String
|
||||
...(defaultPath ? { defaultPath } : {}),
|
||||
});
|
||||
if (!filePath) {
|
||||
return null;
|
||||
}
|
||||
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
|
||||
return writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[request] Failed to send request', e);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.set('error', e);
|
||||
@@ -465,6 +401,201 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export type RunnerSource = 'runner';
|
||||
export interface CollectionRunnerContext {
|
||||
source: RunnerSource;
|
||||
environmentId: string;
|
||||
iterations: number;
|
||||
iterationData: object;
|
||||
duration: number; // millisecond
|
||||
testCount: number;
|
||||
avgRespTime: number; // millisecond
|
||||
results: RequestTestResult[];
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export interface RunnerContextForRequest {
|
||||
requestId: string;
|
||||
requestName: string;
|
||||
requestUrl: string;
|
||||
responseReason: string;
|
||||
duration: number; // millisecond
|
||||
size: number;
|
||||
results: RequestTestResult[];
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export const sendActionImp = async ({
|
||||
requestId,
|
||||
workspaceId,
|
||||
userUploadEnv,
|
||||
shouldPromptForPathAfterResponse,
|
||||
ignoreUndefinedEnvVariable,
|
||||
testResultCollector,
|
||||
iteration,
|
||||
iterationCount,
|
||||
}: {
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
shouldPromptForPathAfterResponse: boolean | undefined;
|
||||
ignoreUndefinedEnvVariable: boolean | undefined;
|
||||
testResultCollector?: RunnerContextForRequest;
|
||||
iteration?: number;
|
||||
iterationCount?: number;
|
||||
userUploadEnv?: UserUploadEnvironment;
|
||||
}) => {
|
||||
window.main.startExecution({ requestId });
|
||||
const requestData = await fetchRequestData(requestId);
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' });
|
||||
const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId, userUploadEnv, iteration, iterationCount);
|
||||
if ('error' in mutatedContext) {
|
||||
throw {
|
||||
error: mutatedContext.error,
|
||||
};
|
||||
}
|
||||
if (mutatedContext.execution?.skipRequest) {
|
||||
// cancel request running if skipRequest in pre-request script
|
||||
const responseId = requestData.responseId;
|
||||
const responsePatch = {
|
||||
_id: responseId,
|
||||
parentId: requestId,
|
||||
environemntId: requestData.environment,
|
||||
statusMessage: 'Cancelled',
|
||||
error: 'Request was cancelled by pre-request script',
|
||||
};
|
||||
// create and update response to activeResponse
|
||||
await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
|
||||
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: responseId });
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
return mutatedContext;
|
||||
}
|
||||
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
|
||||
// disable after-response script here to avoiding rendering it
|
||||
// @TODO This should be handled in a better way. Maybe remove the key from the request object we pass in tryToInterpolateRequest
|
||||
const afterResponseScript = mutatedContext.request.afterResponseScript ? `${mutatedContext.request.afterResponseScript}` : undefined;
|
||||
mutatedContext.request.afterResponseScript = '';
|
||||
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Rendering request' });
|
||||
const renderedResult = await tryToInterpolateRequest({
|
||||
request: mutatedContext.request,
|
||||
environment: mutatedContext.environment,
|
||||
purpose: 'send',
|
||||
extraInfo: undefined,
|
||||
baseEnvironment: mutatedContext.baseEnvironment,
|
||||
userUploadEnv: mutatedContext.userUploadEnv,
|
||||
ignoreUndefinedEnvVariable,
|
||||
});
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
|
||||
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
|
||||
parseGraphQLReqeustBody(renderedRequest);
|
||||
|
||||
const requestMeta = await models.requestMeta.getByParentId(requestId);
|
||||
invariant(requestMeta, 'RequestMeta not found');
|
||||
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Sending request' });
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
mutatedContext.clientCertificates,
|
||||
requestData.caCert,
|
||||
mutatedContext.settings,
|
||||
requestData.timelinePath,
|
||||
requestData.responseId
|
||||
);
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
if ('error' in response) {
|
||||
throw {
|
||||
response: await responseTransform(response, requestData.activeEnvironmentId, renderedRequest, renderedResult.context),
|
||||
maxHistoryResponses: requestData.settings.maxHistoryResponses,
|
||||
requestMeta,
|
||||
error: response.error,
|
||||
};
|
||||
}
|
||||
|
||||
const baseResponsePatch = await responseTransform(response, requestData.activeEnvironmentId, renderedRequest, renderedResult.context);
|
||||
const is2XXWithBodyPath = baseResponsePatch.statusCode && baseResponsePatch.statusCode >= 200 && baseResponsePatch.statusCode < 300 && baseResponsePatch.bodyPath;
|
||||
const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath;
|
||||
|
||||
mutatedContext.request.afterResponseScript = afterResponseScript;
|
||||
window.main.addExecutionStep({ requestId, stepName: 'Executing after-response script' });
|
||||
const postMutatedContext = await tryToExecuteAfterResponseScript({
|
||||
...requestData,
|
||||
...mutatedContext,
|
||||
response,
|
||||
iteration,
|
||||
iterationCount,
|
||||
});
|
||||
if ('error' in postMutatedContext) {
|
||||
throw {
|
||||
response: await responseTransform(response, requestData.activeEnvironmentId, renderedRequest, renderedResult.context),
|
||||
maxHistoryResponses: requestData.settings.maxHistoryResponses,
|
||||
requestMeta,
|
||||
error: postMutatedContext.error,
|
||||
};
|
||||
}
|
||||
|
||||
window.main.completeExecutionStep({ requestId });
|
||||
|
||||
const preTestResults = (mutatedContext.requestTestResults || []).map(
|
||||
(result: RequestTestResult): RequestTestResult => ({ ...result, category: 'pre-request' }),
|
||||
);
|
||||
const postTestResults = (postMutatedContext?.requestTestResults || []).map(
|
||||
(result: RequestTestResult): RequestTestResult => ({ ...result, category: 'after-response' }),
|
||||
) || [];
|
||||
if (testResultCollector) {
|
||||
testResultCollector.results = [
|
||||
...testResultCollector.results,
|
||||
...preTestResults,
|
||||
...postTestResults,
|
||||
];
|
||||
const timingSteps = await window.main.getExecution({ requestId });
|
||||
testResultCollector.duration = timingSteps.reduce((acc: number, cur: TimingStep) => {
|
||||
return acc + (cur.duration || 0);
|
||||
}, 0);
|
||||
testResultCollector.responseId = response._id;
|
||||
}
|
||||
const responsePatch = postMutatedContext ?
|
||||
{
|
||||
...baseResponsePatch,
|
||||
// both pre-request and after-response test results are collected
|
||||
requestTestResults: [
|
||||
...preTestResults,
|
||||
...postTestResults,
|
||||
],
|
||||
}
|
||||
: baseResponsePatch;
|
||||
|
||||
if (!shouldWriteToFile) {
|
||||
const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
|
||||
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
|
||||
return mutatedContext;
|
||||
}
|
||||
|
||||
if (requestMeta.downloadPath) {
|
||||
const header = getContentDispositionHeader(responsePatch.headers || []);
|
||||
const name = header
|
||||
? contentDisposition.parse(header.value).parameters.filename
|
||||
: `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${responsePatch.contentType && mimeExtension(responsePatch.contentType) || 'unknown'}`;
|
||||
return writeToDownloadPath(path.join(requestMeta.downloadPath, name), responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
|
||||
} else {
|
||||
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
|
||||
const { filePath } = await window.dialog.showSaveDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Save',
|
||||
// NOTE: An error will be thrown if defaultPath is supplied but not a String
|
||||
...(defaultPath ? { defaultPath } : {}),
|
||||
});
|
||||
if (!filePath) {
|
||||
return null;
|
||||
}
|
||||
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
|
||||
return writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
|
||||
}
|
||||
};
|
||||
|
||||
export const createAndSendToMockbinAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json() as Partial<Request>;
|
||||
invariant(typeof patch.url === 'string', 'URL is required');
|
||||
@@ -494,7 +625,7 @@ export const createAndSendToMockbinAction: ActionFunction = async ({ request })
|
||||
}
|
||||
);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(req, environment._id, 'send');
|
||||
const renderResult = await tryToInterpolateRequest({ request: req, environment: environment._id, purpose: 'send' });
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
window.main.completeExecutionStep({ requestId: req._id });
|
||||
|
||||
902
packages/insomnia/src/ui/routes/runner.tsx
Normal file
902
packages/insomnia/src/ui/routes/runner.tsx
Normal file
@@ -0,0 +1,902 @@
|
||||
import type { RequestContext, RequestTestResult } from 'insomnia-sdk';
|
||||
import porderedJSON from 'json-order';
|
||||
import React, { type FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { type ActionFunction, redirect, useNavigate, useParams, useRouteLoaderData, useSearchParams, useSubmit } from 'react-router-dom';
|
||||
import { useListData } from 'react-stately';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { Tooltip } from '../../../src/ui/components/tooltip';
|
||||
import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '../../common/constants';
|
||||
import type { ResponseTimelineEntry } from '../../main/network/libcurl-promise';
|
||||
import type { TimingStep } from '../../main/network/request-timing';
|
||||
import * as models from '../../models';
|
||||
import type { UserUploadEnvironment } from '../../models/environment';
|
||||
import { isRequest, type Request } from '../../models/request';
|
||||
import { isRequestGroup } from '../../models/request-group';
|
||||
import type { ResponseInfo, RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result';
|
||||
import { cancelRequestById } from '../../network/cancellation';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { HelpTooltip } from '../components/help-tooltip';
|
||||
import { Icon } from '../components/icon';
|
||||
import { showAlert } from '../components/modals';
|
||||
import { UploadDataModal, type UploadDataType } from '../components/modals/upload-runner-data-modal';
|
||||
import { Pane, PaneBody, PaneHeader } from '../components/panes/pane';
|
||||
import { RunnerResultHistoryPane } from '../components/panes/runner-result-history-pane';
|
||||
import { RunnerTestResultPane } from '../components/panes/runner-test-result-pane';
|
||||
import { ResponseTimer } from '../components/response-timer';
|
||||
import { getTimeAndUnit } from '../components/tags/time-tag';
|
||||
import { ResponseTimelineViewer } from '../components/viewers/response-timeline-viewer';
|
||||
import { type RunnerSource, sendActionImp } from './request';
|
||||
import { useRootLoaderData } from './root';
|
||||
import type { Child, WorkspaceLoaderData } from './workspace';
|
||||
|
||||
const inputStyle = 'placeholder:italic py-0.5 mr-1.5 px-1 w-24 rounded-sm border-2 border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors';
|
||||
const iterationInputStyle = 'placeholder:italic py-0.5 mr-1.5 px-1 w-16 rounded-sm border-2 border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors';
|
||||
|
||||
// TODO: improve the performance for a lot of logs
|
||||
async function aggregateAllTimelines(testResult: RunnerTestResult) {
|
||||
const responsesInfo = testResult.responsesInfo;
|
||||
let timelines = new Array<ResponseTimelineEntry>();
|
||||
|
||||
for (let i = 0; i < responsesInfo.length; i++) {
|
||||
const respInfo = responsesInfo[i];
|
||||
const resp = await models.response.getById(respInfo.responseId);
|
||||
if (resp) {
|
||||
const timeline = models.response.getTimeline(resp, true) as unknown as ResponseTimelineEntry[];
|
||||
timelines = [
|
||||
...timelines,
|
||||
{
|
||||
value: `------ Start of request (${respInfo.originalRequestName}) ------\n\n\n`,
|
||||
name: 'Text',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
...timeline,
|
||||
];
|
||||
} else {
|
||||
console.error(`failed to read response for the request ${respInfo.originalRequestName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return timelines;
|
||||
}
|
||||
|
||||
export const Runner: FC<{}> = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [shouldRefresh, setShouldRefresh] = useState(false);
|
||||
if (searchParams.has('refresh-pane')) {
|
||||
setShouldRefresh(true);
|
||||
// clean up params
|
||||
searchParams.delete('refresh-pane');
|
||||
setSearchParams({});
|
||||
}
|
||||
if (searchParams.has('error')) {
|
||||
showAlert({
|
||||
title: 'Unexpected Runner Failure',
|
||||
message: (
|
||||
<div>
|
||||
<p>The runner failed due to an unhandled error:</p>
|
||||
<code className="wide selectable">
|
||||
<pre>{searchParams.get('error')}</pre>
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
searchParams.delete('error');
|
||||
setSearchParams({});
|
||||
}
|
||||
|
||||
const { organizationId, projectId, workspaceId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
direction: 'vertical' | 'horizontal';
|
||||
};
|
||||
const { settings } = useRootLoaderData();
|
||||
const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [uploadData, setUploadData] = useState<UploadDataType[]>([]);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
|
||||
const [direction, setDirection] = useState<'horizontal' | 'vertical'>(settings.forceVerticalLayout ? 'vertical' : 'horizontal');
|
||||
useEffect(() => {
|
||||
if (settings.forceVerticalLayout) {
|
||||
setDirection('vertical');
|
||||
return () => { };
|
||||
} else {
|
||||
// Listen on media query changes
|
||||
const mediaQuery = window.matchMedia('(max-width: 880px)');
|
||||
setDirection(mediaQuery.matches ? 'vertical' : 'horizontal');
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setDirection(e.matches ? 'vertical' : 'horizontal');
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange);
|
||||
};
|
||||
}
|
||||
}, [settings.forceVerticalLayout, direction]);
|
||||
|
||||
const [iterations, setIterations] = useState(1);
|
||||
const [delay, setDelay] = useState(0);
|
||||
const getEntityById = new Map<string, Child>();
|
||||
const requestRows = collection
|
||||
.filter(item => {
|
||||
getEntityById.set(item.doc._id, item);
|
||||
return isRequest(item.doc);
|
||||
})
|
||||
.map((item: Child) => {
|
||||
const ancestorNames: string[] = [];
|
||||
if (item.ancestors) {
|
||||
item.ancestors.forEach(ancestorId => {
|
||||
const ancestor = getEntityById.get(ancestorId);
|
||||
if (ancestor && isRequestGroup(ancestor?.doc)) {
|
||||
ancestorNames.push(ancestor?.doc.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const requestDoc = item.doc as Request;
|
||||
invariant('method' in item.doc, 'Only Request is supported at the moment');
|
||||
return {
|
||||
id: item.doc._id,
|
||||
name: item.doc.name,
|
||||
ancestorNames,
|
||||
method: requestDoc.method,
|
||||
url: item.doc.url,
|
||||
};
|
||||
});
|
||||
const reqList = useListData({
|
||||
initialItems: requestRows,
|
||||
});
|
||||
const allKeys = reqList.items.map(item => item.id);
|
||||
|
||||
const { dragAndDropHooks: requestsDnD } = useDragAndDrop({
|
||||
getItems: keys => {
|
||||
return [...keys].map(key => {
|
||||
const name = getEntityById.get(key as string)?.doc.name || '';
|
||||
return {
|
||||
'text/plain': key.toString(),
|
||||
name,
|
||||
};
|
||||
});
|
||||
},
|
||||
onReorder: event => {
|
||||
if (event.target.dropPosition === 'before') {
|
||||
reqList.moveBefore(event.target.key, event.keys);
|
||||
} else if (event.target.dropPosition === 'after') {
|
||||
reqList.moveAfter(event.target.key, event.keys);
|
||||
}
|
||||
},
|
||||
renderDragPreview(items) {
|
||||
return (
|
||||
<div className="bg-slate-800 px-2 py-0.5 rounded" >
|
||||
<mark className="text-lg px-2 text-extrabold bg-green-400 rounded dark:bg-green-400" style={{ color: 'black' }}>{` ${items.length}`}</mark> item(s)
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderDropIndicator(target) {
|
||||
if (target.type === 'item') {
|
||||
const item = reqList.items.find(item => item.id === target.key);
|
||||
if (item) {
|
||||
return (
|
||||
<DropIndicator
|
||||
target={target}
|
||||
className={({ isDropTarget }) => {
|
||||
return `${isDropTarget ? 'border border-solid border-[--hl-sm]' : ''}`;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <DropIndicator target={target} />;
|
||||
},
|
||||
});
|
||||
|
||||
const submit = useSubmit();
|
||||
const onRun = () => {
|
||||
const selected = new Set(reqList.selectedKeys);
|
||||
const requests = Array.from(reqList.items)
|
||||
.filter(item => selected.has(item.id));
|
||||
// convert uploadData to environment data
|
||||
const userUploadEnvs = uploadData.map(data => {
|
||||
const orderedJson = porderedJSON.parse<UploadDataType>(
|
||||
JSON.stringify(data),
|
||||
JSON_ORDER_PREFIX,
|
||||
JSON_ORDER_SEPARATOR,
|
||||
);
|
||||
return {
|
||||
name: file!.name,
|
||||
data: orderedJson.object,
|
||||
dataPropertyOrder: orderedJson.map || null,
|
||||
};
|
||||
});
|
||||
|
||||
submit(
|
||||
{
|
||||
requests,
|
||||
iterations,
|
||||
userUploadEnvs,
|
||||
delay,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner/run/`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const goToRequest = (requestId: string) => {
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}`);
|
||||
};
|
||||
const onToggleSelection = () => {
|
||||
if (Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length) {
|
||||
// unselect all
|
||||
reqList.setSelectedKeys(new Set([]));
|
||||
} else {
|
||||
// select all
|
||||
reqList.setSelectedKeys(new Set(reqList.items.map(item => item.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const [testHistory, setTestHistory] = useState<RunnerTestResult[]>([]);
|
||||
useEffect(() => {
|
||||
const readResults = async () => {
|
||||
const results = await models.runnerTestResult.findByParentId(workspaceId) || [];
|
||||
setTestHistory(results.reverse());
|
||||
};
|
||||
readResults();
|
||||
}, [workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadData.length >= 1) {
|
||||
// update iteration number from upload data length
|
||||
setIterations(uploadData.length);
|
||||
}
|
||||
}, [uploadData]);
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [timingSteps, setTimingSteps] = useState<TimingStep[]>([]);
|
||||
const [totalTime, setTotalTime] = useState({
|
||||
duration: 0,
|
||||
unit: 'ms',
|
||||
});
|
||||
|
||||
const [executionResult, setExecutionResult] = useState<RunnerTestResult | null>(null);
|
||||
const [timelines, setTimelines] = useState<ResponseTimelineEntry[]>([]);
|
||||
const gotoExecutionResult = useCallback(async (executionId: string) => {
|
||||
const result = await models.runnerTestResult.getById(executionId);
|
||||
if (result) {
|
||||
setExecutionResult(result);
|
||||
}
|
||||
}, [setExecutionResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshTimeline = async () => {
|
||||
if (executionResult) {
|
||||
const mergedTimelines = await aggregateAllTimelines(executionResult);
|
||||
setTimelines(mergedTimelines);
|
||||
}
|
||||
};
|
||||
refreshTimeline();
|
||||
}, [executionResult]);
|
||||
|
||||
useInterval(() => {
|
||||
const refreshPanes = async () => {
|
||||
const latestTimingSteps = await window.main.getExecution({ requestId: workspaceId });
|
||||
if (latestTimingSteps) {
|
||||
// there is a timingStep item and it is not ended (duration is not assigned)
|
||||
const isRunning = latestTimingSteps.length > 0 && latestTimingSteps[latestTimingSteps.length - 1].stepName !== 'Done';
|
||||
setIsRunning(isRunning);
|
||||
|
||||
if (isRunning) {
|
||||
const duration = Date.now() - latestTimingSteps[latestTimingSteps.length - 1].startedAt;
|
||||
const { number: durationNumber, unit: durationUnit } = getTimeAndUnit(duration);
|
||||
|
||||
setTimingSteps(latestTimingSteps);
|
||||
setTotalTime({
|
||||
duration: durationNumber,
|
||||
unit: durationUnit,
|
||||
});
|
||||
} else {
|
||||
if (shouldRefresh) {
|
||||
const results = await models.runnerTestResult.findByParentId(workspaceId) || [];
|
||||
setTestHistory(results.reverse());
|
||||
if (results.length > 0) {
|
||||
const latestResult = results[0];
|
||||
setExecutionResult(latestResult);
|
||||
}
|
||||
setShouldRefresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
refreshPanes();
|
||||
}, 1000);
|
||||
|
||||
const { passedTestCount, totalTestCount, testResultCountTagColor } = useMemo(() => {
|
||||
let passedTestCount = 0;
|
||||
let totalTestCount = 0;
|
||||
|
||||
if (!isRunning) {
|
||||
if (executionResult?.iterationResults) {
|
||||
for (let i = 0; i < executionResult.iterationResults.length; i++) { // iterations
|
||||
for (let j = 0; j < executionResult.iterationResults[i].length; j++) { // requests
|
||||
for (let k = 0; k < executionResult.iterationResults[i][j].results.length; k++) { // test cases
|
||||
if (executionResult.iterationResults[i][j].results[k].status === 'passed') {
|
||||
passedTestCount++;
|
||||
}
|
||||
totalTestCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testResultCountTagColor = totalTestCount > 0 ?
|
||||
passedTestCount === totalTestCount ? 'bg-lime-600' : 'bg-red-600' :
|
||||
'bg-[var(--hl-sm)]';
|
||||
|
||||
return { passedTestCount, totalTestCount, testResultCountTagColor };
|
||||
}, [executionResult, isRunning]);
|
||||
|
||||
const [selectedTab, setSelectedTab] = React.useState<Key>('test-results');
|
||||
const gotoTestResultsTab = useCallback(() => {
|
||||
setSelectedTab('test-results');
|
||||
}, [setSelectedTab]);
|
||||
|
||||
const disabledKeys = useMemo(() => {
|
||||
return isRunning ? allKeys : [];
|
||||
}, [isRunning, allKeys]);
|
||||
|
||||
return (
|
||||
<PanelGroup autoSaveId="insomnia-sidebar" id="wrapper" className='new-sidebar w-full h-full text-[--color-font]' direction='horizontal'>
|
||||
<Panel>
|
||||
<PanelGroup autoSaveId="insomnia-panels" direction={direction}>
|
||||
|
||||
<Panel id="pane-one" className='pane-one theme--pane'>
|
||||
<ErrorBoundary showAlert>
|
||||
|
||||
<Pane type="request">
|
||||
<PaneHeader>
|
||||
<Heading className="flex items-center w-full h-[--line-height-sm] pl-[--padding-md]">
|
||||
<div className="w-full h-full text-left min-w-[400px]">
|
||||
<span className="mr-6 text-sm">
|
||||
<input
|
||||
value={iterations}
|
||||
name='Iterations'
|
||||
disabled={isRunning}
|
||||
onChange={e => {
|
||||
try {
|
||||
const iterCount = parseInt(e.target.value, 10);
|
||||
if (iterCount > 0) {
|
||||
setIterations(iterCount);
|
||||
}
|
||||
} catch (ex) {
|
||||
// no op
|
||||
}
|
||||
}}
|
||||
type='number'
|
||||
className={iterationInputStyle}
|
||||
/>
|
||||
<span className="border">Iterations</span>
|
||||
</span>
|
||||
<span className="mr-6 text-sm">
|
||||
<input
|
||||
value={delay}
|
||||
disabled={isRunning}
|
||||
name='Delay'
|
||||
onChange={e => {
|
||||
try {
|
||||
const delay = parseInt(e.target.value, 10);
|
||||
if (delay >= 0) {
|
||||
setDelay(delay);
|
||||
}
|
||||
} catch (ex) {
|
||||
// no op
|
||||
}
|
||||
}}
|
||||
type='number'
|
||||
className={inputStyle}
|
||||
/>
|
||||
<span className="mr-1 border">Delay (ms)</span>
|
||||
</span>
|
||||
<Button
|
||||
onPress={() => setShowUploadModal(true)}
|
||||
className="py-0.5 px-1 border-[--hl-sm] h-full bg-[--hl-xxs] aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ring-1 ring-transparent transition-all text-sm"
|
||||
>
|
||||
<Icon icon={file ? 'eye' : 'upload'} /> {file ? 'View Data' : 'Upload Data'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-[100px]">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-sm text-center mr-1 bg-[--color-surprise] text-[--color-font-surprise]"
|
||||
onClick={onRun}
|
||||
style={{ width: '92px', height: '30px' }} // try to make its width same as "Send button"
|
||||
disabled={Array.from(reqList.selectedKeys).length === 0 || isRunning}
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
</Heading>
|
||||
</PaneHeader>
|
||||
<Tabs aria-label='Request group tabs' className="flex-1 w-full h-full flex flex-col">
|
||||
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='request-order'
|
||||
>
|
||||
<i className="fa fa-sort fa-1x h-4 mr-2" />
|
||||
Request Order
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='advanced'
|
||||
>
|
||||
<i className="fa fa-gear fa-1x h-4 mr-2" />
|
||||
Advanced
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='request-order'>
|
||||
<Toolbar className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
|
||||
<span className="mr-2">
|
||||
{
|
||||
Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length ?
|
||||
<span onClick={onToggleSelection}><i style={{ color: 'rgb(74 222 128)' }} className="fa fa-square-check fa-1x h-4 mr-2" /> <span className="cursor-pointer" >Unselect All</span></span> :
|
||||
Array.from(reqList.selectedKeys).length === 0 ?
|
||||
<span onClick={onToggleSelection}><i className="fa fa-square fa-1x h-4 mr-2" /> <span className="cursor-pointer" >Select All</span></span> :
|
||||
<span onClick={onToggleSelection}><i style={{ color: 'rgb(74 222 128)' }} className="fa fa-square-minus fa-1x h-4 mr-2" /> <span className="cursor-pointer" >Select All</span></span>
|
||||
}
|
||||
</span>
|
||||
</Toolbar>
|
||||
<PaneBody placeholder className='p-0'>
|
||||
<GridList
|
||||
id="runner-request-list"
|
||||
// style={{ height: virtualizer.getTotalSize() }}
|
||||
items={reqList.items}
|
||||
selectionMode="multiple"
|
||||
selectedKeys={reqList.selectedKeys}
|
||||
onSelectionChange={reqList.setSelectedKeys}
|
||||
defaultSelectedKeys={allKeys}
|
||||
aria-label="Request Collection"
|
||||
dragAndDropHooks={requestsDnD}
|
||||
className="w-full h-full leading-8 text-base overflow-auto"
|
||||
disabledKeys={disabledKeys}
|
||||
>
|
||||
{item => {
|
||||
const parentFolders = item.ancestorNames.map((parentFolderName: string, i: number) => {
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <TooltipTrigger key={`parent-folder-${i}=${parentFolderName}`} >
|
||||
<Tooltip message={parentFolderName}>
|
||||
<i className="fa fa-folder fa-1x h-4 mr-0.3 text-[--color-font]" />
|
||||
<i className="fa fa-caret-right fa-1x h-4 mr-0.3 text-[--color-font]-50 opacity-50" />
|
||||
</Tooltip>
|
||||
</TooltipTrigger>;
|
||||
});
|
||||
const parentFolderContainer = parentFolders.length > 0 ? <span className="ml-2">{parentFolders}</span> : null;
|
||||
|
||||
return (
|
||||
<RequestItem className='text-[--color-font] border border-solid border-transparent' style={{ 'outline': 'none' }}>
|
||||
{parentFolderContainer}
|
||||
<span className={`ml-2 uppercase text-xs http-method-${item.method}`}>{item.method}</span>
|
||||
<span className="ml-2 hover:underline cursor-pointer" style={{ color: 'white' }} onClick={() => goToRequest(item.id)}>{item.name}</span>
|
||||
</RequestItem>
|
||||
);
|
||||
}}
|
||||
</GridList>
|
||||
</PaneBody>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex align-center overflow-y-auto' id='advanced'>
|
||||
<div className="p-4 w-full">
|
||||
<Heading className="w-full text-lg text-white h-[--line-height-sm] border-solid scro border-b border-b-[--hl-md]">Advanced Settings</Heading>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
name='ignore-undefined-env'
|
||||
onChange={() => { }}
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
checked
|
||||
/>
|
||||
Ignore undefined environments
|
||||
<HelpTooltip className="space-left">Undefined environments will not be rendered for all requests.</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
name='persist-response'
|
||||
onChange={() => { }}
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
checked
|
||||
/>
|
||||
Persist responses for a session
|
||||
<HelpTooltip className="space-left">Enabling this will impact performance while responses are saved for other purposes.</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
name='log-off'
|
||||
onChange={() => { }}
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
/>
|
||||
Turn off logs during run
|
||||
<HelpTooltip className="space-left">Disabling this will improve the performance while logs are not saved.</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
name='stop-on-error'
|
||||
onChange={() => { }}
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
checked
|
||||
/>
|
||||
Stop run if an error occurs
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
checked
|
||||
/>
|
||||
Keep variable values
|
||||
<HelpTooltip className="space-left">Enabling this will persist generated values.</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
/>
|
||||
Run collection without using stored cookies
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={true}
|
||||
checked
|
||||
/>
|
||||
Save cookies after collection run
|
||||
<HelpTooltip className="space-left">Cookies in the running will be saved to the cookie manager.</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{showUploadModal && (
|
||||
<UploadDataModal
|
||||
onUploadFile={(file, uploadData) => {
|
||||
setFile(file);
|
||||
setUploadData(uploadData);
|
||||
}}
|
||||
userUploadData={uploadData}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
/>
|
||||
)}
|
||||
</Pane>
|
||||
</ErrorBoundary>
|
||||
</Panel>
|
||||
<PanelResizeHandle className={direction === 'horizontal' ? 'h-full w-[1px] bg-[--hl-md]' : 'w-full h-[1px] bg-[--hl-md]'} />
|
||||
<Panel id="pane-two" className='pane-two theme--pane'>
|
||||
<PaneHeader className="row-spaced">
|
||||
<Heading className="flex items-center w-full h-[--line-height-sm] pl-3 border-solid scro border-b border-b-[--hl-md]">
|
||||
{
|
||||
executionResult?.duration ?
|
||||
<div className="bg-info tag" >
|
||||
<strong>{`${totalTime.duration} ${totalTime.unit}`}</strong>
|
||||
</div> :
|
||||
<span className="font-bold">Collection Runner</span>
|
||||
}
|
||||
</Heading>
|
||||
</PaneHeader>
|
||||
<Tabs selectedKey={selectedTab} onSelectionChange={setSelectedTab} aria-label='Request group tabs' className="flex-1 w-full h-full flex flex-col">
|
||||
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='test-results'
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
Tests
|
||||
</span>
|
||||
<span
|
||||
className={`test-result-count rounded-sm ml-1 px-1 ${testResultCountTagColor}`}
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{`${passedTestCount} / ${totalTestCount}`}
|
||||
</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='history'
|
||||
>
|
||||
History
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='console'
|
||||
>
|
||||
Console
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='console'>
|
||||
<ResponseTimelineViewer
|
||||
key={workspaceId}
|
||||
timeline={timelines}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='history'>
|
||||
<RunnerResultHistoryPane history={testHistory} gotoExecutionResult={gotoExecutionResult} gotoTestResultsTab={gotoTestResultsTab} />
|
||||
</TabPanel>
|
||||
<TabPanel
|
||||
className='w-full flex-1 flex flex-col overflow-y-auto'
|
||||
id='test-results'
|
||||
>
|
||||
{isRunning &&
|
||||
<div className="h-full w-full text-md flex items-center">
|
||||
<ResponseTimer
|
||||
handleCancel={() => cancelExecution(workspaceId)}
|
||||
activeRequestId={workspaceId}
|
||||
steps={timingSteps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!isRunning && <ErrorBoundary showAlert><RunnerTestResultPane result={executionResult} /></ErrorBoundary>}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default Runner;
|
||||
|
||||
const RequestItem = (
|
||||
{ children, ...props }: GridListItemProps
|
||||
) => {
|
||||
|
||||
return (
|
||||
<GridListItem {...props}>
|
||||
{() => (
|
||||
<>
|
||||
<Button slot="drag" className="hover:cursor-grab">
|
||||
<Icon icon="grip-vertical" className='w-2 text-[--hl] mr-2' />
|
||||
</Button>
|
||||
<Checkbox slot="selection">
|
||||
{({ isSelected }) => {
|
||||
return <>
|
||||
{isSelected ?
|
||||
<i className="fa fa-square-check fa-1x h-4 mr-2" style={{ color: 'rgb(74 222 128)' }} /> :
|
||||
<i className="fa fa-square fa-1x h-4 mr-2" />
|
||||
}
|
||||
</>;
|
||||
}}
|
||||
</Checkbox>
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</GridListItem>
|
||||
);
|
||||
};
|
||||
|
||||
// This is required for tracking the active request for one runner execution
|
||||
// Then in runner cancellation, both the active request and the runner execution will be canceled
|
||||
// TODO(george): Potentially it could be merged with maps in request-timing.ts and cancellation.ts
|
||||
const runnerExecutions = new Map<string, string>();
|
||||
function startExecution(workspaceId: string) {
|
||||
runnerExecutions.set(workspaceId, '');
|
||||
}
|
||||
|
||||
function stopExecution(workspaceId: string) {
|
||||
runnerExecutions.delete(workspaceId);
|
||||
}
|
||||
|
||||
function updateExecution(workspaceId: string, requestId: string) {
|
||||
runnerExecutions.set(workspaceId, requestId);
|
||||
}
|
||||
|
||||
function getExecution(workspaceId: string) {
|
||||
return runnerExecutions.get(workspaceId);
|
||||
}
|
||||
|
||||
function cancelExecution(workspaceId: string) {
|
||||
const activeRequest = getExecution(workspaceId);
|
||||
if (activeRequest) {
|
||||
// TODO: should also try to cancel the request but the cancellation is not idempotent
|
||||
cancelRequestById(activeRequest);
|
||||
window.main.updateLatestStepName({ requestId: workspaceId, stepName: 'Done' });
|
||||
window.main.completeExecutionStep({ requestId: workspaceId });
|
||||
stopExecution(workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
export interface runCollectionActionParams {
|
||||
requests: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export const runCollectionAction: ActionFunction = async ({ request, params }) => {
|
||||
const { organizationId, projectId, workspaceId } = params;
|
||||
invariant(organizationId, 'Organization id is required');
|
||||
invariant(projectId, 'Project id is required');
|
||||
invariant(workspaceId, 'Workspace id is required');
|
||||
const { requests, iterations, delay, userUploadEnvs } = await request.json();
|
||||
const source: RunnerSource = 'runner';
|
||||
|
||||
let testCtx = {
|
||||
source,
|
||||
environmentId: '',
|
||||
iterations,
|
||||
iterationData: userUploadEnvs,
|
||||
duration: 1, // TODO: disable this
|
||||
testCount: 0,
|
||||
avgRespTime: 0,
|
||||
iterationResults: new Array<RunnerResultPerRequest[]>(),
|
||||
done: false,
|
||||
responsesInfo: new Array<ResponseInfo>(),
|
||||
};
|
||||
|
||||
window.main.startExecution({ requestId: workspaceId });
|
||||
window.main.addExecutionStep({
|
||||
requestId: workspaceId,
|
||||
stepName: 'Initializing',
|
||||
});
|
||||
startExecution(workspaceId);
|
||||
|
||||
interface RequestType {
|
||||
name: string;
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
try {
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// nextRequestIdOrName is used to manual set next request in iteration from pre-request script
|
||||
let nextRequestIdOrName = '';
|
||||
|
||||
let iterationResults: RunnerResultPerRequest[] = [];
|
||||
|
||||
for (let j = 0; j < requests.length; j++) {
|
||||
const targetRequest = requests[j] as RequestType;
|
||||
// TODO: we might find a better way to do runner cancellation
|
||||
if (getExecution(workspaceId) === undefined) {
|
||||
throw 'Runner has been stopped';
|
||||
}
|
||||
|
||||
if (nextRequestIdOrName !== '') {
|
||||
if (targetRequest.id === nextRequestIdOrName ||
|
||||
// find the last request with matched name in case mulitple requests with same name in collection runner
|
||||
(targetRequest.name.trim() === nextRequestIdOrName.trim() && j === requests.findLastIndex((req: RequestType) => req.name.trim() === nextRequestIdOrName.trim()))
|
||||
) {
|
||||
// reset nextRequestIdOrName when request name or id meets;
|
||||
nextRequestIdOrName = '';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
updateExecution(workspaceId, targetRequest.id);
|
||||
|
||||
const getCurIterationUserUploadData = (curIteration: number): UserUploadEnvironment | undefined => {
|
||||
if (Array.isArray(userUploadEnvs) && userUploadEnvs.length > 0) {
|
||||
const uploadDataLength = userUploadEnvs.length;
|
||||
if (uploadDataLength >= curIteration + 1) {
|
||||
return userUploadEnvs[curIteration];
|
||||
};
|
||||
return userUploadEnvs[(curIteration + 1) % uploadDataLength];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
window.main.updateLatestStepName({ requestId: workspaceId, stepName: `Iteration ${i + 1} - Executing ${j + 1} of ${requests.length} requests - "${targetRequest.name}"` });
|
||||
|
||||
const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(
|
||||
targetRequest.id,
|
||||
{ lastActive: Date.now() },
|
||||
);
|
||||
invariant(activeRequestMeta, 'Request meta not found');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
const resultCollector = {
|
||||
requestId: targetRequest.id,
|
||||
requestName: targetRequest.name,
|
||||
requestUrl: targetRequest.url,
|
||||
responseReason: '',
|
||||
duration: 1,
|
||||
size: 0,
|
||||
results: new Array<RequestTestResult>(),
|
||||
responseId: '',
|
||||
};
|
||||
const mutatedContext = await sendActionImp({
|
||||
requestId: targetRequest.id,
|
||||
workspaceId,
|
||||
iteration: i + 1,
|
||||
iterationCount: iterations,
|
||||
userUploadEnv: getCurIterationUserUploadData(i),
|
||||
shouldPromptForPathAfterResponse: false,
|
||||
ignoreUndefinedEnvVariable: true,
|
||||
testResultCollector: resultCollector,
|
||||
}) as RequestContext | null;
|
||||
if (mutatedContext?.execution?.nextRequestIdOrName) {
|
||||
nextRequestIdOrName = mutatedContext.execution.nextRequestIdOrName || '';
|
||||
};
|
||||
|
||||
const requestResults: RunnerResultPerRequest = {
|
||||
requestName: targetRequest.name,
|
||||
requestUrl: targetRequest.url,
|
||||
responseCode: 0, // TODO: collect response
|
||||
results: resultCollector.results,
|
||||
};
|
||||
|
||||
iterationResults = [...iterationResults, requestResults];
|
||||
testCtx = {
|
||||
...testCtx,
|
||||
duration: testCtx.duration + resultCollector.duration,
|
||||
responsesInfo: [
|
||||
...testCtx.responsesInfo,
|
||||
{
|
||||
responseId: resultCollector.responseId,
|
||||
originalRequestId: targetRequest.id,
|
||||
originalRequestName: targetRequest.name,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
testCtx = {
|
||||
...testCtx,
|
||||
iterationResults: [...testCtx.iterationResults, iterationResults],
|
||||
};
|
||||
}
|
||||
|
||||
window.main.updateLatestStepName({ requestId: workspaceId, stepName: 'Done' });
|
||||
window.main.completeExecutionStep({ requestId: workspaceId });
|
||||
} catch (e) {
|
||||
// the error could be from third party
|
||||
cancelExecution(workspaceId);
|
||||
const errMsg = encodeURIComponent(e.error || e);
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner?refresh-pane&error=${errMsg}`);
|
||||
} finally {
|
||||
await models.runnerTestResult.create({
|
||||
parentId: workspaceId,
|
||||
source: testCtx.source,
|
||||
// environmentId: string;
|
||||
iterations: testCtx.iterations,
|
||||
duration: testCtx.duration,
|
||||
avgRespTime: testCtx.avgRespTime,
|
||||
iterationResults: testCtx.iterationResults,
|
||||
responsesInfo: testCtx.responsesInfo,
|
||||
});
|
||||
|
||||
// cancelExecution(workspaceId);
|
||||
}
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner?refresh-pane`);
|
||||
};
|
||||
|
||||
export const collectionRunnerStatusLoader: ActionFunction = async ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
invariant(workspaceId, 'Workspace id is required');
|
||||
return null;
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export default defineConfig(({ mode }) => {
|
||||
exclude: ['@getinsomnia/node-libcurl'],
|
||||
// these packages are only used in web worker, Vite won't be able to discover the import on the initial scan,so we need to include them here to let vite pre-bundle them
|
||||
// https://vitejs.dev/guide/dep-pre-bundling.html#customizing-the-behavior
|
||||
include: ['@stoplight/spectral-core', '@stoplight/spectral-ruleset-bundler/with-loader', '@stoplight/spectral-rulesets'],
|
||||
include: ['@stoplight/spectral-core', '@stoplight/spectral-ruleset-bundler/with-loader', '@stoplight/spectral-rulesets', 'codemirror-graphql/utils/SchemaReference', 'openapi-types'],
|
||||
force: true,
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user