diff --git a/.eslintrc b/.eslintrc index 89f8137..fc74d48 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,7 @@ "eslint:recommended" ], "parserOptions": { - "ecmaVersion": 2020, + "ecmaVersion": 2022, "sourceType": "module", "ecmaFeatures": { "jsx": false @@ -46,6 +46,7 @@ "no-extra-semi": 0, "comma-dangle": 2, "no-underscore-dangle": 0, + "no-global-assign": 0, "no-lone-blocks": 0, "array-bracket-spacing": 2, "object-curly-spacing": 2, diff --git a/nightwatch/globals.js b/nightwatch/globals.js index 977566e..05447e0 100644 --- a/nightwatch/globals.js +++ b/nightwatch/globals.js @@ -4,12 +4,23 @@ const {CUSTOM_REPORTER_CALLBACK_TIMEOUT} = require('../src/utils/constants'); const CrashReporter = require('../src/utils/crashReporter'); const helper = require('../src/utils/helper'); const Logger = require('../src/utils/logger'); +const {v4: uuidv4} = require('uuid'); const localTunnel = new LocalTunnel(); const testObservability = new TestObservability(); const nightwatchRerun = process.env.NIGHTWATCH_RERUN_FAILED; const nightwatchRerunFile = process.env.NIGHTWATCH_RERUN_REPORT_FILE; +const _tests = {}; + +const registerListeners = () => { + process.removeAllListeners(`bs:addLog:${process.pid}`); + process.on(`bs:addLog:${process.pid}`, sendTestLog); +}; + +const sendTestLog = (log) => { + testObservability.appendTestItemLog(log, _tests['uniqueId']); +}; module.exports = { @@ -45,10 +56,183 @@ module.exports = { done(results); }, - onEvent({eventName, hook_type, ...args}) { - if (typeof browser !== 'undefined' && eventName === 'TestRunStarted') { - browser.execute(`browserstack_executor: {"action": "annotate", "arguments": {"type":"Annotation","data":"ObservabilitySync:${Date.now()}","level": "debug"}}`); - } + registerEventHandlers(eventBroadcaster) { + + eventBroadcaster.on('TestFinished', (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + if (typeof browser !== 'undefined') { + browser.execute(`browserstack_executor: {"action": "annotate", "arguments": {"type":"Annotation","data":"ObservabilitySync:${Date.now()}","level": "debug"}}`); + } + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing report file for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); + + eventBroadcaster.on('TestCaseStarted', async (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + const reportData = args.report; + const testCaseId = reportData.testCaseStarted.testCaseId; + const pickleId = reportData.testCases.find((testCase) => testCase.id === testCaseId).pickleId; + const pickleData = reportData.pickle.find((pickle) => pickle.id === pickleId); + const gherkinDocument = reportData?.gherkinDocument.find((document) => document.uri === pickleData.uri); + const featureData = gherkinDocument.feature; + const uniqueId = uuidv4(); + _tests['uniqueId'] = uniqueId; + const testMetaData = { + uuid: uniqueId, + startedAt: new Date().toISOString() + }; + if (pickleData) { + testMetaData.scenario = { + name: pickleData.name + }; + } + + if (gherkinDocument && featureData) { + testMetaData.feature = { + path: gherkinDocument.uri, + name: featureData.name, + description: featureData.description + }; + } + _tests[uniqueId] = testMetaData; + await testObservability.sendTestRunEventForCucumber(reportData, gherkinDocument, pickleData, 'TestRunStarted', testMetaData); + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing report file for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); + + eventBroadcaster.on('TestCaseFinished', async (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + const reportData = args.report; + const testCaseId = reportData.testCaseStarted.testCaseId; + const pickleId = reportData.testCases.find((testCase) => testCase.id === testCaseId).pickleId; + const pickleData = reportData.pickle.find((pickle) => pickle.id === pickleId); + const gherkinDocument = reportData?.gherkinDocument.find((document) => document.uri === pickleData.uri); + const uniqueId = _tests['uniqueId']; + const testMetaData = _tests[uniqueId]; + if (testMetaData) { + delete _tests[uniqueId]; + testMetaData.finishedAt = new Date().toISOString(); + await testObservability.sendTestRunEventForCucumber(reportData, gherkinDocument, pickleData, 'TestRunFinished', testMetaData); + } + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing report file for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); + + eventBroadcaster.on('TestStepStarted', (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + const reportData = args.report; + const testCaseId = reportData.testCaseStarted.testCaseId; + const pickleId = reportData.testCases.find((testCase) => testCase.id === testCaseId).pickleId; + const pickleData = reportData.pickle.find((pickle) => pickle.id === pickleId); + const testSteps = reportData.testCases.find((testCase) => testCase.id === testCaseId).testSteps; + const testStepId = reportData.testStepStarted.testStepId; + const pickleStepId = testSteps.find((testStep) => testStep.id === testStepId).pickleStepId; + if (pickleStepId && _tests['testStepId'] !== testStepId) { + const uniqueId = _tests['uniqueId']; + _tests['testStepId'] = testStepId; + const pickleStepData = pickleData.steps.find((pickle) => pickle.id === pickleStepId); + const testMetaData = _tests[uniqueId] || {steps: []}; + if (testMetaData && !testMetaData.steps) { + testMetaData.steps = []; + } + testMetaData.steps?.push({ + id: pickleStepData.id, + text: pickleStepData.text, + started_at: new Date().toISOString() + }); + _tests[uniqueId] = testMetaData; + } + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing report file for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); + + eventBroadcaster.on('TestStepFinished', async (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + const reportData = args.report; + const testCaseId = reportData.testCaseStarted.testCaseId; + const testStepFinished = reportData.testStepFinished; + const pickleId = reportData.testCases.find((testCase) => testCase.id === testCaseId).pickleId; + const pickleData = reportData.pickle.find((pickle) => pickle.id === pickleId); + const testSteps = reportData.testCases.find((testCase) => testCase.id === testCaseId).testSteps; + const testStepId = reportData.testStepFinished.testStepId; + const pickleStepId = testSteps.find((testStep) => testStep.id === testStepId).pickleStepId; + if (pickleStepId && _tests['testStepId']) { + const uniqueId = _tests['uniqueId']; + const pickleStepData = pickleData.steps.find((pickle) => pickle.id === pickleStepId); + const testMetaData = _tests[uniqueId] || {steps: []}; + if (!testMetaData.steps) { + testMetaData.steps = [{ + id: pickleStepData.id, + text: pickleStepData.text, + finished_at: new Date().toISOString(), + result: testStepFinished.testStepResult?.status, + duration: testStepFinished.testStepResult?.duration?.seconds, + failure: testStepFinished.testStepResult?.exception?.message, + failureType: testStepFinished.testStepResult?.exception?.type + }]; + } else { + testMetaData.steps.forEach((step) => { + if (step.id === pickleStepData.id) { + step.finished_at = new Date().toISOString(); + step.result = testStepFinished.testStepResult?.status; + step.duration = testStepFinished.testStepResult?.duration?.seconds; + step.failure = testStepFinished.testStepResult?.exception?.message; + step.failureType = testStepFinished.testStepResult?.exception?.type; + } + }); + } + _tests[uniqueId] = testMetaData; + delete _tests['testStepId']; + if (testStepFinished.httpOutput && testStepFinished.httpOutput.length > 0) { + for (const [index, output] of testStepFinished.httpOutput.entries()) { + if (index % 2 === 0) { + await testObservability.createHttpLogEvent(output, testStepFinished.httpOutput[index + 1], uniqueId); + } + } + } + } + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing report file for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); + + eventBroadcaster.on('ScreenshotCreated', async (args) => { + if (!helper.isTestObservabilitySession()) { + return; + } + try { + if (args.path && _tests['uniqueId']) { + await testObservability.createScreenshotLogEvent(_tests['uniqueId'], args.path, Date.now()); + } + } catch (error) { + CrashReporter.uploadCrashReport(error.message, error.stack); + Logger.error(`Something went wrong in processing screenshot for test observability - ${error.message} with stacktrace ${error.stack}`); + } + }); }, async before(settings) { @@ -73,6 +257,7 @@ module.exports = { try { testObservability.configure(settings); if (helper.isTestObservabilitySession()) { + registerListeners(); settings.globals['customReporterCallbackTimeout'] = CUSTOM_REPORTER_CALLBACK_TIMEOUT; if (testObservability._user && testObservability._key) { await testObservability.launchTestSession(); @@ -91,19 +276,19 @@ module.exports = { async after() { localTunnel.stop(); if (helper.isTestObservabilitySession()) { + process.env.NIGHTWATCH_RERUN_FAILED = nightwatchRerun; + process.env.NIGHTWATCH_RERUN_REPORT_FILE = nightwatchRerunFile; + if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS) { + await helper.deleteRerunFile(); + } try { - await testObservability.stopBuildUpstream(); if (process.env.BS_TESTOPS_BUILD_HASHED_ID) { Logger.info(`\nVisit https://observability.browserstack.com/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`); } + await testObservability.stopBuildUpstream(); } catch (error) { Logger.error(`Something went wrong in stopping build session for test observability - ${error}`); } - process.env.NIGHTWATCH_RERUN_FAILED = nightwatchRerun; - process.env.NIGHTWATCH_RERUN_REPORT_FILE = nightwatchRerunFile; - if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS) { - await helper.deleteRerunFile(); - } } }, diff --git a/package-lock.json b/package-lock.json index 8701fcc..0626c2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,14 @@ { "name": "@nightwatch/browserstack", - "version": "0.1.3", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -70,23 +75,10 @@ "fastq": "^1.6.0" } }, - "@sindresorhus/is": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", - "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==" - }, - "@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "requires": { - "defer-to-connect": "^2.0.1" - } - }, - "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "@types/triple-beam": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", + "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" }, "acorn": { "version": "8.8.1", @@ -244,25 +236,6 @@ "temp-fs": "^0.9.9" } }, - "cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==" - }, - "cacheable-request": { - "version": "10.2.9", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.9.tgz", - "integrity": "sha512-CaAMr53AS1Tb9evO1BIWFnZjSr8A4pbXofpsNVWPMDZZj3ZQKHwsQG9BrTqQ4x5ZYJXz1T2b8LLtTZODxSpzbg==", - "requires": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.2", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -432,21 +405,6 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } - } - }, "deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -462,11 +420,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -706,6 +659,11 @@ "reusify": "^1.0.4" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -782,11 +740,6 @@ "mime-types": "^2.1.12" } }, - "form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" - }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -816,11 +769,6 @@ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -878,24 +826,6 @@ "type-fest": "^0.20.2" } }, - "got": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.0.tgz", - "integrity": "sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==", - "requires": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - } - }, "grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", @@ -928,11 +858,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -943,15 +868,6 @@ "sshpk": "^1.7.0" } }, - "http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "requires": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - } - }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1097,11 +1013,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, "json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1134,14 +1045,6 @@ "verror": "1.10.0" } }, - "keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "requires": { - "json-buffer": "3.0.1" - } - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1177,6 +1080,19 @@ "is-unicode-supported": "^0.1.0" } }, + "logform": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", + "requires": { + "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -1186,11 +1102,6 @@ "get-func-name": "^2.0.0" } }, - "lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" - }, "map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -1209,11 +1120,6 @@ "mime-db": "1.52.0" } }, - "mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1342,11 +1248,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==" - }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -1374,11 +1275,6 @@ "word-wrap": "^1.2.3" } }, - "p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -1483,11 +1379,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1497,6 +1388,16 @@ "safe-buffer": "^5.1.0" } }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1545,25 +1446,12 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "requires": { - "lowercase-keys": "^3.0.0" - } - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -1592,6 +1480,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1681,6 +1574,14 @@ } } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1741,6 +1642,11 @@ "punycode": "^2.1.1" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1783,6 +1689,11 @@ "punycode": "^2.1.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -1807,6 +1718,16 @@ "isexe": "^2.0.0" } }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 3f88edd..373593f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "git-repo-info": "^2.1.1", "gitconfiglocal": "^2.1.0", "request": "^2.88.2", - "strip-ansi": "^6.0.1" + "strip-ansi": "^6.0.1", + "winston-transport": "^4.5.0" }, "devDependencies": { "chai": "^4.3.7", diff --git a/src/testObservability.js b/src/testObservability.js index d65ccd7..9d37b7e 100644 --- a/src/testObservability.js +++ b/src/testObservability.js @@ -367,6 +367,102 @@ class TestObservability { } await helper.uploadEventData(uploadData); } + + async sendTestRunEventForCucumber(reportData, gherkinDocument, pickleData, eventType, testMetaData) { + const sessionCapabilities = reportData.sessionCapabilities; + const {feature, scenario, steps, uuid, startedAt, finishedAt} = testMetaData || {}; + const examples = helper.getScenarioExamples(gherkinDocument, pickleData); + const fullNameWithExamples = examples + ? pickleData.name + ' (' + examples.join(', ') + ')' + : pickleData.name; + const testData = { + uuid: uuid, + started_at: startedAt, + finished_at: finishedAt, + type: 'test', + body: { + lang: 'nightwatch', + code: null + }, + name: fullNameWithExamples, + scope: fullNameWithExamples, + scopes: [feature?.name || ''], + tags: pickleData.tags?.map(({name}) => (name)), + identifier: scenario?.name, + file_name: path.relative(process.cwd(), feature.path), + location: path.relative(process.cwd(), feature.path), + vc_filepath: (this._gitMetadata && this._gitMetadata.root) ? path.relative(this._gitMetadata.root, feature.path) : null, + framework: 'nightwatch', + result: 'pending', + meta: { + feature: feature, + scenario: scenario, + steps: steps, + examples: examples + } + }; + + if (sessionCapabilities) { + testData.integrations = {}; + const provider = helper.getCloudProvider(reportData.host); + testData.integrations[provider] = helper.getIntegrationsObject(sessionCapabilities, reportData.sessionId); + } + + if (reportData.testCaseFinished && steps) { + const testCaseResult = reportData.testCaseFinished; + let result = 'passed'; + steps.every((step) => { + if (step.result === 'FAILED'){ + result = 'failed'; + testCaseResult.failure = step.failure; + testCaseResult.failureType = step.failureType; + + return false; + } else if (step.result === 'SKIPPED') { + result = 'skipped'; + + return false; + } + + return true; + }); + + testData.finished_at = new Date().toISOString(); + testData.result = result; + testData.duration_in_ms = testCaseResult.timestamp.nanos / 1000000; + if (result === 'failed') { + testData.failure = [ + { + 'backtrace': [testCaseResult?.failure ? stripAnsi(testCaseResult?.failure) : 'unknown'] + } + ], + testData.failure_reason = testCaseResult?.failure ? stripAnsi(testCaseResult?.failure) : testCaseResult.message; + if (testCaseResult?.failureType) { + testData.failure_type = testCaseResult.failureType.match(/AssertError/) + ? 'AssertionError' + : 'UnhandledError'; + } + } + } + + const uploadData = { + event_type: eventType, + test_run: testData + }; + await helper.uploadEventData(uploadData); + + } + + async appendTestItemLog (log, testUuid) { + try { + if (testUuid) { + log.test_run_uuid = testUuid; + await helper.uploadEventData({event_type: 'LogCreated', logs: [log]}); + } + } catch (error) { + Logger.error(`Exception in uploading log data to Observability with error : ${error}`); + } + } } module.exports = TestObservability; diff --git a/src/utils/constants.js b/src/utils/constants.js index 4f99831..d871af2 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -7,3 +7,4 @@ exports.RERUN_FILE = 'rerun.json'; exports.DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5000; exports.DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100; exports.CUSTOM_REPORTER_CALLBACK_TIMEOUT = 3600000; +exports.consoleHolder = Object.assign({}, console); diff --git a/src/utils/helper.js b/src/utils/helper.js index d86270c..aaad057 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -8,10 +8,24 @@ const gitconfig = require('gitconfiglocal'); const pGitconfig = promisify(gitconfig); const gitLastCommit = require('git-last-commit'); const {makeRequest} = require('./requestHelper'); -const {RERUN_FILE, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS} = require('./constants'); - +const {RERUN_FILE, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS, consoleHolder} = require('./constants'); const requestQueueHandler = require('./requestQueueHandler'); const Logger = require('./logger'); +const LogPatcher = require('./logPatcher'); +const BSTestOpsPatcher = new LogPatcher({}); + +console = {}; +Object.keys(consoleHolder).forEach(method => { + console[method] = (...args) => { + BSTestOpsPatcher[method](...args); + }; +}); + +exports.debug = (text) => { + if (process.env.BROWSERSTACK_OBSERVABILITY_DEBUG === 'true' || process.env.BROWSERSTACK_OBSERVABILITY_DEBUG === '1') { + consoleHolder.log(`\n[${(new Date()).toISOString()}][ OBSERVABILITY ] ${text}\n`); + } +}; exports.generateLocalIdentifier = () => { const formattedDate = new Intl.DateTimeFormat('en-GB', { @@ -406,7 +420,7 @@ exports.getAccessKey = (settings) => { }; exports.getCloudProvider = (hostname) => { - if (hostname.includes('browserstack')) { + if (hostname && hostname.includes('browserstack')) { return 'browserstack'; } @@ -469,3 +483,43 @@ exports.uploadPending = async ( exports.shutDownRequestHandler = async () => { await requestQueueHandler.shutdown(); }; + +exports.getUniqueIdentifierForCucumber = (pickle) => { + return pickle.uri + '_' + pickle.astNodeIds.join(','); +}; + +exports.getScenarioExamples = (gherkinDocument, scenario) => { + if ((scenario.astNodeIds && scenario.astNodeIds.length <= 1) || scenario.astNodeIds === undefined) { + return; + } + + const pickleId = scenario.astNodeIds[0]; + const examplesId = scenario.astNodeIds[1]; + const gherkinDocumentChildren = gherkinDocument.feature?.children; + + let examples = []; + + gherkinDocumentChildren?.forEach(child => { + if (child.rule) { + child.rule.children.forEach(childLevel2 => { + if (childLevel2.scenario && childLevel2.scenario.id === pickleId && childLevel2.scenario.examples) { + const passedExamples = childLevel2.scenario.examples.flatMap((val) => (val.tableBody)).find((item) => item.id === examplesId)?.cells.map((val) => (val.value)); + if (passedExamples) { + examples = passedExamples; + } + } + }); + } else if (child.scenario && child.scenario.id === pickleId && child.scenario.examples) { + const passedExamples = child.scenario.examples.flatMap((val) => (val.tableBody)).find((item) => item.id === examplesId)?.cells.map((val) => (val.value)); + if (passedExamples) { + examples = passedExamples; + } + } + }); + + if (examples.length) { + return examples; + } + + return; +}; diff --git a/src/utils/logPatcher.js b/src/utils/logPatcher.js new file mode 100644 index 0000000..9642b88 --- /dev/null +++ b/src/utils/logPatcher.js @@ -0,0 +1,54 @@ +const Transport = require('winston-transport'); +const {consoleHolder} = require('./constants'); + +const LOG_LEVELS = { + INFO: 'INFO', + ERROR: 'ERROR', + DEBUG: 'DEBUG', + TRACE: 'TRACE', + WARN: 'WARN' +}; + +class LogPatcher extends Transport { + constructor(opts) { + super(opts); + } + + logToTestOps = (level = LOG_LEVELS.INFO, message = ['']) => { + consoleHolder[level.toLowerCase()](...message); + process.emit(`bs:addLog:${process.pid}`, { + timestamp: new Date().toISOString(), + level: level.toUpperCase(), + message: `"${message.join(', ')}"`, + kind: 'TEST_LOG', + http_response: {} + }); + }; + + /* Patching this would show user an extended trace on their cli */ + trace = (...message) => { + this.logToTestOps(LOG_LEVELS.TRACE, message); + }; + + debug = (...message) => { + this.logToTestOps(LOG_LEVELS.DEBUG, message); + }; + + info = (...message) => { + this.logToTestOps(LOG_LEVELS.INFO, message); + }; + + warn = (...message) => { + this.logToTestOps(LOG_LEVELS.WARN, message); + }; + + error = (...message) => { + this.logToTestOps(LOG_LEVELS.ERROR, message); + }; + + log = (...message) => { + this.logToTestOps(LOG_LEVELS.INFO, message); + }; +}; + +module.exports = LogPatcher; diff --git a/src/utils/requestQueueHandler.js b/src/utils/requestQueueHandler.js index ddb97a8..8fb2396 100644 --- a/src/utils/requestQueueHandler.js +++ b/src/utils/requestQueueHandler.js @@ -10,7 +10,7 @@ class RequestQueueHandler { this.screenshotEventUrl = 'api/v1/screenshots'; this.BATCH_EVENT_TYPES = ['LogCreated', 'TestRunFinished', 'TestRunSkipped', 'HookRunFinished', 'TestRunStarted', 'HookRunStarted']; this.pollEventBatchInterval = null; - RequestQueueHandler.pending_test_uploads = 0; + this.pending_test_uploads = 0; } start() { @@ -31,7 +31,8 @@ class RequestQueueHandler { } this.queue.push(event); - let data = null; const shouldProceed = this.shouldProceed(); + let data = null; + const shouldProceed = this.shouldProceed(); if (shouldProceed) { data = this.queue.slice(0, BATCH_SIZE); this.queue.splice(0, BATCH_SIZE);