From 0669a3fb0c6ff656fb7fcc254dc4c6891fb184fd Mon Sep 17 00:00:00 2001 From: DCM <53174718+dcm@users.noreply.github.com> Date: Fri, 6 Mar 2020 13:08:48 +0000 Subject: [PATCH] Squashed: * bump react to >16.8 to use hooks * add ability to debug using a sample report * bump react-dom to same version as react * require trailing slashes on URLs * stop page from complaining about missing manifest.json * add explicit versions * remove useless imports * fix jsdoc type * fix jsdoc param * Silence loud chrome warnings in the devtools console by switching our fontawesome CDN tag to a crossorigin-friendly tag * First fully working commit, still needs cleanup and passing tests. * package.json: add lodash for uncomplicated comparison and slicing of state (and using module import so as to not bloat production bundle) * .eslintrc.json: Make eslint allow comments up to 120 characters for e.g. JSDoc examples and web links * AssertionPane.js: export aphrodite CSS for reuse * CenterPane.jsx: rewrite of GetCenterPane function in reportUtils.js * state.js: major revamp of UI state management * passing unit tests on local PC, need to combine boilerplate from testsuites into a central place * finishing component unit tests * remove optional dependencies and upgrade react-hooks-testing-library to the non-deprecated version @testing-library/react-hooks * switch to redux to avoid unstable_observedBits log messages --- testplan/web_ui/testing/.env | 4 + testplan/web_ui/testing/.env.production | 5 + testplan/web_ui/testing/.env.test | 2 + testplan/web_ui/testing/.eslintignore | 5 +- testplan/web_ui/testing/.eslintrc.json | 81 +- testplan/web_ui/testing/.gitignore | 6 + testplan/web_ui/testing/DEVELOPMENT.md | 14 + testplan/web_ui/testing/README.md | 5 + .../web_ui/testing/jest-puppeteer.config.js | 40 + testplan/web_ui/testing/package.json | 76 +- testplan/web_ui/testing/public/index.html | 11 +- .../src/AssertionPane/AssertionHeader.js | 2 +- .../src/AssertionPane/AssertionPane.js | 4 +- .../DictAssertions/DictButtonGroup.js | 4 +- .../GraphAssertions/graphUtils.js | 27 +- .../AssertionTypes/NotImplementedAssertion.js | 4 +- .../web_ui/testing/src/Common/ErrorCatch.jsx | 75 + testplan/web_ui/testing/src/Common/Home.jsx | 68 + .../testing/src/Common/LoadingAnimation.jsx | 23 + .../src/Common/{Message.js => Message.jsx} | 35 +- testplan/web_ui/testing/src/Common/Styles.js | 5 +- .../testing/src/Common/SwitchRequireSlash.jsx | 20 + .../testing/src/Common/URLParamRegistry.js | 261 ++ .../__tests__/uriComponentCodec.test.js | 38 + .../src/Common/__tests__/utils.test.js | 36 +- .../web_ui/testing/src/Common/defaults.js | 94 +- .../web_ui/testing/src/Common/fakeReport.js | 3938 ----------------- .../web_ui/testing/src/Common/filterStates.js | 6 + .../testing/src/Common/uriComponentCodec.js | 40 + testplan/web_ui/testing/src/Common/utils.js | 79 +- .../testing/src/Nav/InteractiveNavEntry.js | 2 +- .../web_ui/testing/src/Nav/NavBreadcrumbs.js | 2 +- .../src/Nav/__tests__/InteractiveNav.test.js | 4 +- .../Nav/__tests__/InteractiveNavEntry.test.js | 5 +- .../Nav/__tests__/InteractiveNavList.test.js | 4 +- .../testing/src/Nav/__tests__/Nav.test.js | 9 +- .../src/Nav/__tests__/navUtils.test.js | 12 +- testplan/web_ui/testing/src/Nav/navUtils.js | 10 +- .../web_ui/testing/src/Report/BatchReport.js | 235 - .../BatchReport/__tests__/BatchReport.test.js | 243 + .../__tests__/BatchReport_routing.test.js | 13 + .../__snapshots__/BatchReport.test.js.snap | 2785 ++++++++++++ .../components/AutoSelectRedirect.jsx | 38 + .../BoundStyledListGroupItemLink.jsx | 79 + .../components/CenterPane/Placeholder.jsx | 74 + .../components/CenterPane/index.jsx | 57 + .../components/DisplayEmptyCheckBox.jsx | 52 + .../components/DocumentationButton.jsx | 41 + .../components/EmptyListGroupItem.jsx | 12 + .../BatchReport/components/FilterButton.jsx | 52 + .../components/FilterRadioButton.jsx | 52 + .../BatchReport/components/HelpButton.jsx | 65 + .../BatchReport/components/HelpModal.jsx | 41 + .../BatchReport/components/InfoButton.jsx | 58 + .../BatchReport/components/InfoModal.jsx | 46 + .../BatchReport/components/InfoTable.jsx | 61 + .../BatchReport/components/NavBreadcrumb.jsx | 71 + .../components/NavBreadcrumbContainer.jsx | 31 + .../components/NavBreadcrumbWithNextRoute.jsx | 51 + .../BatchReport/components/NavPanes.jsx | 59 + .../BatchReport/components/NavSidebar.jsx | 47 + .../components/NavSidebarWithNextRoute.jsx | 88 + .../BatchReport/components/PrintButton.jsx | 34 + .../components/StyledListGroupItemLink.jsx | 24 + .../BatchReport/components/StyledNavLink.jsx | 48 + .../BatchReport/components/TagsButton.jsx | 52 + .../Report/BatchReport/components/Toolbar.jsx | 16 + .../BatchReport/components/TopNavbar.jsx | 47 + .../__tests__/AutoSelectRedirect.test.jsx | 14 + .../BoundStyledListGroupItemLink.test.jsx | 14 + .../components/__tests__/CenterPane.test.jsx | 14 + .../__tests__/DisplayEmptyCheckBox.test.jsx | 72 + .../__tests__/DocumentationButton.test.jsx | 53 + .../__tests__/EmptyListGroupItem.test.jsx | 16 + .../__tests__/FilterButton.test.jsx | 34 + .../__tests__/FilterRadioButton.test.jsx | 91 + .../components/__tests__/HelpButton.test.jsx | 71 + .../components/__tests__/HelpModal.test.jsx | 79 + .../components/__tests__/InfoButton.test.jsx | 68 + .../components/__tests__/InfoModal.test.jsx | 69 + .../components/__tests__/InfoTable.test.jsx | 44 + .../__tests__/NavBreadcrumb.test.jsx | 14 + .../__tests__/NavBreadcrumbContainer.test.jsx | 30 + .../NavBreadcrumbWithNextRoute.test.jsx | 14 + .../components/__tests__/NavPanes.test.jsx | 14 + .../components/__tests__/NavSidebar.test.jsx | 14 + .../NavSidebarWithNextRoute.test.jsx | 14 + .../components/__tests__/PrintButton.test.jsx | 18 + .../StyledListGroupItemLink.test.jsx | 14 + .../__tests__/StyledNavLink.test.jsx | 272 ++ .../components/__tests__/TagsButton.test.jsx | 76 + .../components/__tests__/Toolbar.test.jsx | 11 + .../components/__tests__/TopNavbar.test.jsx | 59 + .../components/__tests__/UIRouter.test.jsx | 14 + .../testing/src/Report/BatchReport/index.jsx | 51 + .../src/Report/BatchReport/state/UIRouter.jsx | 16 + .../state/__tests__/reportSlice.test.js | 29 + .../state/__tests__/uiSlice.test.js | 29 + .../state/__tests__/useFetchReport.test.js | 186 + .../state/__tests__/useReportState.test.js | 469 ++ .../state/__tests__/useTargetEntry.test.js | 225 + .../state/reportActions/fetchReport.js | 55 + .../BatchReport/state/reportActions/index.js | 10 + .../BatchReport/state/reportSelectors.js | 55 + .../Report/BatchReport/state/reportSlice.js | 68 + .../src/Report/BatchReport/state/uiActions.js | 14 + .../Report/BatchReport/state/uiMiddleware.js | 17 + .../Report/BatchReport/state/uiSelectors.js | 57 + .../src/Report/BatchReport/state/uiSlice.js | 89 + .../testing/src/Report/BatchReport/style.js | 20 + .../testing/src/Report/InteractiveReport.js | 22 +- .../src/Report/__tests__/BatchReport.test.js | 12 +- .../__tests__/InteractiveReport.test.js | 3 +- .../src/Report/__tests__/reportUtils.test.js | 8 +- .../web_ui/testing/src/Report/reportUtils.js | 30 +- .../web_ui/testing/src/Toolbar/Buttons.js | 2 +- .../testing/src/Toolbar/InteractiveButtons.js | 2 +- .../web_ui/testing/src/Toolbar/Toolbar.js | 2 +- .../web_ui/testing/src/Toolbar/navStyles.js | 2 +- .../src/__tests__/documents/.gitignore | 1 + .../documents/FakeInteractiveReport.js | 72 + .../testing/src/__tests__/documents/README.md | 4 + .../__tests__/documents/SIMPLE_REPORT.json | 99 + .../documents/TESTPLAN_REPORT_1.json} | 217 +- .../documents/TESTPLAN_REPORT_2.json | 265 ++ .../documents/fakeReportAssertions.json | 3671 +++++++++++++++ .../testing/src/__tests__/documents/index.js | 11 + .../web_ui/testing/src/__tests__/testUtils.js | 501 +++ .../testing/src/__tests__/testUtils.test.js | 408 ++ testplan/web_ui/testing/src/index.js | 33 - testplan/web_ui/testing/src/index.jsx | 41 + testplan/web_ui/testing/src/setupTests.js | 8 +- .../web_ui/testing/src/state/AppProvider.jsx | 15 + .../web_ui/testing/src/state/AppRouter.jsx | 16 + .../web_ui/testing/src/state/AppWrapper.jsx | 21 + .../src/state/__tests__/appSlice.test.js | 29 + .../testing/src/state/__tests__/store.test.js | 0 .../web_ui/testing/src/state/appActions.js | 7 + .../web_ui/testing/src/state/appMiddleware.js | 11 + .../web_ui/testing/src/state/appSelectors.js | 18 + testplan/web_ui/testing/src/state/appSlice.js | 55 + testplan/web_ui/testing/src/state/store.js | 22 + 142 files changed, 13181 insertions(+), 4648 deletions(-) create mode 100644 testplan/web_ui/testing/.env create mode 100644 testplan/web_ui/testing/.env.production create mode 100644 testplan/web_ui/testing/.env.test create mode 100644 testplan/web_ui/testing/.gitignore create mode 100644 testplan/web_ui/testing/DEVELOPMENT.md create mode 100644 testplan/web_ui/testing/README.md create mode 100644 testplan/web_ui/testing/jest-puppeteer.config.js create mode 100644 testplan/web_ui/testing/src/Common/ErrorCatch.jsx create mode 100644 testplan/web_ui/testing/src/Common/Home.jsx create mode 100644 testplan/web_ui/testing/src/Common/LoadingAnimation.jsx rename testplan/web_ui/testing/src/Common/{Message.js => Message.jsx} (72%) create mode 100644 testplan/web_ui/testing/src/Common/SwitchRequireSlash.jsx create mode 100644 testplan/web_ui/testing/src/Common/URLParamRegistry.js create mode 100644 testplan/web_ui/testing/src/Common/__tests__/uriComponentCodec.test.js delete mode 100644 testplan/web_ui/testing/src/Common/fakeReport.js create mode 100644 testplan/web_ui/testing/src/Common/filterStates.js create mode 100644 testplan/web_ui/testing/src/Common/uriComponentCodec.js delete mode 100644 testplan/web_ui/testing/src/Report/BatchReport.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport_routing.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/__tests__/__snapshots__/BatchReport.test.js.snap create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/AutoSelectRedirect.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/BoundStyledListGroupItemLink.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/Placeholder.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/index.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/DisplayEmptyCheckBox.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/DocumentationButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/EmptyListGroupItem.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/FilterButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/FilterRadioButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/HelpButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/HelpModal.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/InfoButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/InfoModal.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/InfoTable.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumb.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbContainer.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbWithNextRoute.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavPanes.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebar.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebarWithNextRoute.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/PrintButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/StyledListGroupItemLink.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/StyledNavLink.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/TagsButton.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/Toolbar.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/TopNavbar.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/AutoSelectRedirect.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/BoundStyledListGroupItemLink.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/CenterPane.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DisplayEmptyCheckBox.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DocumentationButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/EmptyListGroupItem.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterRadioButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpModal.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoModal.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoTable.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumb.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbContainer.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbWithNextRoute.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavPanes.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebar.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebarWithNextRoute.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/PrintButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledListGroupItemLink.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledNavLink.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TagsButton.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/Toolbar.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TopNavbar.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/UIRouter.test.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/index.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/UIRouter.jsx create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/reportSlice.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/uiSlice.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useFetchReport.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useReportState.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useTargetEntry.test.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/fetchReport.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/index.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/reportSelectors.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/reportSlice.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/uiActions.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/uiMiddleware.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/uiSelectors.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/state/uiSlice.js create mode 100644 testplan/web_ui/testing/src/Report/BatchReport/style.js create mode 100644 testplan/web_ui/testing/src/__tests__/documents/.gitignore create mode 100644 testplan/web_ui/testing/src/__tests__/documents/FakeInteractiveReport.js create mode 100644 testplan/web_ui/testing/src/__tests__/documents/README.md create mode 100644 testplan/web_ui/testing/src/__tests__/documents/SIMPLE_REPORT.json rename testplan/web_ui/testing/src/{Common/sampleReports.js => __tests__/documents/TESTPLAN_REPORT_1.json} (61%) create mode 100644 testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_2.json create mode 100644 testplan/web_ui/testing/src/__tests__/documents/fakeReportAssertions.json create mode 100644 testplan/web_ui/testing/src/__tests__/documents/index.js create mode 100644 testplan/web_ui/testing/src/__tests__/testUtils.js create mode 100644 testplan/web_ui/testing/src/__tests__/testUtils.test.js delete mode 100644 testplan/web_ui/testing/src/index.js create mode 100644 testplan/web_ui/testing/src/index.jsx create mode 100644 testplan/web_ui/testing/src/state/AppProvider.jsx create mode 100644 testplan/web_ui/testing/src/state/AppRouter.jsx create mode 100644 testplan/web_ui/testing/src/state/AppWrapper.jsx create mode 100644 testplan/web_ui/testing/src/state/__tests__/appSlice.test.js create mode 100644 testplan/web_ui/testing/src/state/__tests__/store.test.js create mode 100644 testplan/web_ui/testing/src/state/appActions.js create mode 100644 testplan/web_ui/testing/src/state/appMiddleware.js create mode 100644 testplan/web_ui/testing/src/state/appSelectors.js create mode 100644 testplan/web_ui/testing/src/state/appSlice.js create mode 100644 testplan/web_ui/testing/src/state/store.js diff --git a/testplan/web_ui/testing/.env b/testplan/web_ui/testing/.env new file mode 100644 index 000000000..11e4a7122 --- /dev/null +++ b/testplan/web_ui/testing/.env @@ -0,0 +1,4 @@ +EXTEND_ESLINT=true +# make 'serve' skip checking its servers for updates +# see: node_modules/serve/bin/serve.js:365 +NO_UPDATE_CHECK=1 diff --git a/testplan/web_ui/testing/.env.production b/testplan/web_ui/testing/.env.production new file mode 100644 index 000000000..ccfcf41f0 --- /dev/null +++ b/testplan/web_ui/testing/.env.production @@ -0,0 +1,5 @@ +#REACT_APP_THREAD=thread +#REACT_APP_THREADJS=thread.js +#REACT_APP_RELTHREAD=./thread +#REACT_APP_USE_THREAD=1 +#EXTEND_ESLINT=1 diff --git a/testplan/web_ui/testing/.env.test b/testplan/web_ui/testing/.env.test new file mode 100644 index 000000000..d37fccd16 --- /dev/null +++ b/testplan/web_ui/testing/.env.test @@ -0,0 +1,2 @@ +SKIP_PREFLIGHT_CHECK=true +BROWSER=none diff --git a/testplan/web_ui/testing/.eslintignore b/testplan/web_ui/testing/.eslintignore index e81312e58..b449a9bfb 100644 --- a/testplan/web_ui/testing/.eslintignore +++ b/testplan/web_ui/testing/.eslintignore @@ -1,4 +1,3 @@ -__tests__ __snapshots__ -sampleReports.js -fakeReport.js +/build/ +/node_modules/* diff --git a/testplan/web_ui/testing/.eslintrc.json b/testplan/web_ui/testing/.eslintrc.json index ce2387180..0d9c41109 100644 --- a/testplan/web_ui/testing/.eslintrc.json +++ b/testplan/web_ui/testing/.eslintrc.json @@ -1,21 +1,42 @@ { "env": { - "browser": 2, - "es6": 2, - "node": 2, - "commonjs": 2 + "browser": true, + "es6": true, + "commonjs": true }, "extends": [ "react-app" ], + "overrides": [ + { + "files": [ + "*.test.js", + "*.test.jsx", + "*.test.ts", + "*.test.tsx", + "./src/setupTests.js", + "./jest-puppeteer.config.js" + ], + "env": { + "jest": true, + "node": true + }, + "globals": { + "page": true, + "browser": true, + "context": true, + "jestPuppeteer": true + } + } + ], "parserOptions": { "ecmaFeatures": { - "experimentalObjectRestSpread": 2, - "jsx": 2, - "arrowFunctions": 2, - "classes": 2, - "modules": 2, - "defaultParams": 2 + "experimentalObjectRestSpread": true, + "jsx": true, + "arrowFunctions": true, + "classes": true, + "modules": true, + "defaultParams": true }, "sourceType": "module" }, @@ -24,27 +45,45 @@ "react" ], "rules": { - "max-len": ["error", { "code": 80 }], + "max-len": [ + "error", + { + "code": 80, + "comments": 120 + } + ], "linebreak-style": [ "error", "unix" ], - "semi": ["error", "always"], + "semi": [ + "error", + "always" + ], "no-const-assign": 2, "no-dupe-class-members": 2, "no-duplicate-case": 2, - "no-extra-parens": [2, "functions"], + "no-extra-parens": [ + 2, + "functions" + ], "no-self-compare": 2, "accessor-pairs": 2, - "comma-spacing": [2, { - "before": false, - "after": true - }], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], "constructor-super": 2, - "new-cap": [2, { - "newIsCap": true, - "capIsNew": false - }], + "new-cap": [ + 2, + { + "newIsCap": true, + "capIsNew": false + } + ], "new-parens": 2, "no-array-constructor": 2, "no-class-assign": 2, diff --git a/testplan/web_ui/testing/.gitignore b/testplan/web_ui/testing/.gitignore new file mode 100644 index 000000000..46fd3f783 --- /dev/null +++ b/testplan/web_ui/testing/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env.local +.env.production.local +.env.development.local +yarn-error.log +/build/ diff --git a/testplan/web_ui/testing/DEVELOPMENT.md b/testplan/web_ui/testing/DEVELOPMENT.md new file mode 100644 index 000000000..ec85d87e4 --- /dev/null +++ b/testplan/web_ui/testing/DEVELOPMENT.md @@ -0,0 +1,14 @@ +# Testplan Web UI + +- Recommended Development Setup + * [IntelliJ-based IDEs](#intellij-based-ides) + * [VSCode](#vscode) + + +#### IntelliJ-based IDEs + +TODO + +### VSCode + +TODO diff --git a/testplan/web_ui/testing/README.md b/testplan/web_ui/testing/README.md new file mode 100644 index 000000000..aefef544b --- /dev/null +++ b/testplan/web_ui/testing/README.md @@ -0,0 +1,5 @@ +# Web UI + +- [Recommended development setup](DEVELOPMENT.md) +- [Report fetch](src/Report/BatchReport/state/reportWorker) +- diff --git a/testplan/web_ui/testing/jest-puppeteer.config.js b/testplan/web_ui/testing/jest-puppeteer.config.js new file mode 100644 index 000000000..05ba94dd2 --- /dev/null +++ b/testplan/web_ui/testing/jest-puppeteer.config.js @@ -0,0 +1,40 @@ +const + { dirname, resolve, delimiter } = require('path'), + { appPackageJson, appNodeModules } = require('react-scripts/config/paths'), + { getServers } = require('jest-dev-server'), + { env: { PATH, SKIP_BUILD, CI, HEADLESS }, execPath } = process, + // serve production build for puppeteer integration tests + { scripts: { build, serve } } = require(appPackageJson), + isSkipBuild = !!JSON.parse(SKIP_BUILD || '0'), + isHeadless = !!JSON.parse(CI || '0') && !!JSON.parse(HEADLESS || '1'); + +module.exports = { + // see github.com/smooth-code/jest-puppeteer/issues/120#issuecomment-464185653 + launch: { + dumpio: true, + ...(isHeadless ? {} : { headless: false, devtools: true }), + }, + server: { + debug: true, + proto: 'http', + host: '127.0.0.1', + port: 5000, + usedPortAction: 'error', + launchTimeout: 1000 * 60 * 5, + command: (isSkipBuild ? '' : `${build} && `) + serve, + options: { + env: { + PATH: [ + dirname(execPath), + resolve(appNodeModules, '.bin'), + PATH + ].join(delimiter), + }, + windowsVerbatimArguments: true, + }, + }, + getOrigin() { + return `${this.server.proto}://${this.server.host}:${this.server.port}`; + }, + getProcesses: getServers, +}; diff --git a/testplan/web_ui/testing/package.json b/testplan/web_ui/testing/package.json index 3298cedfd..5823c6aba 100644 --- a/testplan/web_ui/testing/package.json +++ b/testplan/web_ui/testing/package.json @@ -4,44 +4,62 @@ "private": true, "resolutions": { "@babel/preset-env": "^7.8.7", - "watchpack": "1.6.1" + "watchpack": "1.6.1", + "immer": "^6.0.0" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "1.2.2", "@fortawesome/free-solid-svg-icons": "5.2.0", "@fortawesome/react-fontawesome": "0.1.3", + "@reduxjs/toolkit": "^1.3.4", "ag-grid-community": "^21.2.1", "ag-grid-react": "^21.2.1", "aphrodite": "2.2.3", "axios": "0.19.0", "bootstrap": "4.3.1", "eslint-plugin-react": "^7.14.3", - "react": "16.6.0", + "history": "^4.10.1", + "lodash": "^4.17.15", + "prop-types": "^15.7.2", + "react": "~16.12.0", "react-copy-html-to-clipboard": "6.0.4", "react-custom-scrollbars": "4.2.1", - "react-dom": "16.6.0", + "react-dom": "~16.12.0", "react-portal-tooltip": "2.4.0", + "react-redux": "^7.2.0", + "react-router": "^5.0.0", "react-router-dom": "^5.0.0", "react-scripts": "^3.4.0", "react-spinners": "^0.6.0", "react-syntax-highlighter": "^11.0.2", - "react-test-renderer": "16.6.0", "react-vis": "^1.11.7", - "reactstrap": "6.3.0" + "reactstrap": "6.3.0", + "redux": "^4.0.5" }, "devDependencies": { + "@testing-library/dom": "^6.12.2", + "@testing-library/jest-dom": "^4.0.0", + "@testing-library/react": "^9.3.2", "enzyme": "3.7.0", "enzyme-adapter-react-16": "1.6.0", "enzyme-to-json": "^3.3.5", + "expect-puppeteer": "^4.4.0", + "jest": "^24.9.0", + "jest-dev-server": "^4.4.0", + "jest-environment-node": "^25.3.0", + "jest-environment-puppeteer": "^4.4.0", + "jest-puppeteer": "^4.4.0", "moxios": "0.4.0", - "npm-force-resolutions": "0.0.3" + "puppeteer": "^2.0.0", + "serve": "^11.0.0" }, "scripts": { + "serve": "serve -d -s -l 5000 build", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "lint": "eslint --ext .js src", - "lint:fix": "eslint --ext .js src --fix", + "lint": "eslint --ext .js --ext .jsx src", + "lint:fix": "eslint --ext .js --ext .jsx src --fix", "eject": "react-scripts eject" }, "homepage": "/", @@ -58,9 +76,49 @@ ] }, "jest": { + "globalSetup": "jest-environment-puppeteer/setup", + "globalTeardown": "jest-environment-puppeteer/teardown", "snapshotSerializers": [ "enzyme-to-json/serializer" ] }, - "proxy": "http://localhost:4000" + "optionalDependencies": { + "@babel/cli": "^7.4.4", + "@babel/node": "^7.5.0", + "@babel/register": "^7.9.0", + "@types/bootstrap": "^3.3.37", + "@types/cheerio": "^0.22.11", + "@types/enzyme": "^3.1.16", + "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/enzyme-to-json": "^1.5.1", + "@types/expect-puppeteer": "^3.3.1", + "@types/history": "^4.7.5", + "@types/jest": "^24.9.0", + "@types/jest-environment-puppeteer": "^4.3.1", + "@types/jquery": "^3.3.32", + "@types/lodash": "^4.14.149", + "@types/node": "^13.11.0", + "@types/prop-types": "^15.7.3", + "@types/puppeteer": "^2.0.1", + "@types/react": "^16.9.32", + "@types/react-dom": "^16.9.5", + "@types/react-redux": "^7.1.0", + "@types/react-router": "^5.1.4", + "@types/react-router-dom": "^5.1.3", + "@types/react-syntax-highlighter": "^11.0.2", + "@types/react-test-renderer": "^16.0.2", + "@types/reactstrap": "^8.0.1", + "@types/sizzle": "^2.3.2", + "@types/testing-library__dom": "^6.12.0", + "@types/testing-library__react": "^9.1.2", + "@types/webpack-dev-server": "^3.9.0", + "@types/webpack-env": "^1.14.0", + "@typescript-eslint/eslint-plugin": "^2.27.0", + "@typescript-eslint/parser": "^2.27.0", + "eslint": "^6.8.0", + "qs": "^6.9.1", + "react-devtools": "^3.6.1", + "redux-mock-store": "^1.5.3", + "typescript": "^3.8.3" + } } diff --git a/testplan/web_ui/testing/public/index.html b/testplan/web_ui/testing/public/index.html index 7cc6f4511..787b44095 100644 --- a/testplan/web_ui/testing/public/index.html +++ b/testplan/web_ui/testing/public/index.html @@ -4,9 +4,16 @@ - - + + <% if(process.env.NODE_ENV !== 'production') { %> + <% if(process.env.REACT_APP_DEVTOOLS) { %> + + <% } %> + <% } %> Testplan diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionHeader.js b/testplan/web_ui/testing/src/AssertionPane/AssertionHeader.js index 3b21720a0..cf0c11baf 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionHeader.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionHeader.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {css, StyleSheet} from 'aphrodite'; import {CardHeader, Tooltip} from 'reactstrap'; import {library} from '@fortawesome/fontawesome-svg-core'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import {faLayerGroup} from '@fortawesome/free-solid-svg-icons'; library.add(faLayerGroup); diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionPane.js b/testplan/web_ui/testing/src/AssertionPane/AssertionPane.js index 69366903a..0b163940e 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionPane.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionPane.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {css, StyleSheet} from 'aphrodite'; import {library} from '@fortawesome/fontawesome-svg-core'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import { faMinusCircle, faPlusCircle, @@ -157,7 +157,7 @@ AssertionPane.propTypes = { descriptionEntries: PropTypes.arrayOf(PropTypes.string), }; -const styles = StyleSheet.create({ +export const styles = StyleSheet.create({ icon: { margin: '0rem .75rem 0rem 0rem', cursor: 'pointer', diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/DictButtonGroup.js b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/DictButtonGroup.js index 137240f94..17c1b5d96 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/DictButtonGroup.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/DictButtonGroup.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {Button, ButtonGroup} from 'reactstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import {library} from '@fortawesome/fontawesome-svg-core'; import { faSortAmountUp, @@ -159,4 +159,4 @@ DictButtonGroup.propTypes = { }; -export default DictButtonGroup; \ No newline at end of file +export default DictButtonGroup; diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/GraphAssertions/graphUtils.js b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/GraphAssertions/graphUtils.js index ee36633eb..d95a707de 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/GraphAssertions/graphUtils.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/GraphAssertions/graphUtils.js @@ -7,8 +7,8 @@ * Return the JSX for the 'style' parameter for the graph component * to help render nicer graphs, not currently set by the user * - * @param {str} graph_type - The type of graph being rendered - * @return {dict[key: object]} Returns any style required for the graph + * @param {string} graph_type - The type of graph being rendered + * @return {Object.|undefined} Returns any style required for the graph */ export function returnStyle(graph_type){ if(graph_type === 'Contour'){ @@ -25,8 +25,8 @@ export function returnStyle(graph_type){ * Return the JSX for the 'XType' parameter for the XYPlot * component to be make the x axis increment either numerical or ordinal * - * @param {str} graph_type - The type of graph being rendered - * @return {str} Returns ordinal if x-axis should be + * @param {string} graph_type - The type of graph being rendered + * @return {string|undefined} Returns ordinal if x-axis should be * letters instead of numerical */ export function returnXType(graph_type){ @@ -43,11 +43,12 @@ const COLOUR_PALETTE=['#1c5c9c', '#68caea', '#7448c5', '#633836', * otherwise return from a colour scheme/palette, then random colours * (tinted darker/blue) * - * @param {dict[str, dict[str, object]]} series_options - dictionary with - * series name and user specified options - * @param {dict[str, list]} data - every data series name along - * with the relative list of data - * @return {dict[str, str]} Every series name and it's display colour + * @param {Object.>} series_options - + * dictionary with series name and user specified options + * @param {Object.} data - + * every data series name along with the relative list of data + * @return {Object.} + * Every series name and it's display colour */ export function returnColour(series_options, data){ const series_names = Object.keys(data); @@ -92,10 +93,10 @@ export function returnColour(series_options, data){ * Return an xAxisTitle for the graph component * or nothing if it has not been set * - * @param {dict[str: object]} graph_options - user specified options + * @param {Object.} graph_options - user specified options * for the entire graph * - * @return {str/null} The axis title, or null if not set + * @return {string | undefined} The axis title, or null if not set */ export function returnXAxisTitle(graph_options){ if(graph_options == null){ @@ -109,10 +110,10 @@ export function returnXAxisTitle(graph_options){ /** * Return an yAxisTitle for the graph component * or nothing if it has not been set - * @param {dict[str: object]} graph_options - user specified options + * @param {Object.} graph_options - user specified options * for the entire graph * - * @return {str/null} The axis title, or null if not set + * @return {string | undefined} The axis title, or null if not set */ export function returnYAxisTitle(graph_options){ if(graph_options == null){ diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/NotImplementedAssertion.js b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/NotImplementedAssertion.js index a31ec6869..d36fac255 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/NotImplementedAssertion.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/NotImplementedAssertion.js @@ -1,6 +1,6 @@ import React, {Component, Fragment} from 'react'; import {library} from '@fortawesome/fontawesome-svg-core'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import {faFrown} from '@fortawesome/free-solid-svg-icons'; import {css, StyleSheet} from 'aphrodite'; @@ -34,4 +34,4 @@ const styles = StyleSheet.create({ }, }); -export default NotImplementedAssertion; \ No newline at end of file +export default NotImplementedAssertion; diff --git a/testplan/web_ui/testing/src/Common/ErrorCatch.jsx b/testplan/web_ui/testing/src/Common/ErrorCatch.jsx new file mode 100644 index 000000000..2f426176f --- /dev/null +++ b/testplan/web_ui/testing/src/Common/ErrorCatch.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compose } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; + +const __DEV__ = 'production' !== process.env.NODE_ENV; + +export default class ErrorCatch extends React.Component { + constructor(props) { + super(props); + this.state = { + hasError: false, + errName: '', + errMsg: '', + errMiscEntries: {}, + }; + } + static getDerivedStateFromError(error) { + const { name: errName, message: errMsg } = error, + nonMiscProps = ['name', 'message']; + let errMiscEntries = ''; + try { + errMiscEntries = compose( + Object.fromEntries, + filtered => filtered.map(entry => [entry[0], entry[1].value]), + entries => entries.filter(entry => !nonMiscProps.includes(entry[0])), + Object.entries, + Object.getOwnPropertyDescriptors(error), + ); + // errMiscEntries = + // Object.fromEntries( + // Object.entries( + // Object.getOwnPropertyDescriptors(error) + // ).filter(entry => !nonMiscProps.includes(entry[0])) + // .map(entry => [entry[0], entry[1].value]) + // ); + } catch(err2) { console.error(err2); } + return { hasError: true, errName, errMsg, errMiscEntries }; + } + componentDidCatch(error, errorInfo) { + console.error(error, errorInfo); + } + render() { + if (this.state.hasError) { + if(__DEV__) { + const headline = `A ${this.state.errName} was thrown` + ( + this.props.level ? ` from the ${this.props.level} level:` : ':' + ); + const preBlocks = Array.isArray(this.state.errMiscEntries) ? ( + this.state.errMiscEntries.map(([attr, val], idx) => ( +
+

{attr}

+
{val}
+
+ )) + ) : null; + return ( + <> +

{headline}

+

{this.state.errMsg}

+ {preBlocks} + + ); + } + return ( +

An unexpected error occurred. Please try refreshing the page.

+ ); + } + return this.props.children; + } +} + +ErrorCatch.propTypes = { + level: PropTypes.string.isRequired, + children: PropTypes.element.isRequired, +}; diff --git a/testplan/web_ui/testing/src/Common/Home.jsx b/testplan/web_ui/testing/src/Common/Home.jsx new file mode 100644 index 000000000..eebc501c7 --- /dev/null +++ b/testplan/web_ui/testing/src/Common/Home.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; + +const NoReport = () => ( +

NO TEST REPORT TO RENDER

+); + +/** + * When NODE_ENV === 'development' we can choose an actual report to render + * by setting the environment variable REACT_APP_REPORT_UID_OVERRIDE + * to an actual report UID. + * + * Note that process.env.REACT_APP_* vars are elided during compilation. See + * node_modules/react-scripts/config/env.js:73. + */ +const DevHome = () => { + const REPORT_UID_OVERRIDE = process.env.REACT_APP_REPORT_UID_OVERRIDE; + const { search: query, hash} = useLocation(); + if(typeof REPORT_UID_OVERRIDE !== 'undefined') { + // see CodeSandbox 'example.js' here: + // https://reacttraining.com/react-router/web/example/query-parameters + const dest = `/testplan/${REPORT_UID_OVERRIDE}${query}${hash}`; + console.log(`REACT_APP_REPORT_UID_OVERRIDE='${REPORT_UID_OVERRIDE}' ` + + `so redirecting to '${dest}'`); + return ; + } else { + return ( + <> + +

+ Set these environment variables before compiling: +
    +
  • + REACT_APP_REPORT_UID_OVERRIDE - UID of an existing report +
  • +
  • + REACT_APP_API_BASE_URL - Full 'scheme://host:port/path' of your + API server, e.g. 'http://couch.example.com/devel' +
  • +
+ If your database server has a different origin than your development + server and doesn't have CORS configured, you'll need to launch a + newer Chrome / Chromium with the '--disable-web-security' flag to + allow cross-origin requests. +

+ + ); + } +}; + +const ProdHome = () => ( + <> + +

+ Navigate to the test report link printed at the end of your testplan + run. +

+ +); + +// TODO: Make a homepage that is unconditionally useful +export default function Home() { + return ( + <> + {process.env.NODE_ENV === 'development' ? : } + + ); +} diff --git a/testplan/web_ui/testing/src/Common/LoadingAnimation.jsx b/testplan/web_ui/testing/src/Common/LoadingAnimation.jsx new file mode 100644 index 000000000..7a3ecda1a --- /dev/null +++ b/testplan/web_ui/testing/src/Common/LoadingAnimation.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import FadeLoader from 'react-spinners/FadeLoader'; + +// Used to center spinner on page,suggested by +// github.com/davidhu2000/react-spinners/issues/53#issuecomment-472369554 +const divStyle = { + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', +}; + +const defaultProps = { + height: 15, + width: 5, + radius: 10, +}; + +export default props => ( +
+ +
+); diff --git a/testplan/web_ui/testing/src/Common/Message.js b/testplan/web_ui/testing/src/Common/Message.jsx similarity index 72% rename from testplan/web_ui/testing/src/Common/Message.js rename to testplan/web_ui/testing/src/Common/Message.jsx index 52937c0cb..6fd49c99f 100644 --- a/testplan/web_ui/testing/src/Common/Message.js +++ b/testplan/web_ui/testing/src/Common/Message.jsx @@ -1,8 +1,20 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import {StyleSheet, css} from 'aphrodite'; +import { StyleSheet, css } from 'aphrodite/es'; -import {MEDIUM_GREY} from "./defaults"; +import { MEDIUM_GREY } from './defaults'; + +const styles = StyleSheet.create({ + message: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center', + minHeight: '100vh', + color: MEDIUM_GREY, + }, +}); /** * Displayed a message in the center of the container. @@ -13,9 +25,10 @@ class Message extends Component { paddingLeft: this.props.left, paddingTop: '4.5em', }; + const Tag = this.props.tag || 'h1'; return (
-

{this.props.message}

+ {this.props.message}
); } @@ -28,16 +41,4 @@ Message.propTypes = { left: PropTypes.string, }; -const styles = StyleSheet.create({ - message: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - textAlign: 'center', - minHeight: '100vh', - color: MEDIUM_GREY, - }, -}); - -export default Message; \ No newline at end of file +export default Message; diff --git a/testplan/web_ui/testing/src/Common/Styles.js b/testplan/web_ui/testing/src/Common/Styles.js index 2212d6dfc..0fd316d41 100644 --- a/testplan/web_ui/testing/src/Common/Styles.js +++ b/testplan/web_ui/testing/src/Common/Styles.js @@ -1,9 +1,6 @@ -/** - * Common aphrodite styles. - */ +/** Common aphrodite styles. */ import {StyleSheet} from 'aphrodite'; - const Styles = StyleSheet.create({ unselectable: { "moz-user-select": "-moz-none", diff --git a/testplan/web_ui/testing/src/Common/SwitchRequireSlash.jsx b/testplan/web_ui/testing/src/Common/SwitchRequireSlash.jsx new file mode 100644 index 000000000..e6ec72faa --- /dev/null +++ b/testplan/web_ui/testing/src/Common/SwitchRequireSlash.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; + +export default ({ children = null, location = null }) => ( + + {/* Must be first - require trailing slash */} + ( + + )} + /> + {children} + +); diff --git a/testplan/web_ui/testing/src/Common/URLParamRegistry.js b/testplan/web_ui/testing/src/Common/URLParamRegistry.js new file mode 100644 index 000000000..bba6fb029 --- /dev/null +++ b/testplan/web_ui/testing/src/Common/URLParamRegistry.js @@ -0,0 +1,261 @@ +import * as _qsStringify from 'qs/lib/stringify'; +import * as _qsParse from 'qs/lib/parse'; +import _difference from 'lodash/difference'; +import _intersection from 'lodash/intersection'; +import _isEqual from 'lodash/isEqual'; +import _cloneDeep from 'lodash/cloneDeep'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; + +const qsParse = queryString => _qsParse(queryString, { + ignoreQueryPrefix: true, + allowDots: true, + charset: 'utf-8', + strictNullHandling: true, +}); + +const qsStringify = obj => _qsStringify(obj, { + arrayFormat: 'repeat', + allowDots: true, + addQueryPrefix: true, + strictNullHandling: true, + sort: (s1, s2) => s1.localeCompare(s2), // alphabetical +}); + +const isArray = Array.isArray; +const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const defineProperty = Object.defineProperty.bind(Object); + +export default class URLParamRegistry { + + _prevSearchString = ''; + _prevSearchObj = {}; + _prevSearchKeys = []; + _unlistenHistory = null; + /** @type {{ [queryParam: string]: { defaultVal: any; actionCreators: Array; }}} */ + _param2ActionCreatorsMap = {}; + /** @type {{ [actionType: string]: { defaultVal: any; params: string[]; }}} */ + _actionType2ParamsMap = {}; + + _actionInterceptor(action) { + const { defaultVal, params } = this._actionType2ParamsMap[action.type]; + if(isArray(params) && params.length > 0) { + const currLocation = this._history.location; + const currSearchString = currLocation.search; + const currSearchObj = qsParse(currSearchString); + const newSearchObj = _cloneDeep(currSearchObj); + for(const param of params) { + if(action.payload === defaultVal) { + delete newSearchObj[param]; + } else { + newSearchObj[param] = action.payload; + } + } + if(!_isEqual(newSearchObj, currSearchObj)) { + const newLocation = { + ...currLocation, + search: qsStringify(newSearchObj), + }; + this._history.push(newLocation, undefined, this._history.length); + } + } + } + + _createHistoryListener(store) { + return (location, action, programmaticPushLength) => { + const wasProgrammaticPush = ( + action === 'PUSH' && + typeof programmaticPushLength === 'number' && + this._history.length === (programmaticPushLength + 1) + ); + if(!wasProgrammaticPush) { + if(this._unlistenHistory && location.search !== + this._prevSearchString) { + const searchObj = qsParse(location.search); + const searchKeys = Object.keys(searchObj); + const addedKeys = _difference(searchKeys, this._prevSearchKeys); + for(const newKey of addedKeys) { + const { actionCreators } = this._param2ActionCreatorsMap[newKey]; + if(actionCreators) { + const payloadVal = searchObj[newKey]; + for(const ac of actionCreators) { + store.dispatch(ac(payloadVal)); + } + } + } + const removedKeys = _difference(this._prevSearchKeys, searchKeys); + for(const oldKey of removedKeys) { + const { + defaultVal, actionCreators + } = this._param2ActionCreatorsMap[oldKey]; + if(actionCreators) { + for(const ac of actionCreators) { + store.dispatch(ac(defaultVal)); + } + } + } + const commonKeys = _intersection(searchKeys, this._prevSearchKeys); + for(const key of commonKeys) { + const newVal = searchObj[key]; + const oldVal = this._prevSearchObj[key]; + if(!_isEqual(newVal, oldVal)) { + const { actionCreators } = this._param2ActionCreatorsMap[key]; + if(actionCreators) { + for(const actionCreator of actionCreators) { + store.dispatch(actionCreator(newVal)); + } + } + } + } + this._prevSearchString = location.search; + this._prevSearchObj = searchObj; + this._prevSearchKeys = searchKeys; + } + } + }; + } + + static _tagHistory(history) { + const oldPush = history.push.bind(history); + function newPush(path, state, programmaticPushLength) { + this._programmaticPushLength = programmaticPushLength; + return oldPush(path, state); + } + const pushDescriptor = getOwnPropertyDescriptor(history, 'push'); + defineProperty(history, 'push', { + ...pushDescriptor, + value: newPush.bind(history), + }); + const oldListen = history.listen.bind(history); + function newListen(listener) { + return oldListen((location, action) => { + const res = listener( + location, + action, + this._programmaticPushLength, + ); + this._programmaticPushLength = undefined; + return res; + }); + } + const _listenDescriptor = getOwnPropertyDescriptor(history, 'listen'); + defineProperty(history, 'listen', { + ..._listenDescriptor, + value: newListen.bind(history), + }); + return history; + } + + constructor(history) { + this._history = URLParamRegistry._tagHistory(history); + } + + /** + * Adds a listener that dispatches the passed action creator whenever + * the passed URL query param changes. The value of the query param + * is passed as an argument to the action creator. + * @param {string} queryParam + * @param {function} actionCreator + */ + registerQueryParamListener(queryParam, actionCreator) { + const typeofQueryParam = typeof queryParam; + const typeofActionCreator = typeof actionCreator; + if(typeofQueryParam === 'string' && typeofActionCreator === 'function') { + const defaultVal = actionCreator().payload; + let currACObj = this._param2ActionCreatorsMap[queryParam]; + if(!currACObj) { + this._param2ActionCreatorsMap[queryParam] = { + defaultVal, + actionCreators: [], + }; + currACObj = this._param2ActionCreatorsMap[queryParam]; + } + const currACSet = new Set(currACObj.actionCreators); + if(!currACSet.has(actionCreator)) { + currACObj.actionCreators.push(actionCreator); + } + } else if(__DEV__) { + throw new Error( + 'Expected (, ), received ' + + `(<${typeofQueryParam}>, <${typeofActionCreator}>).` + ); + } + return this; + } + + /** + * Adds a listener that sets the URL query param to the payload value of + * dispatced action + * @param {string} queryParam + * @param {function} actionCreator + */ + registerActionListener(queryParam, actionCreator) { + const typeofQueryParam = typeof queryParam; + const typeofActionCreator = typeof actionCreator; + if(typeofQueryParam === 'string' && typeofActionCreator === 'function') { + const { type: actionType, payload: defaultVal } = actionCreator(); + const typeofActionType = typeof actionType; + if(typeofActionType === 'string') { + let currParamsObj = this._actionType2ParamsMap[actionType]; + if(!currParamsObj) { + this._actionType2ParamsMap[actionType] = { + defaultVal, + params: [], + }; + currParamsObj = this._actionType2ParamsMap[actionType]; + } + if(!(queryParam in currParamsObj.params)) { + currParamsObj.params.push(queryParam); + } + } else if(__DEV__) { + throw new Error( + 'Expected `actionCreator().type` to be type , ' + + `instead got type <${typeofActionType}>.` + ); + } + } else if(__DEV__) { + throw new Error( + 'Expected (, ), received ' + + `(<${typeofQueryParam}>, <${typeofActionCreator}>).` + ); + } + return this; + } + + /** + * @param {string} queryParam + * @param {function} actionCreator + */ + registerBidirectionalListener(queryParam, actionCreator) { + const typeofQueryParam = typeof queryParam; + const typeofActionCreator = typeof actionCreator; + if(typeofQueryParam === 'string' && typeofActionCreator === 'function') { + this.registerActionListener(queryParam, actionCreator); + this.registerQueryParamListener(queryParam, actionCreator); + } else if(__DEV__) { + throw new Error( + 'Expected (, ), received ' + + `(<${typeofQueryParam}>, <${typeofActionCreator}>).` + ); + } + return this; + } + + /** + * This should creates the middleware that should be passed to the store + * creator. + */ + createMiddleware() { + return store => { + this._unlistenHistory = this._history.listen( + this._createHistoryListener(store) + ); + return next => { + return action => { + this._actionInterceptor(action); + next(action); + }; + }; + }; + } +} diff --git a/testplan/web_ui/testing/src/Common/__tests__/uriComponentCodec.test.js b/testplan/web_ui/testing/src/Common/__tests__/uriComponentCodec.test.js new file mode 100644 index 000000000..c6479411f --- /dev/null +++ b/testplan/web_ui/testing/src/Common/__tests__/uriComponentCodec.test.js @@ -0,0 +1,38 @@ +import uriComponentCodec from '../uriComponentCodec'; +import { reverseMap } from '../utils'; + +const reserved2PctEncodedMap = new Map([ + [ 'Dow is down 70%', 'Dow is down 70%25' ], + [ 'è stato bene?', 'è stato bene%3f' ], + [ '#woke', '%23woke' ], + [ 'buy:5', 'buy%3a5' ], + [ 'In / Out', 'In %2f Out' ], + [ '@realDonaldTrump', '%40realDonaldTrump' ], + [ '>=]', '>%3d%5d' ], + [ '<:-[', '<%3a-%5b' ], + [ 'help!', 'help%21' ], + [ '$20', '%2420' ], + [ '&ref', '%26ref' ], + [ "'ciao'", '%27ciao%27' ], + [ '(secret)', '%28secret%29' ], + [ '1=="1"', '1%3d%3d"1"' ], + [ 'end;', 'end%3b' ], + [ '+3', '%2b3' ], + [ '*ptr', '%2aptr' ], + [ 'first,second', 'first%2csecond' ], +]); +const pctEncoded2ReservedMap = reverseMap(reserved2PctEncodedMap); + +describe('uriComponentCodec', () => { + it('checks that the mock component maps are 1-1 reversible', () => { + expect(reserved2PctEncodedMap.size).toBe(pctEncoded2ReservedMap.size); + }); + it.each(Array.from(reserved2PctEncodedMap))( + '(%#) correctly encodes "%s" to "%s"', (raw, encoded) => { + expect(uriComponentCodec.encode(raw)).toBe(encoded); + }); + it.each(Array.from(pctEncoded2ReservedMap))( + '(%#) correctly decodes "%s" to "%s"', (encoded, raw) => { + expect(uriComponentCodec.decode(encoded)).toBe(raw); + }); +}); diff --git a/testplan/web_ui/testing/src/Common/__tests__/utils.test.js b/testplan/web_ui/testing/src/Common/__tests__/utils.test.js index 0c8a9a38a..8f6d7e2ef 100644 --- a/testplan/web_ui/testing/src/Common/__tests__/utils.test.js +++ b/testplan/web_ui/testing/src/Common/__tests__/utils.test.js @@ -1,35 +1,25 @@ -import React from 'react'; - -import {NAV_ENTRY_DISPLAY_DATA} from "../defaults"; -import {getNavEntryDisplayData} from '../utils'; +import { NAV_ENTRY_DISPLAY_DATA } from '../defaults'; +import { getNavEntryDisplayData } from '../utils'; describe('Common/utils', () => { - describe('getNavEntryDisplayData', () => { - it('returns an empty object when given an empty object.', () => { const displayData = getNavEntryDisplayData({}); - for (const attribute of NAV_ENTRY_DISPLAY_DATA) { expect(displayData.hasOwnProperty(attribute)).toBeFalsy(); } }); - - it('returns an object with the expected keys when given an object with them.', () => { - let index = 0; - let entry = {}; - for (const attribute of NAV_ENTRY_DISPLAY_DATA) { - entry[attribute] = index; - index++; + it( + 'returns an object with the expected keys when given an object with them', + () => { + const entry = Object.fromEntries( + NAV_ENTRY_DISPLAY_DATA.concat('unknown').map((val, i) => [val, i]) + ); + const displayData = getNavEntryDisplayData(entry); + for (const attribute of NAV_ENTRY_DISPLAY_DATA) { + expect(displayData[attribute]).toEqual(entry[attribute]); + } } - entry['unknown'] = index; - - const displayData = getNavEntryDisplayData(entry); - for (const attribute of NAV_ENTRY_DISPLAY_DATA) { - expect(displayData[attribute]).toEqual(entry[attribute]); - } - }) - + ); }); - }); diff --git a/testplan/web_ui/testing/src/Common/defaults.js b/testplan/web_ui/testing/src/Common/defaults.js index baaf8a2f2..bd5ac40f0 100644 --- a/testplan/web_ui/testing/src/Common/defaults.js +++ b/testplan/web_ui/testing/src/Common/defaults.js @@ -1,24 +1,22 @@ -/** - * Constants used across the entire application. - */ -const GREEN = '#228F1D'; -const DARK_GREEN = '#1A721D'; -const RED = '#A2000C'; -const DARK_RED = '#840008'; -const ORANGE = '#FFA500'; -const DARK_ORANGE = '#DD8800'; -const LIGHT_GREY = '#F3F3F3'; -const MEDIUM_GREY = '#D0D0D0'; -const DARK_GREY = '#ADADAD'; -const BLACK = '#404040'; - -const COLUMN_WIDTH = 22; // unit: em -const MIN_COLUMN_WIDTH = 180; // unit: px -const INTERACTIVE_COL_WIDTH = 28; // wider to fit interactive buttons - -const INDENT_MULTIPLIER = 1.5; - -const TOOLBAR_BUTTONS_BATCH = [ +/** Constants used across the entire application. */ +export const GREEN = '#228F1D'; +export const DARK_GREEN = '#1A721D'; +export const RED = '#A2000C'; +export const DARK_RED = '#840008'; +export const ORANGE = '#FFA500'; +export const DARK_ORANGE = '#DD8800'; +export const LIGHT_GREY = '#F3F3F3'; +export const MEDIUM_GREY = '#D0D0D0'; +export const DARK_GREY = '#ADADAD'; +export const BLACK = '#404040'; + +export const BOTTOMMOST_ENTRY_CATEGORY = 'testcase'; +export const COLUMN_WIDTH = 22; // unit: em +export const MIN_COLUMN_WIDTH = 180; // unit: px +export const INTERACTIVE_COL_WIDTH = 28; // wider to fit interactive buttons +export const INDENT_MULTIPLIER = 1.5; + +export const TOOLBAR_BUTTONS_BATCH = [ {name: 'cl', type: 'clock'}, {name: 'pr', type: 'print'}, {name: 'if', type: 'info-circle'}, @@ -31,7 +29,7 @@ const TOOLBAR_BUTTONS_BATCH = [ {name: 'mo', type: 'chart-bar'} ]; -const TOOLBAR_BUTTONS_INTERACTIVE = [ +export const TOOLBAR_BUTTONS_INTERACTIVE = [ {name: 'po', type: 'fa-power-off'}, {name: 'bg', type: 'fa-bug'}, {name: 'rf', type: 'fa-refresh'}, @@ -40,7 +38,7 @@ const TOOLBAR_BUTTONS_INTERACTIVE = [ {name: 'fp', type: 'fa-floppy-o'} ]; -const CATEGORIES = { +export const CATEGORIES = { 'test': 'test', 'multitest': 'test', 'cppunit': 'test', @@ -56,7 +54,7 @@ const CATEGORIES = { 'testcase': 'testcase' }; -const CATEGORY_ICONS = { +export const CATEGORY_ICONS = { 'testplan': 'TP', 'test': 'T', 'multitest': 'MT', @@ -73,7 +71,7 @@ const CATEGORY_ICONS = { 'testcase': 'C' }; -const ENTRY_TYPES = [ +export const ENTRY_TYPES = [ 'testplan', 'multitest', 'gtest', @@ -86,7 +84,7 @@ const ENTRY_TYPES = [ 'testcase', ]; -const STATUS = [ +export const STATUS = [ 'error', 'failed', 'passed', @@ -101,7 +99,7 @@ const STATUS = [ 'unknown', ]; -const STATUS_CATEGORY = { +export const STATUS_CATEGORY = { 'error': 'error', 'failed': 'failed', 'incomplete': 'failed', @@ -114,14 +112,14 @@ const STATUS_CATEGORY = { 'unknown': 'unknown', }; -const RUNTIME_STATUS = [ +export const RUNTIME_STATUS = [ 'ready', 'running', 'finished', ]; -const NAV_ENTRY_DISPLAY_DATA = [ +export const NAV_ENTRY_DISPLAY_DATA = [ 'name', 'uid', 'type', @@ -134,7 +132,7 @@ const NAV_ENTRY_DISPLAY_DATA = [ 'logs', ]; -const BASIC_ASSERTION_TYPES = [ +export const BASIC_ASSERTION_TYPES = [ 'Log', 'Equal', 'NotEqual', 'Greater', 'GreaterEqual', 'Less', 'LessEqual', 'IsClose', 'IsTrue', 'IsFalse', @@ -149,7 +147,7 @@ const BASIC_ASSERTION_TYPES = [ 'RawAssertion', ]; -const SORT_TYPES = { +export const SORT_TYPES = { NONE: 0, ALPHABETICAL: 1, REVERSE_ALPHABETICAL: 2, @@ -157,7 +155,7 @@ const SORT_TYPES = { ONLY_FAILURES: 4, }; -const DICT_GRID_STYLE = { +export const DICT_GRID_STYLE = { MAX_VISIBLE_ROW: 20, ROW_HEIGHT: 28, EMPTY_ROW_HEIGHT: 5, @@ -171,34 +169,4 @@ const DICT_GRID_STYLE = { // NOTE: currently we poll for updates using HTTP for simplicity but in future // it might be better to use websockets or SSEs to allow the backend to notify // us when updates are available. -const POLL_MS = 1000; - -export { - GREEN, - DARK_GREEN, - RED, - DARK_RED, - ORANGE, - DARK_ORANGE, - LIGHT_GREY, - MEDIUM_GREY, - DARK_GREY, - BLACK, - COLUMN_WIDTH, - MIN_COLUMN_WIDTH, - INTERACTIVE_COL_WIDTH, - INDENT_MULTIPLIER, - TOOLBAR_BUTTONS_BATCH, - TOOLBAR_BUTTONS_INTERACTIVE, - CATEGORIES, - CATEGORY_ICONS, - ENTRY_TYPES, - STATUS, - STATUS_CATEGORY, - RUNTIME_STATUS, - NAV_ENTRY_DISPLAY_DATA, - BASIC_ASSERTION_TYPES, - SORT_TYPES, - DICT_GRID_STYLE, - POLL_MS, -}; +export const POLL_MS = 1000; diff --git a/testplan/web_ui/testing/src/Common/fakeReport.js b/testplan/web_ui/testing/src/Common/fakeReport.js deleted file mode 100644 index 1f4c93816..000000000 --- a/testplan/web_ui/testing/src/Common/fakeReport.js +++ /dev/null @@ -1,3938 +0,0 @@ -/** - * Sample Testplan reports to be used in development & testing. - */ -const TESTPLAN_REPORT = { - "name": "Sample Testplan", - "status": "failed", - "uid": "520a92e4-325e-4077-93e6-55d7091a3f83", - "tags_index": {}, - "information": [ - [ - "user", - "unknown" - ], - [ - "command_line_string", - "/home/unknown/path_to_testplan_script/testplan.py" - ], - ], - "status_override": null, - "meta": {}, - "timer": { - "run": { - "start": "2018-10-15T14:30:10.998071+00:00", - "end": "2018-10-15T14:30:11.296158+00:00" - } - }, - "entries": [ - { - "name": "Primary", - "status": "failed", - "category": "multitest", - "description": null, - "status_override": null, - "uid": "21739167-b30f-4c13-a315-ef6ae52fd1f7", - "type": "TestGroupReport", - "logs": [], - "tags": { - "simple": ["server"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.009705+00:00", - "end": "2018-10-15T14:30:11.159661+00:00" - } - }, - "entries": [ - { - "status": "failed", - "category": "testsuite", - "name": "AlphaSuite", - "status_override": null, - "description": "This is a failed testsuite", - "uid": "cb144b10-bdb0-44d3-9170-d8016dd19ee7", - "type": "TestGroupReport", - "logs": [], - "tags": { - "simple": ["server"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.009872+00:00", - "end": "2018-10-15T14:30:11.158224+00:00" - } - }, - "entries": [ - { - "name": "test_equality_passing", - "category": "testcase", - "status": "passed", - "status_override": null, - "description": "A testcase example", - "uid": "736706ef-ba65-475d-96c5-f2855f431028", - "type": "TestCaseReport", - "logs": [], - "tags": { - "colour": ["white"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.010072+00:00", - "end": "2018-10-15T14:30:11.132214+00:00" - } - }, - "entries": [ - { - "category": "DEFAULT", - "machine_time": "2018-10-15T15:30:11.010098+00:00", - "description": "passing equality", - "line_no": 24, - "label": "==", - "second": 1, - "meta_type": "assertion", - "passed": true, - "type": "Equal", - "utc_time": "2018-10-15T14:30:11.010094+00:00", - "first": 1 - } - ], - }, - { - "name": "test_equality_passing2", - "category": "testcase", - "status": "failed", - "tags": {}, - "status_override": null, - "description": null, - "uid": "78686a4d-7b94-4ae6-ab50-d9960a7fb714", - "type": "TestCaseReport", - "logs": [], - "timer": { - "run": { - "start": "2018-10-15T14:30:11.510072+00:00", - "end": "2018-10-15T14:30:11.632214+00:00" - } - }, - "entries": [ - { - "category": "DEFAULT", - "machine_time": "2018-10-15T15:30:11.510098+00:00", - "description": "passing equality", - "line_no": 24, - "label": "==", - "second": 1, - "meta_type": "assertion", - "passed": true, - "type": "Equal", - "utc_time": "2018-10-15T14:30:11.510094+00:00", - "first": 1 - } - ], - }, - ], - }, - { - "status": "passed", - "category": "testsuite", - "name": "BetaSuite", - "status_override": null, - "description": null, - "uid": "6fc5c008-4d1a-454e-80b6-74bdc9bca49e", - "type": "TestGroupReport", - "logs": [], - "tags": { - "simple": ["client"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.009872+00:00", - "end": "2018-10-15T14:30:11.158224+00:00" - } - }, - "entries": [ - { - "name": "test_equality_passing", - "category": "testcase", - "status": "passed", - "tags": {}, - "status_override": null, - "description": null, - "uid": "8865a23d-1823-4c8d-ab37-58d24fc8ac05", - "type": "TestCaseReport", - "logs": [], - "timer": { - "run": { - "start": "2018-10-15T14:30:11.010072+00:00", - "end": "2018-10-15T14:30:11.132214+00:00" - } - }, - "entries": [ - { - "category": "DEFAULT", - "machine_time": "2018-10-15T15:30:11.010098+00:00", - "description": "passing equality", - "line_no": 24, - "label": "==", - "second": 1, - "meta_type": "assertion", - "passed": true, - "type": "Equal", - "utc_time": "2018-10-15T14:30:11.010094+00:00", - "first": 1 - } - ], - }, - ], - }, - ], - }, - { - "name": "Secondary", - "status": "passed", - "category": "multitest", - "tags": {}, - "description": null, - "status_override": null, - "uid": "8c3c7e6b-48e8-40cd-86db-8c8aed2592c8", - "type": "TestGroupReport", - "logs": [], - "timer": { - "run": { - "start": "2018-10-15T14:30:12.009705+00:00", - "end": "2018-10-15T14:30:12.159661+00:00" - } - }, - "entries": [ - { - "status": "passed", - "category": "testsuite", - "name": "GammaSuite", - "tags": {}, - "status_override": null, - "description": null, - "uid": "08d4c671-d55d-49d4-96ba-dc654d12be26", - "type": "TestGroupReport", - "logs": [], - "timer": { - "run": { - "start": "2018-10-15T14:30:12.009872+00:00", - "end": "2018-10-15T14:30:12.158224+00:00" - } - }, - "entries": [ - { - "name": "test_equality_passing", - "category": "testcase", - "status": "passed", - "tags": {}, - "status_override": null, - "description": null, - "uid": "f73bd6ea-d378-437b-a5db-00d9e427f36a", - "type": "TestCaseReport", - "logs": [], - "timer": { - "run": { - "start": "2018-10-15T14:30:12.010072+00:00", - "end": "2018-10-15T14:30:12.132214+00:00" - } - }, - "entries": [ - { - "category": "DEFAULT", - "machine_time": "2018-10-15T15:30:12.010098+00:00", - "description": "passing equality", - "line_no": 24, - "label": "==", - "second": 1, - "meta_type": "assertion", - "passed": true, - "type": "Equal", - "utc_time": "2018-10-15T14:30:12.010094+00:00", - "first": 1 - } - ], - }, - ], - } - ], - }, - ], -}; - -var fakeReportAssertions = { - "category": "testplan", - "tags_index": {}, - "meta": {}, - "information": [ - [ - "user", - "yifan" - ], - [ - "command_line_string", - "oss/examples/Assertions/Basic/test_plan.py --json example.json" - ], - [ - "python_version", - "3.7.1" - ] - ], - "counter": { - "passed": 2, - "failed": 6, - "total": 8 - }, - "uid": "c648a283-22f3-4503-ae6d-c982b4c7cca0", - "attachments": {}, - "status": "failed", - "timer": { - "run": { - "end": "2020-01-10T03:06:59.348924+00:00", - "start": "2020-01-10T03:06:58.537339+00:00" - } - }, - "runtime_status": "finished", - "name": "Assertions Example", - "status_override": null, - "entries": [ - { - "description": null, - "counter": { - "passed": 2, - "failed": 6, - "total": 8 - }, - "name": "Assertions Test", - "tags": {}, - "env_status": "STOPPED", - "type": "TestGroupReport", - "status_reason": null, - "runtime_status": "finished", - "fix_spec_path": null, - "part": null, - "uid": "99aef9f5-6957-4842-a6fa-e0cd9e358473", - "status": "failed", - "parent_uids": [ - "Assertions Example" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.141338+00:00", - "start": "2020-01-10T03:06:58.629871+00:00" - } - }, - "hash": 3697482064019099674, - "status_override": null, - "logs": [], - "category": "multitest", - "entries": [ - { - "description": null, - "counter": { - "passed": 2, - "failed": 6, - "total": 8 - }, - "name": "SampleSuite", - "tags": {}, - "env_status": null, - "type": "TestGroupReport", - "status_reason": null, - "runtime_status": "finished", - "fix_spec_path": null, - "part": null, - "uid": "9f98c732-d040-4a13-84e1-563adcd9dd32", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.135813+00:00", - "start": "2020-01-10T03:06:58.629972+00:00" - } - }, - "hash": -4958192469702756289, - "status_override": null, - "logs": [], - "category": "testsuite", - "entries": [ - { - "category": "testcase", - "logs": [], - "description": "Description test\nSecond description", - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "25d0115f-91c4-481b-ad0f-37382d95fabd", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:58.939142+00:00", - "start": "2020-01-10T03:06:58.630091+00:00" - } - }, - "hash": 4069384282795794238, - "runtime_status": "finished", - "name": "test_basic_assertions", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "==", - "type": "Equal", - "utc_time": "2020-01-10T03:06:58.630121+00:00", - "second": "foo", - "passed": true, - "first": "foo", - "machine_time": "2020-01-10T11:06:58.630129+00:00", - "line_no": 25 - }, - { - "category": "DEFAULT", - "description": "Description for failing equality", - "meta_type": "assertion", - "label": "==", - "type": "Equal", - "utc_time": "2020-01-10T03:06:58.893461+00:00", - "second": 2, - "passed": false, - "first": 1, - "machine_time": "2020-01-10T11:06:58.893477+00:00", - "line_no": 28 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "!=", - "type": "NotEqual", - "utc_time": "2020-01-10T03:06:58.895795+00:00", - "second": "bar", - "passed": true, - "first": "foo", - "machine_time": "2020-01-10T11:06:58.895806+00:00", - "line_no": 30 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">", - "type": "Greater", - "utc_time": "2020-01-10T03:06:58.898075+00:00", - "second": 2, - "passed": true, - "first": 5, - "machine_time": "2020-01-10T11:06:58.898084+00:00", - "line_no": 31 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">=", - "type": "GreaterEqual", - "utc_time": "2020-01-10T03:06:58.899619+00:00", - "second": 2, - "passed": true, - "first": 2, - "machine_time": "2020-01-10T11:06:58.899627+00:00", - "line_no": 32 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">=", - "type": "GreaterEqual", - "utc_time": "2020-01-10T03:06:58.901156+00:00", - "second": 1, - "passed": true, - "first": 2, - "machine_time": "2020-01-10T11:06:58.901163+00:00", - "line_no": 33 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "<", - "type": "Less", - "utc_time": "2020-01-10T03:06:58.902604+00:00", - "second": 20, - "passed": true, - "first": 10, - "machine_time": "2020-01-10T11:06:58.902613+00:00", - "line_no": 34 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "<=", - "type": "LessEqual", - "utc_time": "2020-01-10T03:06:58.904109+00:00", - "second": 10, - "passed": true, - "first": 10, - "machine_time": "2020-01-10T11:06:58.904117+00:00", - "line_no": 35 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "<=", - "type": "LessEqual", - "utc_time": "2020-01-10T03:06:58.905543+00:00", - "second": 30, - "passed": true, - "first": 10, - "machine_time": "2020-01-10T11:06:58.905550+00:00", - "line_no": 36 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "==", - "type": "Equal", - "utc_time": "2020-01-10T03:06:58.906994+00:00", - "second": 15, - "passed": true, - "first": 15, - "machine_time": "2020-01-10T11:06:58.907002+00:00", - "line_no": 41 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "!=", - "type": "NotEqual", - "utc_time": "2020-01-10T03:06:58.908433+00:00", - "second": 20, - "passed": true, - "first": 10, - "machine_time": "2020-01-10T11:06:58.908440+00:00", - "line_no": 42 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "<", - "type": "Less", - "utc_time": "2020-01-10T03:06:58.909946+00:00", - "second": 3, - "passed": true, - "first": 2, - "machine_time": "2020-01-10T11:06:58.909954+00:00", - "line_no": 43 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">", - "type": "Greater", - "utc_time": "2020-01-10T03:06:58.911441+00:00", - "second": 2, - "passed": true, - "first": 3, - "machine_time": "2020-01-10T11:06:58.911449+00:00", - "line_no": 44 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": "<=", - "type": "LessEqual", - "utc_time": "2020-01-10T03:06:58.912920+00:00", - "second": 15, - "passed": true, - "first": 10, - "machine_time": "2020-01-10T11:06:58.912928+00:00", - "line_no": 45 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">=", - "type": "GreaterEqual", - "utc_time": "2020-01-10T03:06:58.914465+00:00", - "second": 10, - "passed": true, - "first": 15, - "machine_time": "2020-01-10T11:06:58.914473+00:00", - "line_no": 46 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "rel_tol": 0.1, - "label": "~=", - "type": "IsClose", - "utc_time": "2020-01-10T03:06:58.915976+00:00", - "second": 95, - "abs_tol": 0.0, - "passed": true, - "first": 100, - "machine_time": "2020-01-10T11:06:58.915984+00:00", - "line_no": 50 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "rel_tol": 0.01, - "label": "~=", - "type": "IsClose", - "utc_time": "2020-01-10T03:06:58.917481+00:00", - "second": 95, - "abs_tol": 0.0, - "passed": false, - "first": 100, - "machine_time": "2020-01-10T11:06:58.917489+00:00", - "line_no": 51 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "entry", - "type": "Log", - "utc_time": "2020-01-10T03:06:58.919181+00:00", - "machine_time": "2020-01-10T11:06:58.919189+00:00", - "line_no": 56, - "message": "This is a log message, it will be displayed along with other assertion details." - }, - { - "category": "DEFAULT", - "description": "Boolean Truthiness check", - "meta_type": "assertion", - "type": "IsTrue", - "utc_time": "2020-01-10T03:06:58.921013+00:00", - "expr": true, - "passed": true, - "machine_time": "2020-01-10T11:06:58.921021+00:00", - "line_no": 61 - }, - { - "category": "DEFAULT", - "description": "Boolean Falseness check", - "meta_type": "assertion", - "type": "IsFalse", - "utc_time": "2020-01-10T03:06:58.923056+00:00", - "expr": false, - "passed": true, - "machine_time": "2020-01-10T11:06:58.923064+00:00", - "line_no": 62 - }, - { - "category": "DEFAULT", - "description": "This is an explicit failure.", - "meta_type": "assertion", - "type": "Fail", - "utc_time": "2020-01-10T03:06:58.924595+00:00", - "passed": false, - "machine_time": "2020-01-10T11:06:58.924621+00:00", - "line_no": 64 - }, - { - "category": "DEFAULT", - "description": "Passing membership", - "meta_type": "assertion", - "type": "Contain", - "utc_time": "2020-01-10T03:06:58.926405+00:00", - "container": "foobar", - "passed": true, - "machine_time": "2020-01-10T11:06:58.926413+00:00", - "line_no": 67, - "member": "foo" - }, - { - "category": "DEFAULT", - "description": "Failing membership", - "meta_type": "assertion", - "type": "NotContain", - "utc_time": "2020-01-10T03:06:58.928507+00:00", - "container": "{'a': 1, 'b': 2}", - "passed": true, - "machine_time": "2020-01-10T11:06:58.928515+00:00", - "line_no": 71, - "member": 10 - }, - { - "category": "DEFAULT", - "description": "Comparison of slices", - "meta_type": "assertion", - "type": "EqualSlices", - "utc_time": "2020-01-10T03:06:58.930479+00:00", - "data": [ - [ - "slice(2, 4, None)", - [ - 2, - 3 - ], - [], - [ - 3, - 4 - ], - [ - 3, - 4 - ] - ], - [ - "slice(6, 8, None)", - [ - 6, - 7 - ], - [], - [ - 7, - 8 - ], - [ - 7, - 8 - ] - ] - ], - "passed": true, - "included_indices": [], - "machine_time": "2020-01-10T11:06:58.930488+00:00", - "expected": [ - "a", - "b", - 3, - 4, - "c", - "d", - 7, - 8 - ], - "line_no": 79, - "actual": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ] - }, - { - "category": "DEFAULT", - "description": "Comparison of slices (exclusion)", - "meta_type": "assertion", - "type": "EqualExcludeSlices", - "utc_time": "2020-01-10T03:06:58.932694+00:00", - "data": [ - [ - "slice(0, 2, None)", - [ - 2, - 3, - 4, - 5, - 6, - 7 - ], - [ - 4, - 5, - 6, - 7 - ], - [ - 3, - 4, - 5, - 6, - 7, - 8 - ], - [ - 3, - 4, - "c", - "d", - "e", - "f" - ] - ], - [ - "slice(4, 8, None)", - [ - 0, - 1, - 2, - 3 - ], - [ - 0, - 1 - ], - [ - 1, - 2, - 3, - 4 - ], - [ - "a", - "b", - 3, - 4 - ] - ] - ], - "passed": true, - "included_indices": [ - 2, - 3 - ], - "machine_time": "2020-01-10T11:06:58.932703+00:00", - "expected": [ - "a", - "b", - 3, - 4, - "c", - "d", - "e", - "f" - ], - "line_no": 91, - "actual": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ] - }, - { - "unified": false, - "category": "DEFAULT", - "ignore_blank_lines": true, - "description": null, - "meta_type": "assertion", - "type": "LineDiff", - "utc_time": "2020-01-10T03:06:58.934779+00:00", - "delta": [], - "second": [ - "abc\n", - "xyz\n", - "\n" - ], - "context": false, - "passed": true, - "first": [ - "abc\n", - "xyz\n" - ], - "machine_time": "2020-01-10T11:06:58.934786+00:00", - "ignore_space_change": false, - "line_no": 98, - "ignore_whitespaces": false - }, - { - "unified": 3, - "category": "DEFAULT", - "ignore_blank_lines": false, - "description": null, - "meta_type": "assertion", - "type": "LineDiff", - "utc_time": "2020-01-10T03:06:58.936975+00:00", - "delta": [], - "second": [ - "1\n", - "1\n", - "1\n", - "abc \n", - "xy\t\tz\n", - "2\n", - "2\n", - "2\n" - ], - "context": false, - "passed": true, - "first": [ - "1\r\n", - "1\r\n", - "1\r\n", - "abc\r\n", - "xy z\r\n", - "2\r\n", - "2\r\n", - "2\r\n" - ], - "machine_time": "2020-01-10T11:06:58.936983+00:00", - "ignore_space_change": true, - "line_no": 102, - "ignore_whitespaces": false - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 1, - "failed": 0, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "cd31b565-3702-4540-a140-ff9fd480e8ce", - "status": "passed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:58.963478+00:00", - "start": "2020-01-10T03:06:58.954190+00:00" - } - }, - "hash": -6066149844839810607, - "runtime_status": "finished", - "name": "test_raised_exceptions", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": null, - "type": "ExceptionRaised", - "utc_time": "2020-01-10T03:06:58.954270+00:00", - "func_match": true, - "raised_exception": [ - "", - "'bar'" - ], - "exception_match": true, - "expected_exceptions": [ - "KeyError" - ], - "passed": true, - "pattern_match": true, - "machine_time": "2020-01-10T11:06:58.954275+00:00", - "func": null, - "line_no": 112 - }, - { - "category": "DEFAULT", - "description": "Exception raised with custom pattern.", - "meta_type": "assertion", - "pattern": "foobar", - "type": "ExceptionRaised", - "utc_time": "2020-01-10T03:06:58.955863+00:00", - "func_match": true, - "raised_exception": [ - "", - "abc foobar xyz" - ], - "exception_match": true, - "expected_exceptions": [ - "ValueError" - ], - "passed": true, - "pattern_match": true, - "machine_time": "2020-01-10T11:06:58.955871+00:00", - "func": null, - "line_no": 121 - }, - { - "category": "DEFAULT", - "description": "Exception raised with custom func.", - "meta_type": "assertion", - "pattern": null, - "type": "ExceptionRaised", - "utc_time": "2020-01-10T03:06:58.957489+00:00", - "func_match": true, - "raised_exception": [ - ".MyException'>", - "4" - ], - "exception_match": true, - "expected_exceptions": [ - "MyException" - ], - "passed": true, - "pattern_match": true, - "machine_time": "2020-01-10T11:06:58.957497+00:00", - "func": ".custom_func at 0x7f9cfc64fea0>", - "line_no": 139 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": null, - "type": "ExceptionNotRaised", - "utc_time": "2020-01-10T03:06:58.958956+00:00", - "func_match": true, - "raised_exception": [ - "", - "'bar'" - ], - "exception_match": false, - "expected_exceptions": [ - "TypeError" - ], - "passed": true, - "pattern_match": true, - "machine_time": "2020-01-10T11:06:58.958964+00:00", - "func": null, - "line_no": 146 - }, - { - "category": "DEFAULT", - "description": "Exception not raised with custom pattern.", - "meta_type": "assertion", - "pattern": "foobar", - "type": "ExceptionNotRaised", - "utc_time": "2020-01-10T03:06:58.960503+00:00", - "func_match": true, - "raised_exception": [ - "", - "abc" - ], - "exception_match": true, - "expected_exceptions": [ - "ValueError" - ], - "passed": true, - "pattern_match": null, - "machine_time": "2020-01-10T11:06:58.960510+00:00", - "func": null, - "line_no": 157 - }, - { - "category": "DEFAULT", - "description": "Exception not raised with custom func.", - "meta_type": "assertion", - "pattern": null, - "type": "ExceptionNotRaised", - "utc_time": "2020-01-10T03:06:58.962023+00:00", - "func_match": false, - "raised_exception": [ - ".MyException'>", - "5" - ], - "exception_match": true, - "expected_exceptions": [ - "MyException" - ], - "passed": true, - "pattern_match": true, - "machine_time": "2020-01-10T11:06:58.962031+00:00", - "func": ".custom_func at 0x7f9cfc64fea0>", - "line_no": 165 - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "fca0596d-c220-4267-9a38-57968aca92d5", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:58.979777+00:00", - "start": "2020-01-10T03:06:58.971424+00:00" - } - }, - "hash": -2707574492059523373, - "runtime_status": "finished", - "name": "test_assertion_group -- very long long long long long long long longlong long long longlong long long long name", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": "Equality assertion outside the group", - "meta_type": "assertion", - "label": "==", - "type": "Equal", - "utc_time": "2020-01-10T03:06:58.971447+00:00", - "second": 1, - "passed": true, - "first": 1, - "machine_time": "2020-01-10T11:06:58.971451+00:00", - "line_no": 173 - }, - { - "description": "Custom group description", - "meta_type": "assertion", - "type": "Group", - "passed": false, - "entries": [ - { - "category": "DEFAULT", - "description": "Assertion within a group", - "meta_type": "assertion", - "label": "!=", - "type": "NotEqual", - "utc_time": "2020-01-10T03:06:58.973038+00:00", - "second": 3, - "passed": true, - "first": 2, - "machine_time": "2020-01-10T11:06:58.973047+00:00", - "line_no": 176 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "label": ">", - "type": "Greater", - "utc_time": "2020-01-10T03:06:58.974577+00:00", - "second": 3, - "passed": true, - "first": 5, - "machine_time": "2020-01-10T11:06:58.974586+00:00", - "line_no": 177 - }, - { - "description": "This is a sub group", - "meta_type": "assertion", - "type": "Group", - "passed": false, - "entries": [ - { - "category": "DEFAULT", - "description": "Assertion within sub group", - "meta_type": "assertion", - "label": "<", - "type": "Less", - "utc_time": "2020-01-10T03:06:58.976376+00:00", - "second": 3, - "passed": false, - "first": 6, - "machine_time": "2020-01-10T11:06:58.976384+00:00", - "line_no": 181 - } - ] - } - ] - }, - { - "category": "DEFAULT", - "description": "Final assertion outside all groups", - "meta_type": "assertion", - "label": "==", - "type": "Equal", - "utc_time": "2020-01-10T03:06:58.978219+00:00", - "second": "foo", - "passed": true, - "first": "foo", - "machine_time": "2020-01-10T11:06:58.978227+00:00", - "line_no": 184 - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "a3fd1023-b150-487a-bc7d-c0f64e326e63", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.006101+00:00", - "start": "2020-01-10T03:06:58.987035+00:00" - } - }, - "hash": -8719069130512673532, - "runtime_status": "finished", - "name": "test_regex_namespace", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": "string pattern match", - "meta_type": "assertion", - "pattern": "foo", - "type": "RegexMatch", - "utc_time": "2020-01-10T03:06:58.987140+00:00", - "match_indexes": [ - [ - 0, - 3 - ] - ], - "passed": true, - "string": "foobar", - "machine_time": "2020-01-10T11:06:58.987146+00:00", - "line_no": 196 - }, - { - "category": "DEFAULT", - "description": "SRE match", - "meta_type": "assertion", - "pattern": "foo", - "type": "RegexMatch", - "utc_time": "2020-01-10T03:06:58.988905+00:00", - "match_indexes": [ - [ - 0, - 3 - ] - ], - "passed": true, - "string": "foobar", - "machine_time": "2020-01-10T11:06:58.988913+00:00", - "line_no": 201 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "first line.*second", - "type": "RegexMatch", - "utc_time": "2020-01-10T03:06:58.991277+00:00", - "match_indexes": [ - [ - 0, - 17 - ] - ], - "passed": true, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:58.991285+00:00", - "line_no": 212 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "baz", - "type": "RegexMatchNotExists", - "utc_time": "2020-01-10T03:06:58.992937+00:00", - "match_indexes": [], - "passed": true, - "string": "foobar", - "machine_time": "2020-01-10T11:06:58.992945+00:00", - "line_no": 217 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "foobar", - "type": "RegexMatchNotExists", - "utc_time": "2020-01-10T03:06:58.994520+00:00", - "match_indexes": [], - "passed": true, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:58.994527+00:00", - "line_no": 222 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "second", - "type": "RegexSearch", - "utc_time": "2020-01-10T03:06:58.996148+00:00", - "match_indexes": [ - [ - 11, - 17 - ] - ], - "passed": true, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:58.996156+00:00", - "line_no": 225 - }, - { - "category": "DEFAULT", - "description": "Passing search empty", - "meta_type": "assertion", - "pattern": "foobar", - "type": "RegexSearchNotExists", - "utc_time": "2020-01-10T03:06:58.997760+00:00", - "match_indexes": [], - "passed": true, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:58.997768+00:00", - "line_no": 230 - }, - { - "category": "DEFAULT", - "description": "Failing search_empty", - "meta_type": "assertion", - "pattern": "second", - "type": "RegexSearchNotExists", - "utc_time": "2020-01-10T03:06:58.999296+00:00", - "match_indexes": [ - [ - 11, - 17 - ] - ], - "passed": false, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:58.999303+00:00", - "line_no": 233 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "foo", - "type": "RegexFindIter", - "utc_time": "2020-01-10T03:06:59.000852+00:00", - "match_indexes": [ - [ - 0, - 3 - ], - [ - 4, - 7 - ], - [ - 8, - 11 - ], - [ - 20, - 23 - ] - ], - "condition": "", - "passed": true, - "string": "foo foo foo bar bar foo bar", - "machine_time": "2020-01-10T11:06:59.000860+00:00", - "condition_match": true, - "line_no": 243 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "foo", - "type": "RegexFindIter", - "utc_time": "2020-01-10T03:06:59.002669+00:00", - "match_indexes": [ - [ - 0, - 3 - ], - [ - 4, - 7 - ], - [ - 8, - 11 - ], - [ - 20, - 23 - ] - ], - "condition": "(VAL > 2 and VAL < 5)", - "passed": true, - "string": "foo foo foo bar bar foo bar", - "machine_time": "2020-01-10T11:06:59.002676+00:00", - "condition_match": true, - "line_no": 250 - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "pattern": "\\w+ line$", - "type": "RegexMatchLine", - "utc_time": "2020-01-10T03:06:59.004622+00:00", - "match_indexes": [ - [ - 0, - 0, - 10 - ], - [ - 1, - 0, - 11 - ], - [ - 2, - 0, - 10 - ] - ], - "passed": true, - "string": "first line\nsecond line\nthird line", - "machine_time": "2020-01-10T11:06:59.004630+00:00", - "line_no": 257 - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "e8fb2848-cc83-4df3-83e0-82fe839d6526", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.072704+00:00", - "start": "2020-01-10T03:06:59.016322+00:00" - } - }, - "hash": -8829886055223884393, - "runtime_status": "finished", - "name": "test_table_namespace", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": "Table Match: list of list vs list of list", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.016418+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 0, - [ - "Bob", - 32 - ], - {}, - {}, - {} - ], - [ - 1, - [ - "Susan", - 24 - ], - {}, - {}, - {} - ], - [ - 2, - [ - "Rick", - 67 - ], - {}, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.016424+00:00", - "line_no": 284, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: list of dict vs list of dict", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.018525+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 0, - [ - "Bob", - 32 - ], - {}, - {}, - {} - ], - [ - 1, - [ - "Susan", - 24 - ], - {}, - {}, - {} - ], - [ - 2, - [ - "Rick", - 67 - ], - {}, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.018533+00:00", - "line_no": 289, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: list of dict vs list of list", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.020629+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 0, - [ - "Bob", - 32 - ], - {}, - {}, - {} - ], - [ - 1, - [ - "Susan", - 24 - ], - {}, - {}, - {} - ], - [ - 2, - [ - "Rick", - 67 - ], - {}, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.020640+00:00", - "line_no": 294, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: list of list vs list of list", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.023695+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [], - "report_fails_only": true, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.023703+00:00", - "line_no": 299, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: list of dict vs list of dict", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.026093+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [], - "report_fails_only": true, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.026102+00:00", - "line_no": 304, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: list of dict vs list of list", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.027835+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [], - "report_fails_only": true, - "passed": true, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.027843+00:00", - "line_no": 309, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: simple comparators", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.029541+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 0, - [ - "Bob", - 32 - ], - {}, - {}, - { - "name": "REGEX(\\w{3})", - "age": "" - } - ], - [ - 1, - [ - "Susan", - 24 - ], - {}, - {}, - {} - ], - [ - 2, - [ - "Rick", - 67 - ], - { - "name": "" - }, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.029549+00:00", - "line_no": 338, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: simple comparators", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.031666+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 2, - [ - "Rick", - 67 - ], - { - "name": "" - }, - {}, - {} - ] - ], - "report_fails_only": true, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.031674+00:00", - "line_no": 343, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: readable comparators", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.034598+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 0, - [ - "Bob", - 32 - ], - {}, - {}, - { - "name": "REGEX(\\w{3})", - "age": "(VAL > 30 and VAL < 40)" - } - ], - [ - 1, - [ - "Susan", - 24 - ], - {}, - {}, - {} - ], - [ - 2, - [ - "Rick", - 67 - ], - { - "name": "VAL in ['David', 'Helen', 'Pablo']" - }, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.034625+00:00", - "line_no": 361, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: readable comparators", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.037495+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "name", - "age" - ], - "data": [ - [ - 2, - [ - "Rick", - 67 - ], - { - "name": "VAL in ['David', 'Helen', 'Pablo']" - }, - {}, - {} - ] - ], - "report_fails_only": true, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.037502+00:00", - "line_no": 366, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: Trimmed columns", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.040045+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "column_1", - "column_2" - ], - "data": [ - [ - 0, - [ - 0, - 0 - ], - {}, - {}, - {} - ], - [ - 1, - [ - 1, - 2 - ], - {}, - {}, - {} - ], - [ - 2, - [ - 2, - 4 - ], - {}, - {}, - {} - ], - [ - 3, - [ - 3, - 6 - ], - {}, - {}, - {} - ], - [ - 4, - [ - 4, - 8 - ], - {}, - {}, - {} - ], - [ - 5, - [ - 5, - 10 - ], - {}, - {}, - {} - ], - [ - 6, - [ - 6, - 12 - ], - {}, - {}, - {} - ], - [ - 7, - [ - 7, - 14 - ], - {}, - {}, - {} - ], - [ - 8, - [ - 8, - 16 - ], - {}, - {}, - {} - ], - [ - 9, - [ - 9, - 18 - ], - {}, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": true, - "include_columns": [ - "column_1", - "column_2" - ], - "machine_time": "2020-01-10T11:06:59.040052+00:00", - "line_no": 383, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: Trimmed columns", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.042860+00:00", - "exclude_columns": null, - "fail_limit": 0, - "columns": [ - "column_1", - "column_2" - ], - "data": [], - "report_fails_only": true, - "passed": true, - "include_columns": [ - "column_1", - "column_2" - ], - "machine_time": "2020-01-10T11:06:59.042869+00:00", - "line_no": 391, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Match: Trimmed rows", - "meta_type": "assertion", - "type": "TableMatch", - "utc_time": "2020-01-10T03:06:59.046590+00:00", - "exclude_columns": null, - "fail_limit": 2, - "columns": [ - "amount", - "product_id" - ], - "data": [ - [ - 0, - [ - 0, - 4240 - ], - {}, - {}, - {} - ], - [ - 1, - [ - 10, - 3961 - ], - {}, - {}, - {} - ], - [ - 2, - [ - 20, - 1627 - ], - {}, - {}, - {} - ], - [ - 3, - [ - 30, - 1351 - ], - {}, - {}, - {} - ], - [ - 4, - [ - 40, - 2123 - ], - {}, - {}, - {} - ], - [ - 5, - [ - 25, - 1111 - ], - { - "amount": 35 - }, - {}, - {} - ], - [ - 6, - [ - 20, - 2222 - ], - { - "product_id": 1234 - }, - {}, - {} - ] - ], - "report_fails_only": false, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.046598+00:00", - "line_no": 428, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": "Table Diff: Trimmed rows", - "meta_type": "assertion", - "type": "TableDiff", - "utc_time": "2020-01-10T03:06:59.049590+00:00", - "exclude_columns": null, - "fail_limit": 2, - "columns": [ - "amount", - "product_id" - ], - "data": [ - [ - 5, - [ - 25, - 1111 - ], - { - "amount": 35 - }, - {}, - {} - ], - [ - 6, - [ - 20, - 2222 - ], - { - "product_id": 1234 - }, - {}, - {} - ] - ], - "report_fails_only": true, - "passed": false, - "include_columns": null, - "machine_time": "2020-01-10T11:06:59.049598+00:00", - "line_no": 437, - "message": null, - "strict": false - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "column": "symbol", - "limit": null, - "type": "ColumnContain", - "utc_time": "2020-01-10T03:06:59.051390+00:00", - "data": [ - [ - 0, - "AAPL", - true - ], - [ - 1, - "GOOG", - false - ], - [ - 2, - "FB", - false - ], - [ - 3, - "AMZN", - true - ], - [ - 4, - "MSFT", - false - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.051397+00:00", - "values": [ - "AAPL", - "AMZN" - ], - "line_no": 454, - "report_fails_only": false - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "column": "symbol", - "limit": 20, - "type": "ColumnContain", - "utc_time": "2020-01-10T03:06:59.057037+00:00", - "data": [ - [ - 1, - "GOOG", - false - ], - [ - 2, - "FB", - false - ], - [ - 4, - "MSFT", - false - ], - [ - 6, - "GOOG", - false - ], - [ - 7, - "FB", - false - ], - [ - 9, - "MSFT", - false - ], - [ - 11, - "GOOG", - false - ], - [ - 12, - "FB", - false - ], - [ - 14, - "MSFT", - false - ], - [ - 16, - "GOOG", - false - ], - [ - 17, - "FB", - false - ], - [ - 19, - "MSFT", - false - ], - [ - 21, - "GOOG", - false - ], - [ - 22, - "FB", - false - ], - [ - 24, - "MSFT", - false - ], - [ - 26, - "GOOG", - false - ], - [ - 27, - "FB", - false - ], - [ - 29, - "MSFT", - false - ], - [ - 31, - "GOOG", - false - ], - [ - 32, - "FB", - false - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.057048+00:00", - "values": [ - "AAPL", - "AMZN" - ], - "line_no": 467, - "report_fails_only": true - }, - { - "category": "DEFAULT", - "description": "Table Log: list of dicts", - "meta_type": "entry", - "type": "TableLog", - "utc_time": "2020-01-10T03:06:59.060012+00:00", - "table": [ - { - "name": "Bob", - "age": 32 - }, - { - "name": "Susan", - "age": 24 - }, - { - "name": "Rick", - "age": 67 - } - ], - "display_index": false, - "columns": [ - "name", - "age" - ], - "machine_time": "2020-01-10T11:06:59.060020+00:00", - "line_no": 472, - "indices": [ - 0, - 1, - 2 - ] - }, - { - "category": "DEFAULT", - "description": "Table Log: list of lists", - "meta_type": "entry", - "type": "TableLog", - "utc_time": "2020-01-10T03:06:59.061711+00:00", - "table": [ - { - "name": "Bob", - "age": 32 - }, - { - "name": "Susan", - "age": 24 - }, - { - "name": "Rick", - "age": 67 - } - ], - "display_index": false, - "columns": [ - "name", - "age" - ], - "machine_time": "2020-01-10T11:06:59.061718+00:00", - "line_no": 473, - "indices": [ - 0, - 1, - 2 - ] - }, - { - "category": "DEFAULT", - "description": "Table Log: many rows", - "meta_type": "entry", - "type": "TableLog", - "utc_time": "2020-01-10T03:06:59.063421+00:00", - "table": [ - { - "symbol": "AAPL", - "amount": 12 - }, - { - "symbol": "GOOG", - "amount": 21 - }, - { - "symbol": "FB", - "amount": 32 - }, - { - "symbol": "AMZN", - "amount": 5 - }, - { - "symbol": "MSFT", - "amount": 42 - }, - { - "symbol": "AAPL", - "amount": 12 - }, - { - "symbol": "GOOG", - "amount": 21 - }, - { - "symbol": "FB", - "amount": 32 - }, - { - "symbol": "AMZN", - "amount": 5 - }, - { - "symbol": "MSFT", - "amount": 42 - }, - { - "symbol": "AAPL", - "amount": 12 - }, - { - "symbol": "GOOG", - "amount": 21 - }, - { - "symbol": "FB", - "amount": 32 - }, - { - "symbol": "AMZN", - "amount": 5 - }, - { - "symbol": "MSFT", - "amount": 42 - }, - { - "symbol": "AAPL", - "amount": 12 - }, - { - "symbol": "GOOG", - "amount": 21 - }, - { - "symbol": "FB", - "amount": 32 - }, - { - "symbol": "AMZN", - "amount": 5 - }, - { - "symbol": "MSFT", - "amount": 42 - } - ], - "display_index": false, - "columns": [ - "symbol", - "amount" - ], - "machine_time": "2020-01-10T11:06:59.063429+00:00", - "line_no": 479, - "indices": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19 - ] - }, - { - "category": "DEFAULT", - "description": "Table Log: many columns", - "meta_type": "entry", - "type": "TableLog", - "utc_time": "2020-01-10T03:06:59.065884+00:00", - "table": [ - { - "col_0": "row 0 col 0", - "col_1": "row 0 col 1", - "col_2": "row 0 col 2", - "col_3": "row 0 col 3", - "col_4": "row 0 col 4", - "col_5": "row 0 col 5", - "col_6": "row 0 col 6", - "col_7": "row 0 col 7", - "col_8": "row 0 col 8", - "col_9": "row 0 col 9", - "col_10": "row 0 col 10", - "col_11": "row 0 col 11", - "col_12": "row 0 col 12", - "col_13": "row 0 col 13", - "col_14": "row 0 col 14", - "col_15": "row 0 col 15", - "col_16": "row 0 col 16", - "col_17": "row 0 col 17", - "col_18": "row 0 col 18", - "col_19": "row 0 col 19" - }, - { - "col_0": "row 1 col 0", - "col_1": "row 1 col 1", - "col_2": "row 1 col 2", - "col_3": "row 1 col 3", - "col_4": "row 1 col 4", - "col_5": "row 1 col 5", - "col_6": "row 1 col 6", - "col_7": "row 1 col 7", - "col_8": "row 1 col 8", - "col_9": "row 1 col 9", - "col_10": "row 1 col 10", - "col_11": "row 1 col 11", - "col_12": "row 1 col 12", - "col_13": "row 1 col 13", - "col_14": "row 1 col 14", - "col_15": "row 1 col 15", - "col_16": "row 1 col 16", - "col_17": "row 1 col 17", - "col_18": "row 1 col 18", - "col_19": "row 1 col 19" - }, - { - "col_0": "row 2 col 0", - "col_1": "row 2 col 1", - "col_2": "row 2 col 2", - "col_3": "row 2 col 3", - "col_4": "row 2 col 4", - "col_5": "row 2 col 5", - "col_6": "row 2 col 6", - "col_7": "row 2 col 7", - "col_8": "row 2 col 8", - "col_9": "row 2 col 9", - "col_10": "row 2 col 10", - "col_11": "row 2 col 11", - "col_12": "row 2 col 12", - "col_13": "row 2 col 13", - "col_14": "row 2 col 14", - "col_15": "row 2 col 15", - "col_16": "row 2 col 16", - "col_17": "row 2 col 17", - "col_18": "row 2 col 18", - "col_19": "row 2 col 19" - }, - { - "col_0": "row 3 col 0", - "col_1": "row 3 col 1", - "col_2": "row 3 col 2", - "col_3": "row 3 col 3", - "col_4": "row 3 col 4", - "col_5": "row 3 col 5", - "col_6": "row 3 col 6", - "col_7": "row 3 col 7", - "col_8": "row 3 col 8", - "col_9": "row 3 col 9", - "col_10": "row 3 col 10", - "col_11": "row 3 col 11", - "col_12": "row 3 col 12", - "col_13": "row 3 col 13", - "col_14": "row 3 col 14", - "col_15": "row 3 col 15", - "col_16": "row 3 col 16", - "col_17": "row 3 col 17", - "col_18": "row 3 col 18", - "col_19": "row 3 col 19" - }, - { - "col_0": "row 4 col 0", - "col_1": "row 4 col 1", - "col_2": "row 4 col 2", - "col_3": "row 4 col 3", - "col_4": "row 4 col 4", - "col_5": "row 4 col 5", - "col_6": "row 4 col 6", - "col_7": "row 4 col 7", - "col_8": "row 4 col 8", - "col_9": "row 4 col 9", - "col_10": "row 4 col 10", - "col_11": "row 4 col 11", - "col_12": "row 4 col 12", - "col_13": "row 4 col 13", - "col_14": "row 4 col 14", - "col_15": "row 4 col 15", - "col_16": "row 4 col 16", - "col_17": "row 4 col 17", - "col_18": "row 4 col 18", - "col_19": "row 4 col 19" - }, - { - "col_0": "row 5 col 0", - "col_1": "row 5 col 1", - "col_2": "row 5 col 2", - "col_3": "row 5 col 3", - "col_4": "row 5 col 4", - "col_5": "row 5 col 5", - "col_6": "row 5 col 6", - "col_7": "row 5 col 7", - "col_8": "row 5 col 8", - "col_9": "row 5 col 9", - "col_10": "row 5 col 10", - "col_11": "row 5 col 11", - "col_12": "row 5 col 12", - "col_13": "row 5 col 13", - "col_14": "row 5 col 14", - "col_15": "row 5 col 15", - "col_16": "row 5 col 16", - "col_17": "row 5 col 17", - "col_18": "row 5 col 18", - "col_19": "row 5 col 19" - }, - { - "col_0": "row 6 col 0", - "col_1": "row 6 col 1", - "col_2": "row 6 col 2", - "col_3": "row 6 col 3", - "col_4": "row 6 col 4", - "col_5": "row 6 col 5", - "col_6": "row 6 col 6", - "col_7": "row 6 col 7", - "col_8": "row 6 col 8", - "col_9": "row 6 col 9", - "col_10": "row 6 col 10", - "col_11": "row 6 col 11", - "col_12": "row 6 col 12", - "col_13": "row 6 col 13", - "col_14": "row 6 col 14", - "col_15": "row 6 col 15", - "col_16": "row 6 col 16", - "col_17": "row 6 col 17", - "col_18": "row 6 col 18", - "col_19": "row 6 col 19" - }, - { - "col_0": "row 7 col 0", - "col_1": "row 7 col 1", - "col_2": "row 7 col 2", - "col_3": "row 7 col 3", - "col_4": "row 7 col 4", - "col_5": "row 7 col 5", - "col_6": "row 7 col 6", - "col_7": "row 7 col 7", - "col_8": "row 7 col 8", - "col_9": "row 7 col 9", - "col_10": "row 7 col 10", - "col_11": "row 7 col 11", - "col_12": "row 7 col 12", - "col_13": "row 7 col 13", - "col_14": "row 7 col 14", - "col_15": "row 7 col 15", - "col_16": "row 7 col 16", - "col_17": "row 7 col 17", - "col_18": "row 7 col 18", - "col_19": "row 7 col 19" - }, - { - "col_0": "row 8 col 0", - "col_1": "row 8 col 1", - "col_2": "row 8 col 2", - "col_3": "row 8 col 3", - "col_4": "row 8 col 4", - "col_5": "row 8 col 5", - "col_6": "row 8 col 6", - "col_7": "row 8 col 7", - "col_8": "row 8 col 8", - "col_9": "row 8 col 9", - "col_10": "row 8 col 10", - "col_11": "row 8 col 11", - "col_12": "row 8 col 12", - "col_13": "row 8 col 13", - "col_14": "row 8 col 14", - "col_15": "row 8 col 15", - "col_16": "row 8 col 16", - "col_17": "row 8 col 17", - "col_18": "row 8 col 18", - "col_19": "row 8 col 19" - }, - { - "col_0": "row 9 col 0", - "col_1": "row 9 col 1", - "col_2": "row 9 col 2", - "col_3": "row 9 col 3", - "col_4": "row 9 col 4", - "col_5": "row 9 col 5", - "col_6": "row 9 col 6", - "col_7": "row 9 col 7", - "col_8": "row 9 col 8", - "col_9": "row 9 col 9", - "col_10": "row 9 col 10", - "col_11": "row 9 col 11", - "col_12": "row 9 col 12", - "col_13": "row 9 col 13", - "col_14": "row 9 col 14", - "col_15": "row 9 col 15", - "col_16": "row 9 col 16", - "col_17": "row 9 col 17", - "col_18": "row 9 col 18", - "col_19": "row 9 col 19" - } - ], - "display_index": false, - "columns": [ - "col_0", - "col_1", - "col_2", - "col_3", - "col_4", - "col_5", - "col_6", - "col_7", - "col_8", - "col_9", - "col_10", - "col_11", - "col_12", - "col_13", - "col_14", - "col_15", - "col_16", - "col_17", - "col_18", - "col_19" - ], - "machine_time": "2020-01-10T11:06:59.065891+00:00", - "line_no": 490, - "indices": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ] - }, - { - "category": "DEFAULT", - "description": "Table Log: long cells", - "meta_type": "entry", - "type": "TableLog", - "utc_time": "2020-01-10T03:06:59.070773+00:00", - "table": [ - { - "Name": "Bob Stevens", - "Age": "33", - "Address": "89 Trinsdale Avenue, LONDON, E8 0XW" - }, - { - "Name": "Susan Evans", - "Age": "21", - "Address": "100 Loop Road, SWANSEA, U8 12JK" - }, - { - "Name": "Trevor Dune", - "Age": "88", - "Address": "28 Kings Lane, MANCHESTER, MT16 2YT" - }, - { - "Name": "Belinda Baggins", - "Age": "38", - "Address": "31 Prospect Hill, DOYNTON, BS30 9DN" - }, - { - "Name": "Cosimo Hornblower", - "Age": "89", - "Address": "65 Prospect Hill, SURREY, PH33 4TY" - }, - { - "Name": "Sabine Wurfel", - "Age": "31", - "Address": "88 Clasper Way, HEXWORTHY, PL20 4BG" - } - ], - "display_index": false, - "columns": [ - "Name", - "Age", - "Address" - ], - "machine_time": "2020-01-10T11:06:59.070780+00:00", - "line_no": 504, - "indices": [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "ca8979be-8eb3-4ff4-8c18-aba4c8348bac", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.102866+00:00", - "start": "2020-01-10T03:06:59.087638+00:00" - } - }, - "hash": -6007544999293600650, - "runtime_status": "finished", - "name": "test_dict_namespace", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": "Simple dict match", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.087672+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Passed", - [ - "int", - "1" - ], - [ - "int", - "1" - ] - ], - [ - 0, - "bar", - "Failed", - [ - "int", - "2" - ], - [ - "int", - "5" - ] - ], - [ - 0, - "extra-key", - "Failed", - [ - null, - "ABSENT" - ], - [ - "int", - "10" - ] - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.087677+00:00", - "exclude_keys": null, - "line_no": 524 - }, - { - "category": "DEFAULT", - "description": "Nested dict match", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.089583+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Failed", - "", - "" - ], - [ - 1, - "alpha", - "Failed", - "", - "" - ], - [ - 1, - "", - "Passed", - [ - "int", - "1" - ], - [ - "int", - "1" - ] - ], - [ - 1, - "", - "Passed", - [ - "int", - "2" - ], - [ - "int", - "2" - ] - ], - [ - 1, - "", - "Failed", - [ - "int", - "3" - ], - [ - null, - null - ] - ], - [ - 1, - "beta", - "Failed", - "", - "" - ], - [ - 2, - "color", - "Failed", - [ - "str", - "red" - ], - [ - "str", - "blue" - ] - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.089619+00:00", - "exclude_keys": null, - "line_no": 542 - }, - { - "category": "DEFAULT", - "description": "Dict match: Custom comparators", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.091710+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Passed", - "", - "" - ], - [ - 0, - "", - "Passed", - [ - "int", - "1" - ], - [ - "int", - "1" - ] - ], - [ - 0, - "", - "Passed", - [ - "int", - "2" - ], - [ - "int", - "2" - ] - ], - [ - 0, - "", - "Passed", - [ - "int", - "3" - ], - [ - "func", - "" - ] - ], - [ - 0, - "bar", - "Passed", - "", - "" - ], - [ - 1, - "color", - "Passed", - [ - "str", - "blue" - ], - [ - "func", - "VAL in ['blue', 'red', 'yellow']" - ] - ], - [ - 0, - "baz", - "Passed", - [ - "str", - "hello world" - ], - [ - "REGEX", - "\\w+ world" - ] - ] - ], - "passed": true, - "machine_time": "2020-01-10T11:06:59.091718+00:00", - "exclude_keys": null, - "line_no": 560 - }, - { - "category": "DEFAULT", - "description": "default assertion passes because the values are numerically equal", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.093424+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Passed", - [ - "int", - "1" - ], - [ - "float", - 1.0 - ] - ], - [ - 0, - "bar", - "Passed", - [ - "int", - "2" - ], - [ - "float", - 2.0 - ] - ], - [ - 0, - "baz", - "Passed", - [ - "int", - "3" - ], - [ - "float", - 3.0 - ] - ] - ], - "passed": true, - "machine_time": "2020-01-10T11:06:59.093432+00:00", - "exclude_keys": null, - "line_no": 572 - }, - { - "category": "DEFAULT", - "description": "when we check types the assertion will fail", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.094973+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Failed", - [ - "int", - "1" - ], - [ - "float", - 1.0 - ] - ], - [ - 0, - "bar", - "Failed", - [ - "int", - "2" - ], - [ - "float", - 2.0 - ] - ], - [ - 0, - "baz", - "Failed", - [ - "int", - "3" - ], - [ - "float", - 3.0 - ] - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.094981+00:00", - "exclude_keys": null, - "line_no": 578 - }, - { - "category": "DEFAULT", - "description": "use a custom comparison function to check within a tolerance", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.096547+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "foo", - "Passed", - [ - "float", - 1.02 - ], - [ - "float", - 0.98 - ] - ], - [ - 0, - "bar", - "Passed", - [ - "float", - 2.28 - ], - [ - "float", - 2.33 - ] - ], - [ - 0, - "baz", - "Passed", - [ - "float", - 3.5 - ], - [ - "float", - 3.46 - ] - ] - ], - "passed": true, - "machine_time": "2020-01-10T11:06:59.096554+00:00", - "exclude_keys": null, - "line_no": 587 - }, - { - "category": "DEFAULT", - "description": "only report the failing comparison", - "meta_type": "assertion", - "type": "DictMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.098102+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - "bad_key", - "Failed", - [ - "str", - "actual" - ], - [ - "str", - "expected" - ] - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.098109+00:00", - "exclude_keys": null, - "line_no": 601 - }, - { - "absent_keys_diff": [ - "bar" - ], - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "has_keys_diff": [ - "alpha" - ], - "type": "DictCheck", - "utc_time": "2020-01-10T03:06:59.099751+00:00", - "passed": false, - "absent_keys": [ - "bar", - "beta" - ], - "machine_time": "2020-01-10T11:06:59.099760+00:00", - "line_no": 611, - "has_keys": [ - "foo", - "alpha" - ] - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "entry", - "type": "DictLog", - "utc_time": "2020-01-10T03:06:59.101282+00:00", - "flattened_dict": [ - [ - 0, - "foo", - "" - ], - [ - 0, - "", - [ - "int", - "1" - ] - ], - [ - 0, - "", - [ - "int", - "2" - ] - ], - [ - 0, - "", - [ - "int", - "3" - ] - ], - [ - 0, - "bar", - "" - ], - [ - 1, - "color", - [ - "str", - "blue" - ] - ], - [ - 0, - "baz", - [ - "str", - "hello world" - ] - ] - ], - "machine_time": "2020-01-10T11:06:59.101290+00:00", - "line_no": 620 - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 0, - "failed": 1, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "826ee3d4-0dea-412b-9652-86f5847706d9", - "status": "failed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.116938+00:00", - "start": "2020-01-10T03:06:59.111312+00:00" - } - }, - "hash": 3253704292606433761, - "runtime_status": "finished", - "name": "test_fix_namespace", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "type": "FixMatch", - "include_keys": null, - "utc_time": "2020-01-10T03:06:59.111446+00:00", - "actual_description": null, - "expected_description": null, - "comparison": [ - [ - 0, - 36, - "Passed", - [ - "int", - "6" - ], - [ - "int", - "6" - ] - ], - [ - 0, - 22, - "Passed", - [ - "int", - "5" - ], - [ - "int", - "5" - ] - ], - [ - 0, - 55, - "Passed", - [ - "int", - "2" - ], - [ - "int", - "2" - ] - ], - [ - 0, - 38, - "Passed", - [ - "int", - "5" - ], - [ - "func", - "VAL >= 4" - ] - ], - [ - 0, - 555, - "Failed", - "", - "" - ], - [ - 0, - "", - "Failed", - "", - "" - ], - [ - 1, - 600, - "Passed", - [ - "str", - "A" - ], - [ - "str", - "A" - ] - ], - [ - 1, - 601, - "Failed", - [ - "str", - "A" - ], - [ - "str", - "B" - ] - ], - [ - 1, - 683, - "Passed", - "", - "" - ], - [ - 1, - "", - "Passed", - "", - "" - ], - [ - 2, - 688, - "Passed", - [ - "str", - "a" - ], - [ - "str", - "a" - ] - ], - [ - 2, - 689, - "Passed", - [ - "str", - "a" - ], - [ - "REGEX", - "[a-z]" - ] - ], - [ - 1, - "", - "Passed", - "", - "" - ], - [ - 2, - 688, - "Passed", - [ - "str", - "b" - ], - [ - "str", - "b" - ] - ], - [ - 2, - 689, - "Passed", - [ - "str", - "b" - ], - [ - "str", - "b" - ] - ], - [ - 0, - "", - "Failed", - "", - "" - ], - [ - 1, - 600, - "Failed", - [ - "str", - "B" - ], - [ - "str", - "C" - ] - ], - [ - 1, - 601, - "Passed", - [ - "str", - "B" - ], - [ - "str", - "B" - ] - ], - [ - 1, - 683, - "Passed", - "", - "" - ], - [ - 1, - "", - "Passed", - "", - "" - ], - [ - 2, - 688, - "Passed", - [ - "str", - "c" - ], - [ - "str", - "c" - ] - ], - [ - 2, - 689, - "Passed", - [ - "str", - "c" - ], - [ - "func", - "VAL in ('c', 'd')" - ] - ], - [ - 1, - "", - "Passed", - "", - "" - ], - [ - 2, - 688, - "Passed", - [ - "str", - "d" - ], - [ - "str", - "d" - ] - ], - [ - 2, - 689, - "Passed", - [ - "str", - "d" - ], - [ - "str", - "d" - ] - ] - ], - "passed": false, - "machine_time": "2020-01-10T11:06:59.111452+00:00", - "exclude_keys": null, - "line_no": 708 - }, - { - "absent_keys_diff": [ - 555 - ], - "category": "DEFAULT", - "description": null, - "meta_type": "assertion", - "has_keys_diff": [ - 26, - 11 - ], - "type": "FixCheck", - "utc_time": "2020-01-10T03:06:59.113689+00:00", - "passed": false, - "absent_keys": [ - 444, - 555 - ], - "machine_time": "2020-01-10T11:06:59.113697+00:00", - "line_no": 716, - "has_keys": [ - 26, - 22, - 11 - ] - }, - { - "category": "DEFAULT", - "description": null, - "meta_type": "entry", - "type": "FixLog", - "utc_time": "2020-01-10T03:06:59.115483+00:00", - "flattened_dict": [ - [ - 0, - 36, - [ - "int", - "6" - ] - ], - [ - 0, - 22, - [ - "int", - "5" - ] - ], - [ - 0, - 55, - [ - "int", - "2" - ] - ], - [ - 0, - 38, - [ - "int", - "5" - ] - ], - [ - 0, - 555, - "" - ], - [ - 0, - "", - "" - ], - [ - 1, - 556, - [ - "str", - "USD" - ] - ], - [ - 1, - 624, - [ - "int", - "1" - ] - ], - [ - 0, - "", - "" - ], - [ - 1, - 556, - [ - "str", - "EUR" - ] - ], - [ - 1, - 624, - [ - "int", - "2" - ] - ] - ], - "machine_time": "2020-01-10T11:06:59.115490+00:00", - "line_no": 729 - } - ] - }, - { - "category": "testcase", - "logs": [], - "description": null, - "suite_related": false, - "counter": { - "passed": 1, - "failed": 0, - "total": 1 - }, - "status_reason": null, - "type": "TestCaseReport", - "uid": "52a8a7d9-80e6-4f7f-8eef-065bb25d38f8", - "status": "passed", - "parent_uids": [ - "Assertions Example", - "Assertions Test", - "SampleSuite" - ], - "timer": { - "run": { - "end": "2020-01-10T03:06:59.129247+00:00", - "start": "2020-01-10T03:06:59.123570+00:00" - } - }, - "hash": -5041530229790182508, - "runtime_status": "finished", - "name": "test_xml_namespace", - "status_override": null, - "tags": {}, - "entries": [ - { - "category": "DEFAULT", - "description": "Simple XML check for existence of xpath.", - "meta_type": "assertion", - "type": "XMLCheck", - "utc_time": "2020-01-10T03:06:59.123813+00:00", - "namespaces": null, - "data": [], - "passed": true, - "xml": "\n Foo\n \n", - "machine_time": "2020-01-10T11:06:59.123821+00:00", - "tags": null, - "line_no": 751, - "message": "xpath: `/Root/Test` exists in the XML.", - "xpath": "/Root/Test" - }, - { - "category": "DEFAULT", - "description": "XML check for tags in the given xpath.", - "meta_type": "assertion", - "type": "XMLCheck", - "utc_time": "2020-01-10T03:06:59.125438+00:00", - "namespaces": null, - "data": [ - [ - "Value1", - null, - null, - null - ], - [ - "Value2", - null, - null, - null - ] - ], - "passed": true, - "xml": "\n Value1\n Value2\n \n", - "machine_time": "2020-01-10T11:06:59.125447+00:00", - "tags": [ - "Value1", - "Value2" - ], - "line_no": 765, - "message": null, - "xpath": "/Root/Test" - }, - { - "category": "DEFAULT", - "description": "XML check with namespace matching.", - "meta_type": "assertion", - "type": "XMLCheck", - "utc_time": "2020-01-10T03:06:59.127250+00:00", - "namespaces": { - "a": "http://testplan" - }, - "data": [ - [ - "Hello world!", - null, - null, - "REGEX(Hello*)" - ] - ], - "passed": true, - "xml": "\n \n \n Hello world!\n \n \n", - "machine_time": "2020-01-10T11:06:59.127259+00:00", - "tags": [ - "re.compile('Hello*')" - ], - "line_no": 784, - "message": null, - "xpath": "//*/a:message" - } - ] - } - ] - } - ] - } - ] -} - - -export { - TESTPLAN_REPORT, - fakeReportAssertions, -} diff --git a/testplan/web_ui/testing/src/Common/filterStates.js b/testplan/web_ui/testing/src/Common/filterStates.js new file mode 100644 index 000000000..6ebeb3244 --- /dev/null +++ b/testplan/web_ui/testing/src/Common/filterStates.js @@ -0,0 +1,6 @@ +/** @const */ +export const ALL = 'all'; +/** @const */ +export const FAILED = 'fail'; +/** @const */ +export const PASSED = 'pass'; diff --git a/testplan/web_ui/testing/src/Common/uriComponentCodec.js b/testplan/web_ui/testing/src/Common/uriComponentCodec.js new file mode 100644 index 000000000..33f2544c9 --- /dev/null +++ b/testplan/web_ui/testing/src/Common/uriComponentCodec.js @@ -0,0 +1,40 @@ +import _escapeRegExp from 'lodash/escapeRegExp'; +import { reverseMap } from './utils'; + +/** + * The keys here are characters that have special meaning in a URL, + * and unfortunately React Router can't be relied upon to consistently + * serialize (search "react router URL encode slashes in route"), and + * doesn't provide a super-easy way to DIY the serialization. + * + * We use a `Map` since we need '%' to be serialized first, and `Map` + * maintains order. + */ +const originalToPctEscapedMap = new Map([ + '%', '?', '#', ':', '/', '@', ']', '[', '!', '$', + '&', "'", '(', ')', '=', ';', '+', '*', ',', +].map(c => [ c, `%${c.charCodeAt(0).toString(16)}` ])); + +const mkTranslator = charmap => uriComponent => ( + [ charmap ].reduce((prevStr, [oldChar, newChar]) => + prevStr.replace(RegExp(_escapeRegExp(oldChar), 'g'), newChar), + uriComponent + ) +); + +export default { + /** + * @function uriComponentCodec.encode + * @desc Apply custom URL encodings to a string. + * @param {string} uriComponent - The string to encode + * @return {string} - `uriComponent` with URL-unsafe characters replaced + */ + encode: mkTranslator(originalToPctEscapedMap), + /** + * @function uriComponentCodec.decode + * @desc Unapply custom URL encodings to a string. + * @param {string} uriComponent - The string to decode + * @return {string} - `uriComponent` with URL-unsafe characters restored + */ + decode: mkTranslator(reverseMap(originalToPctEscapedMap)), +}; diff --git a/testplan/web_ui/testing/src/Common/utils.js b/testplan/web_ui/testing/src/Common/utils.js index d24423cc9..ebc5c10be 100644 --- a/testplan/web_ui/testing/src/Common/utils.js +++ b/testplan/web_ui/testing/src/Common/utils.js @@ -1,7 +1,7 @@ /** * Common utility functions. */ -import {NAV_ENTRY_DISPLAY_DATA} from "./defaults"; +import { NAV_ENTRY_DISPLAY_DATA } from './defaults'; /** * Get the data to be used when displaying the nav entry. @@ -9,8 +9,8 @@ import {NAV_ENTRY_DISPLAY_DATA} from "./defaults"; * @param {object} entry - nav entry. * @returns {Object} */ -function getNavEntryDisplayData(entry) { - let metadata = {}; +export function getNavEntryDisplayData(entry) { + const metadata = {}; for (const attribute of NAV_ENTRY_DISPLAY_DATA) { if (entry.hasOwnProperty(attribute)) { metadata[attribute] = entry[attribute]; @@ -25,13 +25,7 @@ function getNavEntryDisplayData(entry) { * @param iterable * @returns {boolean} */ -function any(iterable) { - for (let index = 0; index < iterable.length; ++index) { - if (iterable[index]) return true; - } - - return false; -} +export const any = iterable => Array.from(iterable).some(e => !!e); /** * Returns a sorted array of the given iterable. @@ -41,9 +35,9 @@ function any(iterable) { * @param {boolean} reverse - if true, the sorted list is reversed * @returns {Array} */ -function sorted(iterable, key=(item) => (item), reverse=false) { +export function sorted(iterable, key=(item) => (item), reverse=false) { return iterable.sort((firstMember, secondMember) => { - let reverser = reverse ? 1 : -1; + const reverser = reverse ? 1 : -1; return ((key(firstMember) < key(secondMember)) ? reverser @@ -58,7 +52,7 @@ function sorted(iterable, key=(item) => (item), reverse=false) { * Example: "id-so7567s1pcpojemi" * @returns {string} */ -function uniqueId() { +export function uniqueId() { return 'id-' + Math.random().toString(36).substr(2, 16); } @@ -67,8 +61,8 @@ function uniqueId() { * @param {string} str - string that generate hash code * @returns {number} */ -function hashCode(str) { - var hash = 0, i, chr, len; +export function hashCode(str) { + let hash = 0, i, chr, len; if (str.length === 0) return hash; for (i = 0, len = str.length; i < len; i++) { chr = str.charCodeAt(i); @@ -83,17 +77,52 @@ function hashCode(str) { * @param {object} dom - HTML DOM node * @returns {string} */ -function domToString(dom) { - let tmp = document.createElement("div"); +export function domToString(dom) { + const tmp = document.createElement("div"); tmp.appendChild(dom); return tmp.innerHTML; } -export { - getNavEntryDisplayData, - any, - sorted, - uniqueId, - hashCode, - domToString, -}; +/** + * Reverses a Map. + * @template T, U + * @param {Map} aMap - The map to reverse + * @returns {Map} + */ +export const reverseMap = aMap => new Map( + Array.from(aMap).map(([newVal, newKey]) => [ newKey, newVal ]) +); + +export const SI_FILE_SIZES = [ + 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' +]; +export const BINARY_FILE_SIZES = [ + 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' +]; + +/** + * Taken from {@link https://stackoverflow.com/a/14919494 this great SO answer}. + * @example + > toHumanReadableSize(5000, true) + '5.0 kB' + > toHumanReadableSize(5000, false) + '4.9 KiB' + > toHumanReadableSize(-10000000000000000000000000000) + '-8271.8 YiB' + * + * @param {number} numBytes + * @param {boolean} useSI + * @param {number} decimals + * @returns {string} + */ +export function humanReadableSize(numBytes, useSI = false, decimals = 1) { + numBytes = typeof numBytes !== 'number' ? 0 : numBytes; + decimals = typeof decimals !== 'number' ? 1 : decimals; + const div = useSI ? 10 ** 3 : 2 ** 10; + if(Math.abs(numBytes) < div) return `${numBytes} B`; + const units = useSI ? SI_FILE_SIZES : BINARY_FILE_SIZES; + const lastUnitIdx = units.length - 1; + let u = -1; + do numBytes /= div; while(Math.abs(numBytes) >= div && ++u < lastUnitIdx); + return `${numBytes.toFixed(decimals)} ${units[u]}`; +} diff --git a/testplan/web_ui/testing/src/Nav/InteractiveNavEntry.js b/testplan/web_ui/testing/src/Nav/InteractiveNavEntry.js index e875562f9..53bc15116 100644 --- a/testplan/web_ui/testing/src/Nav/InteractiveNavEntry.js +++ b/testplan/web_ui/testing/src/Nav/InteractiveNavEntry.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Badge} from 'reactstrap'; import {StyleSheet, css} from "aphrodite"; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import { faPlay, faRedo, diff --git a/testplan/web_ui/testing/src/Nav/NavBreadcrumbs.js b/testplan/web_ui/testing/src/Nav/NavBreadcrumbs.js index f88d931b7..ef2a638b4 100644 --- a/testplan/web_ui/testing/src/Nav/NavBreadcrumbs.js +++ b/testplan/web_ui/testing/src/Nav/NavBreadcrumbs.js @@ -62,7 +62,7 @@ NavBreadcrumbs.propTypes = { handleNavClick: PropTypes.func, }; -const styles = StyleSheet.create({ +export const styles = StyleSheet.create({ navBreadcrumbs: { top: '2.5em', borderBottom: 'solid 1px rgba(0, 0, 0, 0.1)', diff --git a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNav.test.js b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNav.test.js index aef9856e2..2e7b028d5 100644 --- a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNav.test.js +++ b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNav.test.js @@ -3,8 +3,8 @@ import React from 'react'; import {shallow} from 'enzyme'; import {StyleSheetTestUtils} from "aphrodite"; -import InteractiveNav from '../InteractiveNav.js'; -import {FakeInteractiveReport} from '../../Common/sampleReports.js'; +import InteractiveNav from '../InteractiveNav'; +import { FakeInteractiveReport } from '../../__tests__/documents'; describe('InteractiveNav', () => { beforeEach(() => { diff --git a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavEntry.test.js b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavEntry.test.js index 9d84e82b1..898eab498 100644 --- a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavEntry.test.js +++ b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavEntry.test.js @@ -2,10 +2,9 @@ import React from 'react'; import {shallow} from 'enzyme'; import {StyleSheetTestUtils} from "aphrodite"; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; -import InteractiveNavEntry from '../InteractiveNavEntry.js'; -import {FakeInteractiveReport} from '../../Common/sampleReports.js'; +import InteractiveNavEntry from '../InteractiveNavEntry'; describe('InteractiveNavEntry', () => { beforeEach(() => { diff --git a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavList.test.js b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavList.test.js index 16826c8ac..c624c6243 100644 --- a/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavList.test.js +++ b/testplan/web_ui/testing/src/Nav/__tests__/InteractiveNavList.test.js @@ -3,8 +3,8 @@ import React from 'react'; import {shallow} from 'enzyme'; import {StyleSheetTestUtils} from "aphrodite"; -import InteractiveNavList from '../InteractiveNavList.js'; -import {FakeInteractiveReport} from '../../Common/sampleReports.js'; +import InteractiveNavList from '../InteractiveNavList'; +import { FakeInteractiveReport } from '../../__tests__/documents'; describe('InteractiveNavList', () => { beforeEach(() => { diff --git a/testplan/web_ui/testing/src/Nav/__tests__/Nav.test.js b/testplan/web_ui/testing/src/Nav/__tests__/Nav.test.js index ef4892fa0..96715a118 100644 --- a/testplan/web_ui/testing/src/Nav/__tests__/Nav.test.js +++ b/testplan/web_ui/testing/src/Nav/__tests__/Nav.test.js @@ -1,13 +1,12 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import { shallow } from 'enzyme'; import {StyleSheetTestUtils} from "aphrodite"; - import Nav from '../Nav'; -import {TESTPLAN_REPORT} from '../../Common/sampleReports'; +import { TESTPLAN_REPORT_1 as REPORT } from '../../__tests__/documents'; const defaultProps = { - report: TESTPLAN_REPORT, - selected: [TESTPLAN_REPORT], + report: REPORT, + selected: [REPORT], }; describe('Nav', () => { diff --git a/testplan/web_ui/testing/src/Nav/__tests__/navUtils.test.js b/testplan/web_ui/testing/src/Nav/__tests__/navUtils.test.js index ff43fc744..d84408ee8 100644 --- a/testplan/web_ui/testing/src/Nav/__tests__/navUtils.test.js +++ b/testplan/web_ui/testing/src/Nav/__tests__/navUtils.test.js @@ -1,8 +1,6 @@ -import React from 'react'; import {StyleSheetTestUtils} from "aphrodite"; - import {CreateNavButtons, GetSelectedUid} from '../navUtils'; -import {TESTPLAN_REPORT} from '../../Common/sampleReports'; +import { TESTPLAN_REPORT_1 as REPORT } from '../../__tests__/documents'; describe('navUtils', () => { @@ -25,11 +23,11 @@ describe('navUtils', () => { displayTime: false, displayEmpty: true, handleNavClick: jest.fn(), - entries: TESTPLAN_REPORT.entries, + entries: REPORT.entries, filter: null, } const createEntryComponent = jest.fn(); - const selectedUid = TESTPLAN_REPORT.uid; + const selectedUid = REPORT.uid; const navButtons = CreateNavButtons( props, createEntryComponent, selectedUid @@ -41,9 +39,9 @@ describe('navUtils', () => { describe('GetSelectedUid', () => { it('gets the selected UID', () => { - const selected = [TESTPLAN_REPORT]; + const selected = [REPORT]; const uid = GetSelectedUid(selected); - expect(uid).toBe(TESTPLAN_REPORT.uid); + expect(uid).toBe(REPORT.uid); }); }); diff --git a/testplan/web_ui/testing/src/Nav/navUtils.js b/testplan/web_ui/testing/src/Nav/navUtils.js index d69083283..5144aa6e7 100644 --- a/testplan/web_ui/testing/src/Nav/navUtils.js +++ b/testplan/web_ui/testing/src/Nav/navUtils.js @@ -105,7 +105,7 @@ const applyNamedFilter = (entries, filter) => { } }; -const styles = StyleSheet.create({ +export const styles = StyleSheet.create({ navButton: { position: 'relative', display: 'block', @@ -148,8 +148,8 @@ const GetSelectedUid = (selected) => { * we do not drill down any further and instead display all entries in the * suite that testcase belongs to. * - * @param {Array[ReportNode]} selected - Current selection hierarchy. - * @return {Array[ReportNode]} Report nodes to display in the navigation + * @param {Array} selected - Current selection hierarchy. + * @return {Array} Report nodes to display in the navigation * column. */ const GetNavEntries = (selected) => { @@ -178,8 +178,8 @@ const GetNavEntries = (selected) => { * this is just the selection hierarchy. As a special case, when a testcase * is selected, we only display up to the suite level in the breadcrumb bar. * - * @param {Array[ReportNode]} selected - Current selection hierarchy. - * @return {Array[ReportNode]} Report nodes to display in the breadcrumb bar. + * @param {Array} selected - Current selection hierarchy. + * @return {Array} Report nodes to display in the breadcrumb bar. */ const GetNavBreadcrumbs = (selected) => { const selectedEntry = selected[selected.length - 1]; diff --git a/testplan/web_ui/testing/src/Report/BatchReport.js b/testplan/web_ui/testing/src/Report/BatchReport.js deleted file mode 100644 index 29bf4870d..000000000 --- a/testplan/web_ui/testing/src/Report/BatchReport.js +++ /dev/null @@ -1,235 +0,0 @@ -import React from 'react'; -import {StyleSheet, css} from 'aphrodite'; -import axios from 'axios'; - -import Toolbar from '../Toolbar/Toolbar'; -import {TimeButton} from '../Toolbar/Buttons'; -import Nav from '../Nav/Nav'; -import { - PropagateIndices, - UpdateSelectedState, - GetReportState, - GetCenterPane, - GetSelectedEntries, -} from "./reportUtils"; -import {COLUMN_WIDTH} from "../Common/defaults"; -import {fakeReportAssertions} from "../Common/fakeReport"; - -/** - * BatchReport component: - * * fetch Testplan report. - * * display messages when loading report or error in report. - * * render toolbar, nav & assertion components. - */ -class BatchReport extends React.Component { - constructor(props) { - super(props); - this.handleNavFilter = this.handleNavFilter.bind(this); - this.updateFilter = this.updateFilter.bind(this); - this.updateTagsDisplay = this.updateTagsDisplay.bind(this); - this.updateTimeDisplay = this.updateTimeDisplay.bind(this); - this.updateDisplayEmpty = this.updateDisplayEmpty.bind(this); - this.handleNavClick = this.handleNavClick.bind(this); - this.handleColumnResizing = this.handleColumnResizing.bind(this); - - this.state = { - navWidth: `${COLUMN_WIDTH}em`, - report: null, - testcaseUid: null, - loading: false, - error: null, - filter: null, - displayTags: false, - displayTime: false, - displayEmpty: true, - selectedUIDs: [], - }; - } - - /** - * Fetch the Testplan report. - * * Get the UID from the URL. - * * Handle UID errors. - * * Make a GET request for the Testplan report. - * * Handle error response. - * @public - */ - getReport() { - // Inspect the UID to determine the report to render. As a special case, - // we will display a fake report for development purposes. - const uid = this.props.match.params.uid; - if (uid === "_dev") { - const processedReport = PropagateIndices(fakeReportAssertions); - setTimeout( - () => this.setState({ - report: processedReport, - selectedUIDs: this.autoSelect(processedReport), - loading: false, - }), - 1500); - } else { - axios.get(`/api/v1/reports/${uid}`) - .then(response => { - const processedReport = PropagateIndices(response.data); - this.setState({ - report: processedReport, - selectedUIDs: this.autoSelect(processedReport), - loading: false, - }); - }) - .catch(error => this.setState({ - error: error, - loading: false, - })); - } - } - - /** - * Auto-select an entry in the report when it is first loaded. - * @param {reportNode} reportEntry - the current report entry to select from. - * @return {Array[string]} List of UIDs of the currently selected entries. - */ - autoSelect(reportEntry) { - const selection = [reportEntry.uid]; - - // If the current report entry has only one child entry and that entry is - // not a testcase, we automatically expand it. - if ((reportEntry.entries.length === 1) && - (reportEntry.entries[0].category!== "testcase")) { - return selection.concat(this.autoSelect(reportEntry.entries[0])); - } else { - return selection; - } - } - - /** - * Fetch the Testplan report once the component has mounted. - * @public - */ - componentDidMount() { - this.setState({loading: true}, this.getReport); - } - - /** - * Handle filter expressions being typed into the filter box. Placeholder. - * - * @param {Object} e - keyup event. - * @public - */ - handleNavFilter(e) { // eslint-disable-line no-unused-vars - // Save expressions to state. - } - - /** - * Update the global filter state of the entry. - * - * @param {string} filter - null, all, pass or fail. - * @public - */ - updateFilter(filter) { - this.setState({filter: filter}); - } - - /** - * Update tag display of each navigation entry. - * - * @param {boolean} displayTags. - * @public - */ - updateTagsDisplay(displayTags) { - this.setState({displayTags: displayTags}); - } - - /** - * Update navigation pane to show/hide entries of empty testcases. - * - * @param {boolean} displayEmpty. - * @public - */ - updateDisplayEmpty(displayEmpty) { - this.setState({displayEmpty: displayEmpty}); - } - - /** - * Update execution time display of each navigation entry and each assertion. - * - * @param {boolean} displayTime. - * @public - */ - updateTimeDisplay(displayTime) { - this.setState({displayTime: displayTime}); - } - - /** - * Handle resizing event and update NavList & Center Pane. - */ - handleColumnResizing(navWidth) { - this.setState({navWidth: navWidth}); - } - - /** - * Handle a navigation entry being clicked. - */ - handleNavClick(e, entry, depth) { - e.stopPropagation(); - this.setState((state, props) => UpdateSelectedState(state, entry, depth)); - } - - render() { - const {reportStatus, reportFetchMessage} = GetReportState(this.state); - - if (this.state.report && this.state.report.name) { - window.document.title = this.state.report.name; - } - - const selectedEntries = GetSelectedEntries( - this.state.selectedUIDs, this.state.report - ); - const centerPane = GetCenterPane( - this.state, - reportFetchMessage, - this.props.match.params.uid, - selectedEntries, - ); - - return ( -
- ]} - /> -
- ); - } -} - -const styles = StyleSheet.create({ - batchReport: { - /** overflow will hide dropdown div */ - // overflow: 'hidden' - } -}); - -export default BatchReport; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport.test.js b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport.test.js new file mode 100644 index 000000000..a94142382 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport.test.js @@ -0,0 +1,243 @@ +/// +import React from 'react'; +import ReactTestUtils from 'react-dom/test-utils'; +import { mount } from 'enzyme'; +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { StyleSheetTestUtils } from 'aphrodite/es'; +import moxios from 'moxios'; +import { Route } from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom'; +import { ReportStateProvider } from '../state'; +import { ReportStateContext } from '../state'; +import BatchReport from '../'; +import { UIRouter } from '../components'; +import Message from '../../../Common/Message'; +import { COLUMN_WIDTH } from '../../../Common/defaults'; +import SwitchRequireSlash from '../../../Common/SwitchRequireSlash'; +import { TESTPLAN_REPORT_1 } from '../../../__tests__/documents'; +import { SIMPLE_REPORT } from '../../../__tests__/documents'; + +configure({ adapter: new Adapter() }); + +describe('BatchReport', () => { + + // this is only used to force a rerender of TestAppStateProvider by providing + // different props each time + const dummyCounter = 0; + class TestStateProvider extends React.Component { + intercepted = { + appState: null, + appActions: null, + browserRouterProps: null, + uiRouterProps: null, + }; + interceptors = { + appContext(val) { + this.intercepted.appState = val[0]; + this.intercepted.appActions = val[1]; + return null; + }, + browserRouterProps(val) { + this.intercepted.browserRouterProps = val; + return null; + }, + uiRouterProps(val) { + this.intercepted.uiRouterProps = val; + return null; + } + }; + constructor(props) { super(props); } + render() { + return ( + + + { + this.interceptors.browserRouterProps(props); + return ( + + + + + + + + ); + }}/> + + + ); + } + } + + /** @returns {[ any, any ]} */ + function appStateAccessor() { + class AppContextAccessor extends React.Component { + constructor(props) { super(props); } + render = () => ( + + + { /** @param {any} ctx */ ctx => (this.state = ctx) && null } + + + ); + } + // @ts-ignore + return mount().instance().state; + } + + /** + * @property {string} uid + * @property {any[]} props + */ + const renderBatchReport = ({ + _dummyCounter = dummyCounter, uid = "123", ...props } = {}) => { + // Mock the match object that would be passed down from react-router. + // BatchReport uses this object to get the report UID. + const mockMatch = { params: { uid: uid } }; + const startLocation = { pathname: uid ? `/testplan/${uid}` : null }; + return mount( + /* + // @ts-ignore + + */ + + ); + }; + + beforeEach(() => { + // Stop Aphrodite from injecting styles, this crashes the tests. + StyleSheetTestUtils.suppressStyleInjection(); + moxios.install(); + }); + + afterEach(() => { + // Resume style injection once test is finished. + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + moxios.uninstall(); + }); + + it('shallow renders without crashing', () => { + renderBatchReport(); + }); + + it('shallow renders the correct HTML structure when report loaded', () => { + // @ts-ignore + const batchReport = renderBatchReport({ skipFetch: true }), + [, { setAppBatchReportJsonReport } ] = appStateAccessor(); + ReactTestUtils.act( + () => setAppBatchReportJsonReport(TESTPLAN_REPORT_1) + ); + batchReport.update(); + expect(batchReport).toMatchSnapshot(); + }); + + it('renders loading message when fetching report', () => { + let BR = mount( + + ); + // @ts-ignore + // let BR = renderBatchReport({ skipFetch: true }); + /*, + [ appState, { setAppBatchReportIsFetching } ] = appStateAccessor(), + isFetchingMessage = appState.app.reports.batch.isFetchingMessage;*/ + const + setAppBatchReportIsFetching = + BR.instance().intercepted.appActions.setAppBatchReportIsFetching, + isFetchingMessage = + BR.instance().intercepted.appState.app.reports.batch.isFetchingMessage; + ReactTestUtils.act(() => { + setAppBatchReportIsFetching(true); + // dummyCounter++; + // ReportStateContext.forceUpdate(); + BR.update(); + }); + // batchReport = renderBatchReport({ skipFetch: true }); + const messageDOM = mount( + + ).instance(); + const ass = BR.containsMatchingElement( + // messageDOM + + ); + expect(ass).toBe(true); + let x = 1; + }); + + it('renders waiting message when waiting to start fetch', () => { + // @ts-ignore + const batchReport = renderBatchReport({ skipFetch: true }), + [ appState, { setAppBatchReportIsLoading } ] = appStateAccessor(), + isLoadingMessage = appState.app.reports.batch.isLoadingMessage; + ReactTestUtils.act(() => setAppBatchReportIsLoading(true)); + batchReport.update(); + expect(batchReport.contains( + + )); + }); + + it('loads a simple report and autoselects entries', done => { + const batchReport = renderBatchReport(); + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/reports/123"); + request.respondWith({ + status: 200, + response: SIMPLE_REPORT, + }).then(() => { + batchReport.update(); + const selection = batchReport.state("selectedUIDs"); + expect(selection.length).toBe(3); + expect(selection).toEqual([ + "520a92e4-325e-4077-93e6-55d7091a3f83", + "21739167-b30f-4c13-a315-ef6ae52fd1f7", + "cb144b10-bdb0-44d3-9170-d8016dd19ee7", + ]); + expect(batchReport).toMatchSnapshot(); + done(); + }); + }); + }); + + it('loads a more complex report and autoselects entries', done => { + const batchReport = renderBatchReport(); + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + expect(request.url).toBe("/api/v1/reports/123"); + request.respondWith({ + status: 200, + response: TESTPLAN_REPORT_1, + }).then(() => { + batchReport.update(); + const selection = batchReport.state("selectedUIDs"); + expect(selection.length).toBe(1); + expect(selection).toEqual(["520a92e4-325e-4077-93e6-55d7091a3f83"]); + expect(batchReport).toMatchSnapshot(); + done(); + }); + }); + }); + + it('renders an error message when Testplan report cannot be found.', done => { + const batchReport = renderBatchReport(); + moxios.wait(function () { + let request = moxios.requests.mostRecent(); + request.respondWith({ + status: 404, + }).then(function () { + batchReport.update(); + const message = batchReport.find(Message); + const expectedMessage = 'Error: Request failed with status code 404'; + expect(message.props().message).toEqual(expectedMessage); + done(); + }) + }) + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport_routing.test.js b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport_routing.test.js new file mode 100644 index 000000000..414243489 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/BatchReport_routing.test.js @@ -0,0 +1,13 @@ +/** @jest-environment puppeteer */ +const serverOrigin = global.puppeteerConfig.getOrigin(); + +describe('Google', () => { + beforeAll(async () => { + await page.goto(`${serverOrigin}?dev=true`); + }); + + it('should display "google" text on page', async () => { + await expect(page.url()).toMatch(serverOrigin); + await jestPuppeteer.debug(); + }, /*2**31-1*/); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/__tests__/__snapshots__/BatchReport.test.js.snap b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/__snapshots__/BatchReport.test.js.snap new file mode 100644 index 000000000..3bdb4a23a --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/__tests__/__snapshots__/BatchReport.test.js.snap @@ -0,0 +1,2785 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BatchReport loads a more complex report and autoselects entries 1`] = ` +
+ +
+`; + +exports[`BatchReport loads a simple report and autoselects entries 1`] = ` +
+ +
+`; + +exports[`BatchReport shallow renders the correct HTML structure when report loaded 1`] = ` + + + + + +
+ +
+ + + + + + + + + + + +
+
+ + + +
  • + No entries to display... +
  • +
    +
    +
    + + +
    +

    + Please select an entry. +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/AutoSelectRedirect.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/AutoSelectRedirect.jsx new file mode 100644 index 000000000..0d1e50361 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/AutoSelectRedirect.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Redirect } from 'react-router'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIDoAutoSelect } from '../state/uiSelectors'; + +const connector = connect( + () => { + const getDoAutoSelect = mkGetUIDoAutoSelect(); + return state => ({ + doAutoSelect: getDoAutoSelect(state), + }); + }, + null, + (stateProps, _, ownProps) => { + const { doAutoSelect } = stateProps; + const { basePath } = ownProps; + let { entry } = ownProps; + // trim trailing slashes from basePath and join with the first entry's name + let toPath = `${basePath.replace(/\/+$/, '')}/${entry.name || ''}`; + if(doAutoSelect) { + while(entry.category !== 'testcase' + && Array.isArray(entry.entries) + && entry.entries.length === 1 + && typeof (entry = entry.entries[0] || {}) === 'object' + && typeof (entry.name) === 'string' + ) { + toPath = `${toPath}/${entry.name}`; + } + } + return { + toPath, + }; + }, +); + +export default connector(({ toPath }) => ( + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/BoundStyledListGroupItemLink.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/BoundStyledListGroupItemLink.jsx new file mode 100644 index 000000000..c8f516d8b --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/BoundStyledListGroupItemLink.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { withRouter } from 'react-router'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsShowTags } from '../state/uiSelectors'; +import { setHashComponentAlias } from '../state/uiActions'; +import { setSelectedTestCase } from '../state/uiActions'; +import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults'; +import { uriComponentCodec } from '../utils'; +import StyledListGroupItemLink from './StyledListGroupItemLink'; +import TagList from '../../../Nav/TagList'; +import NavEntry from '../../../Nav/NavEntry'; + +const connector = connect( + () => { + const getIsShowTags = mkGetUIIsShowTags(); + return state => ({ + isShowTags: getIsShowTags(state), + }); + }, + { + setSelectedTestCase, + setHashComponentAlias, + }, + (stateProps, dispatchProps, ownProps) => { + const { isShowTags } = stateProps; + const { setSelectedTestCase, setHashComponentAlias } = dispatchProps; + const { entry, idx, nPass, nFail, match: { url: matchedUrl } } = ownProps; + return { + isShowTags, + setSelectedTestCase, + setHashComponentAlias, + entry, + idx, + nPass, + nFail, + matchedUrl, + }; + } +); + +export default connector(withRouter(({ + entry, idx, nPass, nFail, isShowTags, setHashComponentAlias, + setSelectedTestCase, matchedUrl +}) => { + const { name, status, category, tags, uid } = entry, + isBottommost = category === BOTTOMMOST_ENTRY_CATEGORY, + encodedName = uriComponentCodec.encode(name), + nextPathname = isBottommost ? matchedUrl : `${matchedUrl}/${encodedName}`, + onClickOverride = !isBottommost ? { + onClick() { setSelectedTestCase(null); }, + } : { + onClick(evt) { + evt.preventDefault(); + evt.stopPropagation(); + setSelectedTestCase(entry); + }, + }; + setHashComponentAlias(encodedName, name); + return ( + + { + isShowTags && tags + ? + : null + } + + + ); +})); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/Placeholder.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/Placeholder.jsx new file mode 100644 index 000000000..b5ba94c9c --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/Placeholder.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import Progress from 'reactstrap/lib/Progress'; +import connect from 'react-redux/es/connect/connect'; +import _debounce from 'lodash/debounce'; +import _isObject from 'lodash/isObject'; +import { + mkGetReportDownloadProgress, + mkGetReportIsFetching, + mkGetReportLastFetchError, + mkGetReportDocument, +} from '../../state/reportSelectors'; +import Message from '../../../../Common/Message'; +import { humanReadableSize } from '../../../../Common/utils'; +import { COLUMN_WIDTH } from '../../../../Common/defaults'; + +const STARTING_MSG = 'Waiting to fetch Testplan report...'; +const FETCHING_MSG = 'Fetching Testplan report...'; +const FINISHED_MSG = 'Please select an entry.'; +const ERRORED_PREFIX_MSG = 'Error fetching Testplan report.'; + +const MessageStyled = props => ( + +); + +const connector = connect( + () => { + const getDocument = mkGetReportDocument(); + const getIsFetching = mkGetReportIsFetching(); + const getError = mkGetReportLastFetchError(); + const getProgress = _debounce(mkGetReportDownloadProgress(), 100); + return state => ({ + progress: getProgress(state), + isFetching: getIsFetching(state), + error: getError(state), + document: getDocument(state), + }); + }, +); + +export default connector(({ progress, isFetching, error, document }) => { + if(!isFetching && error) { + const sfx = (typeof error === 'object' ? error.message : 0) || ''; + return ( + + ); + } + if(isFetching) { + if(progress.lengthComputable) { + const pct = 100 * progress.loaded / progress.total; + const color = pct > 99 ? 'success' : 'info'; + const pctStr = pct.toFixed(1).padStart(5); + const MessageFilled = () => ( + <> +

    {`${FETCHING_MSG} ${pctStr}%`}

    + + + { + `${humanReadableSize(progress.loaded)}` + + ` / ` + + `${humanReadableSize(progress.total)}` + } + + + + ); + return (); + } + return (); + } + if(!isFetching && !error && _isObject(document)) { + return (); + } + return (); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/index.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/index.jsx new file mode 100644 index 000000000..31aef8fa4 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/CenterPane/index.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import connect from 'react-redux/es/connect/connect'; +import { createSelector } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import { mkGetUIFilter } from '../../state/uiSelectors'; +import { mkGetUISelectedTestCase } from '../../state/uiSelectors'; +import { mkGetReportDocument } from '../../state/reportSelectors'; +import AssertionPane from '../../../../AssertionPane/AssertionPane'; +import { COLUMN_WIDTH } from '../../../../Common/defaults'; +import Placeholder from './Placeholder'; + +const isNonemptyArray = v => Array.isArray(v) && v.length > 0; + +const connector = connect( + () => { + const getFilter = mkGetUIFilter(); + const getSelectedTestCase = createSelector( + mkGetUISelectedTestCase(), + tc => tc || {} + ); + const getDocument = createSelector( + mkGetReportDocument(), + doc => doc || {} + ); + return state => { + return { + filter: getFilter(state), + selectedTestCase: getSelectedTestCase(state), + document: getDocument(state), + leftWidth: `${(COLUMN_WIDTH || 0) + 1.5}`, + }; + }; + }, +); + +export default connector(({ + selectedTestCase, document, filter, leftWidth +}) => { + const { uid: reportUID } = document; + const { uid: testcaseUID, logs, entries, description } = selectedTestCase; + const descriptionEntries = React.useMemo( + () => isNonemptyArray(description) ? description : [ description ], + [ description ] + ); + if(isNonemptyArray(entries) || isNonemptyArray(entries)) { + return ( + + ); + } + return ; +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/DisplayEmptyCheckBox.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/DisplayEmptyCheckBox.jsx new file mode 100644 index 000000000..993f59b5e --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/DisplayEmptyCheckBox.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Label from 'reactstrap/lib/Label'; +import Input from 'reactstrap/lib/Input'; +import DropdownItem from 'reactstrap/lib/DropdownItem'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsDisplayEmpty } from '../state/uiSelectors'; +import { setDisplayEmpty } from '../state/uiActions'; +import navStyles from '../../../Toolbar/navStyles'; + +const connector = connect( + () => { + const getIsDisplayEmpty = mkGetUIIsDisplayEmpty(); + return state => ({ + isDisplayEmpty: getIsDisplayEmpty(state), + dropdownItemClasses: css(navStyles.dropdownItem), + filterLabelClasses: css(navStyles.filterLabel), + }); + }, + { + setDisplayEmpty, + }, + (stateProps, dispatchProps, ownProps) => { + const { + dropdownItemClasses, filterLabelClasses, isDisplayEmpty + } = stateProps; + const { setDisplayEmpty } = dispatchProps; + const { label } = ownProps; + return { + label: label || '', + dropdownItemClasses, + filterLabelClasses, + isDisplayEmpty, + onChange: () => setDisplayEmpty(!isDisplayEmpty), + }; + } +); + +export default connector(({ + label, isDisplayEmpty, onChange, dropdownItemClasses, filterLabelClasses, +}) => ( + + + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/DocumentationButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/DocumentationButton.jsx new file mode 100644 index 000000000..0748d8398 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/DocumentationButton.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import NavItem from 'reactstrap/lib/NavItem'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faBook } from '@fortawesome/free-solid-svg-icons'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetDocumentationURL } from '../../../state/appSelectors'; +import navStyles from '../../../Toolbar/navStyles'; + +library.add(faBook); + +const connector = connect( + () => { + const getDocsURL = mkGetDocumentationURL(); + return state => ({ + docsURL: getDocsURL(state), + docsIconName: faBook.iconName, + docsIconClasses: css(navStyles.toolbarButton), + docsAnchorClasses: css(navStyles.buttonsBar), + }); + }, +); + +export default connector(({ + docsURL, docsIconName, docsIconClasses, docsAnchorClasses +}) => ( + + + + + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/EmptyListGroupItem.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/EmptyListGroupItem.jsx new file mode 100644 index 000000000..06bb2720a --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/EmptyListGroupItem.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ListGroupItem from 'reactstrap/lib/ListGroupItem'; +import { css } from 'aphrodite/es'; +import { navUtilsStyles } from '../style'; + +const lgiClasses = css(navUtilsStyles.navButton); + +export default () => ( + + No entries to display... + +); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/FilterButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/FilterButton.jsx new file mode 100644 index 000000000..17766162b --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/FilterButton.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import DropdownItem from 'reactstrap/lib/DropdownItem'; +import DropdownMenu from 'reactstrap/lib/DropdownMenu'; +import DropdownToggle from 'reactstrap/lib/DropdownToggle'; +import UncontrolledDropdown from 'reactstrap/lib/UncontrolledDropdown'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { faFilter } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import connect from 'react-redux/es/connect/connect'; +import navStyles from '../../../Toolbar/navStyles'; +import FilterRadioButton from './FilterRadioButton'; +import DisplayEmptyCheckBox from './DisplayEmptyCheckBox'; +import * as filterStates from '../../../Common/filterStates'; + +library.add(faFilter); + +const connector = connect( + () => ({ + buttonsBarClasses: css(navStyles.buttonsBar), + filterIconName: faFilter.iconName, + dropdownButtonClasses: css(navStyles.toolbarButton), + filterDropdownClasses: css(navStyles.filterDropdown), + filter_ALL: filterStates.ALL, + filter_FAILED: filterStates.FAILED, + filter_PASSED: filterStates.PASSED, + }), +); + +export default connector(({ + toolbarStyle, buttonsBarClasses, filterIconName, dropdownButtonClasses, + filterDropdownClasses, filter_ALL, filter_PASSED, filter_FAILED +}) => ( + +
    + + + +
    + + + + + + + +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/FilterRadioButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/FilterRadioButton.jsx new file mode 100644 index 000000000..3b63b7dcb --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/FilterRadioButton.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import DropdownItem from 'reactstrap/lib/DropdownItem'; +import Input from 'reactstrap/lib/Input'; +import Label from 'reactstrap/lib/Label'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIFilter } from '../state/uiSelectors'; +import { setFilter } from '../state/uiActions'; +import navStyles from '../../../Toolbar/navStyles'; + +const connector = connect( + () => { + const getFilter = mkGetUIFilter(); + return state => ({ + filter: getFilter(state), + dropdownItemClasses: css(navStyles.dropdownItem), + filterLabelClasses: css(navStyles.filterLabel), + }); + }, + { + setFilter + }, + (stateProps, dispatchProps, ownProps) => { + const { filter, dropdownItemClasses, filterLabelClasses } = stateProps; + const { setFilter } = dispatchProps; + const { value, label } = ownProps; + return { + isChecked: filter === value, + onChange: evt => setFilter(evt.currentTarget.value), + value: value || '', + label: label || '', + dropdownItemClasses, + filterLabelClasses, + }; + }, +); + +export default connector(({ + isChecked, onChange, value, label, dropdownItemClasses, filterLabelClasses, +}) => ( + + + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/HelpButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/HelpButton.jsx new file mode 100644 index 000000000..701e89de7 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/HelpButton.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import NavItem from 'reactstrap/lib/NavItem'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsShowHelpModal } from '../state/uiSelectors'; +import { setShowHelpModal } from '../state/uiActions'; +import navStyles from '../../../Toolbar/navStyles'; + +library.add(faQuestionCircle); + +const connector = connect( + () => { + const getIsShowHelpModal = mkGetUIIsShowHelpModal(); + return state => ({ + isShowHelpModal: getIsShowHelpModal(state), + toolbarButtonClasses: css(navStyles.toolbarButton), + toolbarIconName: faQuestionCircle.iconName, + buttonsBarClasses: css(navStyles.buttonsBar), + }); + }, + { + setShowHelpModal, + }, + (stateProps, dispatchProps) => { + const { + isShowHelpModal, + toolbarButtonClasses, + toolbarIconName, + buttonsBarClasses, + } = stateProps; + const { setShowHelpModal } = dispatchProps; + return { + toolbarButtonClasses, + buttonsBarClasses, + toolbarIconName, + onClick: evt => { + evt.stopPropagation(); + setShowHelpModal(!isShowHelpModal); + }, + }; + }, +); + +/** + * Return the button which toggles the help modal. + * @returns {React.FunctionComponentElement} + */ +export default connector(({ + toolbarButtonClasses, buttonsBarClasses, toolbarIconName, onClick +}) => ( + +
    + + + +
    +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/HelpModal.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/HelpModal.jsx new file mode 100644 index 000000000..429fb8eaf --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/HelpModal.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import ModalHeader from 'reactstrap/lib/ModalHeader'; +import ModalFooter from 'reactstrap/lib/ModalFooter'; +import ModalBody from 'reactstrap/lib/ModalBody'; +import Modal from 'reactstrap/lib/Modal'; +import Button from 'reactstrap/lib/Button'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsShowHelpModal } from '../state/uiSelectors'; +import { setShowHelpModal } from '../state/uiActions'; + +const connector = connect( + () => { + const getIsShowHelpModal = mkGetUIIsShowHelpModal(); + return state => ({ + isShowHelpModal: getIsShowHelpModal(state), + }); + }, + { + setShowHelpModal, + }, + (stateProps, dispatchProps) => { + const { isShowHelpModal } = stateProps; + const { setShowHelpModal } = dispatchProps; + return { + isShowHelpModal, + toggleModal: () => setShowHelpModal(!isShowHelpModal), + }; + }, +); + +export default connector(({ isShowHelpModal, toggleModal }) => ( + + Help + + This is filter box help! + + + + + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/InfoButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoButton.jsx new file mode 100644 index 000000000..84b62ebac --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoButton.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import NavItem from 'reactstrap/lib/NavItem'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faInfo } from '@fortawesome/free-solid-svg-icons'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsShowInfoModal } from '../state/uiSelectors'; +import { setShowInfoModal } from '../state/uiActions'; +import navStyles from '../../../Toolbar/navStyles'; + +library.add(faInfo); + +const connector = connect( + () => { + const getIsShowInfoModal = mkGetUIIsShowInfoModal(); + return state => ({ + isShowInfoModal: getIsShowInfoModal(state), + toolbarIconName: faInfo.iconName, + toolbarIconClasses: css(navStyles.toolbarButton), + buttonsBarClasses: css(navStyles.buttonsBar), + }); + }, + { + setShowInfoModal, + }, + (stateProps, dispatchProps) => { + const { + isShowInfoModal, toolbarIconName, toolbarIconClasses, buttonsBarClasses + } = stateProps; + const { setShowInfoModal } = dispatchProps; + return { + buttonsBarClasses, + toolbarIconClasses, + toolbarIconName, + toggleInfo: evt => { + evt.stopPropagation(); + setShowInfoModal(!isShowInfoModal); + }, + }; + }, +); + +export default connector(({ + buttonsBarClasses, toolbarIconClasses, toolbarIconName, toggleInfo +}) => ( + +
    + + + +
    +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/InfoModal.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoModal.jsx new file mode 100644 index 000000000..f7abf8c85 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoModal.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import ModalHeader from 'reactstrap/lib/ModalHeader'; +import ModalFooter from 'reactstrap/lib/ModalFooter'; +import ModalBody from 'reactstrap/lib/ModalBody'; +import Modal from 'reactstrap/lib/Modal'; +import Button from 'reactstrap/lib/Button'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIIsShowInfoModal } from '../state/uiSelectors'; +import { setShowInfoModal } from '../state/uiActions'; +import InfoTable from './InfoTable'; + +const connector = connect( + () => { + const getIsShowInfoModal = mkGetUIIsShowInfoModal(); + return state => ({ + isShowInfoModal: getIsShowInfoModal(state), + }); + }, + { + setShowInfoModal, + }, + (stateProps, dispatchProps) => { + const { isShowInfoModal } = stateProps; + const { setShowInfoModal } = dispatchProps; + return { + isShowInfoModal, + toggleInfo: () => setShowInfoModal(!isShowInfoModal), + }; + }, +); + +export default connector(({ isShowInfoModal, toggleInfo }) => ( + + Information + + + + + + + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/InfoTable.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoTable.jsx new file mode 100644 index 000000000..983bb6494 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/InfoTable.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { css } from 'aphrodite/es'; +import Table from 'reactstrap/lib/Table'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetReportDocument } from '../state/reportSelectors'; +import navStyles from '../../../Toolbar/navStyles'; + +const connector = connect( + () => { + const getReportDocument = mkGetReportDocument(); + return state => ({ + reportDocument: getReportDocument(state), + infoTableClasses: css(navStyles.infoTable), + infoTableKeyClasses: css(navStyles.infoTableKey), + infoTableValClasses: css(navStyles.infoTableValue), + }); + }, +); + +export default connector(({ + reportDocument, infoTableClasses, infoTableKeyClasses, infoTableValClasses, +}) => React.useMemo(() => { + if(!(reportDocument && reportDocument.information)) { + return ( + + +
    No information to display.
    + ); + } + const infoList = reportDocument.information.map((item, i) => ( + + {item[0]} + {item[1]} + + )); + if(reportDocument.timer && reportDocument.timer.run) { + if(reportDocument.timer.run.start) { + infoList.push( + + start + {reportDocument.timer.run.start} + , + ); + } + if(reportDocument.timer.run.end) { + infoList.push( + + end + {reportDocument.timer.run.end} + , + ); + } + } + return ( + + {infoList} +
    + ); +}, [ + infoTableClasses, infoTableKeyClasses, infoTableValClasses, reportDocument, +])); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumb.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumb.jsx new file mode 100644 index 000000000..3ef3e4b0e --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumb.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { withRouter } from 'react-router'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { setSelectedTestCase } from '../state/uiActions'; +import StyledNavLink from './StyledNavLink'; +import { CommonStyles, navBreadcrumbStyles } from '../style'; +import NavEntry from '../../../Nav/NavEntry'; +import { safeGetNumPassedFailedErrored } from '../utils'; + +const connector = connect( + () => ({ + linkClasses: css( + navBreadcrumbStyles.breadcrumbEntry, + CommonStyles.unselectable, + ), + }), + { + setSelectedTestCase, + }, + (stateProps, dispatchProps, ownProps) => { + const { linkClasses } = stateProps; + const { setSelectedTestCase } = dispatchProps; + // `matchedUrl` is the matched Route, not necessarily the current URL + const { + entry: { + name: entryName, + status: entryStatus, + category: entryCategory, + counter: entryCounter, + uid: entryUid, + }, + match: { + url: matchedUrl, + }, + } = ownProps; + const [ + numPassed, + numFailed + ] = safeGetNumPassedFailedErrored(entryCounter, 0); + return { + linkClasses, + entryName, + entryStatus, + entryCategory, + entryUid, + numPassed, + numFailed, + matchedUrl, + onClick: () => setSelectedTestCase(null), + }; + } +); + +export default connector(withRouter(({ + linkClasses, entryName, entryStatus, entryCategory, entryUid, + numPassed, numFailed, matchedUrl, onClick, +}) => ( + + + +))); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbContainer.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbContainer.jsx new file mode 100644 index 000000000..273150123 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbContainer.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { navBreadcrumbStyles } from '../style'; + +const connector = connect( + () => ({ + navBreadcrumbClasses: css(navBreadcrumbStyles.navBreadcrumbs), + breadcrumbContainerClasses: css(navBreadcrumbStyles.breadcrumbContainer), + }), + null, + (stateProps, _, ownProps) => { + const { navBreadcrumbClasses, breadcrumbContainerClasses } = stateProps; + const { children } = ownProps; + return { + navBreadcrumbClasses, + breadcrumbContainerClasses, + children: children || null, + }; + }, +); + +export default connector(({ + children, navBreadcrumbClasses, breadcrumbContainerClasses, +}) => ( +
    +
      + {children} +
    +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbWithNextRoute.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbWithNextRoute.jsx new file mode 100644 index 000000000..880a06d09 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavBreadcrumbWithNextRoute.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Route } from 'react-router'; +import { withRouter } from 'react-router'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIHashComponentToAlias } from '../state/uiSelectors'; +import { setHashComponentAlias } from '../state/uiActions'; +import uriComponentCodec from '../../../Common/uriComponentCodec'; +import NavBreadcrumb from './NavBreadcrumb'; + +const connector = connect( + () => { + const getHashComponentToAlias = mkGetUIHashComponentToAlias(); + return state => ({ + hashComponentToAlias: getHashComponentToAlias(state), + }); + }, + { + setHashComponentAlias, + }, + (stateProps, dispatchProps, ownProps) => { + const { hashComponentToAlias } = stateProps; + const { setHashComponentAlias } = dispatchProps; + const { entries, url, match: { params: { id: encodedID } } } = ownProps; + let tgtEntry = null; + if(Array.isArray(entries)) { + let decodedID = hashComponentToAlias[encodedID]; + if(!decodedID) { + decodedID = uriComponentCodec.decode(encodedID); + setHashComponentAlias({ [decodedID]: encodedID }); + } + tgtEntry = entries.find(e => decodedID === e.name); + } + return { + tgtEntry, + url, + }; + }, +); + +const NavBreadcrumbWithNextRoute = connector(withRouter(({ tgtEntry, url }) => { + return !tgtEntry ? null : ( + <> + + + + }/> + + ); +})); + +export default NavBreadcrumbWithNextRoute; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavPanes.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavPanes.jsx new file mode 100644 index 000000000..2256ecea9 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavPanes.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Route } from 'react-router'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetReportIsFetching } from '../state/reportSelectors'; +import { mkGetReportLastFetchError } from '../state/reportSelectors'; +import { mkGetReportDocument } from '../state/reportSelectors'; +import EmptyListGroupItem from './EmptyListGroupItem'; +import NavBreadcrumbContainer from './NavBreadcrumbContainer'; +import NavBreadcrumbWithNextRoute from './NavBreadcrumbWithNextRoute'; +import NavSidebarWithNextRoute from './NavSidebarWithNextRoute'; +import AutoSelectRedirect from './AutoSelectRedirect'; + +const connector = connect( + () => { + const getReportIsFetching = mkGetReportIsFetching(); + const getReportLastFetchError = mkGetReportLastFetchError(); + const getReportDocument = mkGetReportDocument(); + return state => { + const document = getReportDocument(state); + return { + document, + documentEntries: [ document ], + lastFetchError: getReportLastFetchError(state), + isFetching: getReportIsFetching(state), + }; + }; + }, +); + +export default connector(({ + document, documentEntries, fetchError, isFetching +}) => (isFetching || fetchError || !document) ? : ( + <> + { + /** + * Here each path component adds a new breadcrumb to the top nav, + * and it sets up the next route that will receive the next path + * component when the user navigates further + */ + } + + ( + + )}/> + + { + /** + * Here each path component completely replaces the nav sidebar. + * This contains the links that will determine the next set of routes. + */ + } + + + }/> + + + }/> + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebar.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebar.jsx new file mode 100644 index 000000000..87b91d2d8 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebar.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import ListGroup from 'reactstrap/lib/ListGroup'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIFilter } from '../state/uiSelectors'; +import { isFilteredOut, safeGetNumPassedFailedErrored } from '../utils'; +import BoundStyledListGroupItemLink from './BoundStyledListGroupItemLink'; +import Column from '../../../Nav/Column'; +import { COLUMN_WIDTH } from '../../../Common/defaults'; +import { navListStyles } from '../style'; +import EmptyListGroupItem from './EmptyListGroupItem'; + +const connector = connect( + () => { + const getFilter = mkGetUIFilter(); + return state => ({ + filter: getFilter(state), + buttonListClasses: css(navListStyles.buttonList), + colWidthStr: `${COLUMN_WIDTH}`, + }); + }, +); + +export default connector(({ + entries, filter, buttonListClasses, colWidthStr +}) => { + const items = entries.map((entry, idx) => { + const [ + nPass, nFail, nErr, + ] = safeGetNumPassedFailedErrored(entry.counter, 0); + return isFilteredOut(filter, [ nPass, nFail, nErr ]) ? null : ( + + ); + }).filter(Boolean); + return ( + + + {items.length ? items : } + + + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebarWithNextRoute.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebarWithNextRoute.jsx new file mode 100644 index 000000000..806af58b6 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/NavSidebarWithNextRoute.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Redirect } from 'react-router'; +import { Route } from 'react-router'; +import { withRouter } from 'react-router'; +import connect from 'react-redux/es/connect/connect'; +import { setHashComponentAlias } from '../state/uiActions'; +import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults'; +import { mkGetUIHashComponentToAlias } from '../state/uiSelectors'; +import uriComponentCodec from '../../../Common/uriComponentCodec'; +import NavSidebar from './NavSidebar'; + +const connector = connect( + () => { + const getHashComponentToAlias = mkGetUIHashComponentToAlias(); + return state => ({ + hashComponentToAlias: getHashComponentToAlias(state), + }); + }, + { + setHashComponentAlias + }, + (stateProps, dispatchProps, ownProps) => { + const { hashComponentToAlias } = stateProps; + const { setHashComponentAlias } = dispatchProps; + const { + previousPath, + bottommostPath, + entries, + // Assume: + // - The route that was matched === "/aaa/bbb/ccc/:id" + // - The URL that matched === "/aaa/bbb/ccc/12345" + // Then the value of the following variables are: + // * url = "/aaa/bbb/ccc/12345" + // * path = "/aaa/bbb/ccc/:id" + // * params.id = "12345" + match: { url, params: { id: encodedID } }, + } = ownProps; + let tgtEntry = null; + if(Array.isArray(entries)) { + // ths incoming `encodedID` may be URL-encoded and so it won't match + // `entry.name` in the `entries` array, so we grab whatever `id` is + // actually an alias for, and use that to find our target `entry` object. + let decodedID = hashComponentToAlias[encodedID]; + // on refresh on an aliased path, the `componentAliases` will be empty so + // we need to fill it with the aliased component + if(!decodedID) { + decodedID = uriComponentCodec.decode(encodedID); + setHashComponentAlias({ [decodedID]: encodedID }); + } + tgtEntry = entries.find(e => decodedID === e.name); + } + return { + tgtEntry, + url, + previousPath, + bottommostPath, + }; + }, +); + +const NavSidebarWithNextRoute = connector(withRouter(({ + tgtEntry, url, previousPath, bottommostPath +}) => { + if(!tgtEntry) return null; + const isBottommost = tgtEntry.category === BOTTOMMOST_ENTRY_CATEGORY; + if(typeof bottommostPath === 'undefined' && isBottommost && previousPath) { + bottommostPath = previousPath; + } + const routePath = typeof bottommostPath === 'string' ? bottommostPath : url; + return ( + <> + + + }/> + + {(() => isBottommost ? : ( + + ) + )()} + + + ); +})); + +export default NavSidebarWithNextRoute; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/PrintButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/PrintButton.jsx new file mode 100644 index 000000000..d1b9db101 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/PrintButton.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import NavItem from 'reactstrap/lib/NavItem'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { faPrint } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import connect from 'react-redux/es/connect/connect'; +import navStyles from '../../../Toolbar/navStyles'; + +library.add(faPrint); + +const connector = connect( + () => ({ + buttonsBarClasses: css(navStyles.buttonsBar), + toolbarButtonClasses: css(navStyles.toolbarButton), + printIconName: faPrint.iconName, + }), +); + +export default connector(({ + buttonsBarClasses, toolbarButtonClasses, printIconName +}) => ( + +
    + + + +
    +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/StyledListGroupItemLink.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/StyledListGroupItemLink.jsx new file mode 100644 index 000000000..c16431b83 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/StyledListGroupItemLink.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ListGroupItem from 'reactstrap/lib/ListGroupItem'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import StyledNavLink from './StyledNavLink'; +import { navUtilsStyles } from '../style'; + +const connector = connect( + () => ({ + linkClasses: css( + navUtilsStyles.navButton, + navUtilsStyles.navButtonInteract, + ), + }), +); + +export default connector(({ linkClasses, pathname, dataUid, ...props }) => ( + +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/StyledNavLink.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/StyledNavLink.jsx new file mode 100644 index 000000000..32475325b --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/StyledNavLink.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import connect from 'react-redux/es/connect/connect'; +import { withRouter } from 'react-router'; +import { NavLink } from 'react-router-dom'; +import { css } from 'aphrodite/es'; +import { mkGetUISelectedTestCase } from '../state/uiSelectors'; +import { navUtilsStyles } from '../style'; + +const connector = connect( + () => { + const getSelectedTestCase = mkGetUISelectedTestCase(); + return state => ({ selectedTestCase: getSelectedTestCase(state) }); + }, + null, + (stateProps, _, ownProps) => { + const { selectedTestCase } = stateProps; + const { pathname, dataUid, style, location, ...props } = ownProps; + return { + style: style && typeof style === 'object' ? style : { + textDecoration: 'none', + color: 'currentColor', + }, + linkedLocation: { + search: location.search, + pathname: pathname.replace(/\/{2,}/g, '/'), + }, + isActive: () => ( + selectedTestCase && + (typeof selectedTestCase === 'object') && + selectedTestCase.uid === dataUid + ), + navButtonClasses: css(navUtilsStyles.navButtonInteract), + ...props, + }; + }, +); + +export default connector(withRouter(({ + style, linkedLocation, isActive, dataUid, navButtonClasses, ...props +}) => ( + +))); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/TagsButton.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/TagsButton.jsx new file mode 100644 index 000000000..9d53fa689 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/TagsButton.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import NavItem from 'reactstrap/lib/NavItem'; +import { css } from 'aphrodite/es'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome/index.es'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faTags } from '@fortawesome/free-solid-svg-icons'; +import connect from 'react-redux/es/connect/connect'; + +import { mkGetUIIsShowTags } from '../state/uiSelectors'; +import { setShowTags } from '../state/uiActions'; +import navStyles from '../../../Toolbar/navStyles'; + +library.add(faTags); + +const connector = connect( + () => { + const getIsShowTags = mkGetUIIsShowTags(); + return state => ({ isShowTags: getIsShowTags(state) }); + }, + { + setShowTags, + }, + (stateProps, dispatchProps) => { + const { isShowTags } = stateProps; + const { setShowTags } = dispatchProps; + return { + buttonsBarClasses: css(navStyles.buttonsBar), + toolbarButtonClasses: css(navStyles.toolbarButton), + tagsIconName: faTags.iconName, + onClick: evt => { + evt.stopPropagation(); + setShowTags(!isShowTags); + }, + }; + }, +); + +export default connector(({ + onClick, buttonsBarClasses, toolbarButtonClasses, tagsIconName +}) => ( + +
    + + + +
    +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/Toolbar.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/Toolbar.jsx new file mode 100644 index 000000000..37bf11020 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/Toolbar.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import InfoModal from './InfoModal'; +import HelpModal from './HelpModal'; +import TopNavbar from './TopNavbar'; + +export default function Toolbar({ children = null }) { + return ( +
    + + {children} + + + +
    + ); +} diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/TopNavbar.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/TopNavbar.jsx new file mode 100644 index 000000000..e9bd55742 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/TopNavbar.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { css } from 'aphrodite/es'; +import Navbar from 'reactstrap/lib/Navbar'; +import Nav from 'reactstrap/lib/Nav'; +import Collapse from 'reactstrap/lib/Collapse'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetUIToolbarStyle } from '../state/uiSelectors'; +import navStyles from '../../../Toolbar/navStyles'; +import FilterBox from '../../../Toolbar/FilterBox'; +import InfoButton from './InfoButton'; +import FilterButton from './FilterButton'; +import PrintButton from './PrintButton'; +import TagsButton from './TagsButton'; +import HelpButton from './HelpButton'; +import DocumentationButton from './DocumentationButton'; + +const connector = connect( + () => { + const getToolbarStyle = mkGetUIToolbarStyle(); + return state => ({ + toolbarStyle: getToolbarStyle(state), + toolbarClasses: css(navStyles.toolbar), + filterBoxClasses: css(navStyles.filterBox), + }); + } +); + +export default connector(({ + toolbarStyle, toolbarClasses, filterBoxClasses, children = null +}) => ( + +
    + +
    + + + +
    +)); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/AutoSelectRedirect.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/AutoSelectRedirect.test.jsx new file mode 100644 index 000000000..606e9849d --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/AutoSelectRedirect.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { AutoSelectRedirect } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('AutoSelectRedirect', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/BoundStyledListGroupItemLink.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/BoundStyledListGroupItemLink.test.jsx new file mode 100644 index 000000000..68f531d62 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/BoundStyledListGroupItemLink.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { BoundStyledListGroupItemLink } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('BoundStyledListGroupItemLink', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/CenterPane.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/CenterPane.test.jsx new file mode 100644 index 000000000..fa9768eb4 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/CenterPane.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { CenterPane } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('CenterPane', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DisplayEmptyCheckBox.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DisplayEmptyCheckBox.test.jsx new file mode 100644 index 000000000..373ea7f3f --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DisplayEmptyCheckBox.test.jsx @@ -0,0 +1,72 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; +import { DisplayEmptyCheckBox } from '../'; +import useReportState from '../../hooks/useReportState'; + +jest.mock('../../hooks/useReportState'); +const DISPLAY_EMPTY_LABEL = 'DISPLAY_EMPTY_LABEL'; + +describe('DisplayEmptyCheckBox', () => { + + beforeEach(() => { + useReportState.mockName('useReportState'); + StyleSheetTestUtils.suppressStyleInjection(); + }); + + it.each(_shuffle([ true, false ]))( + 'renders as expected when .checked === `isDisplayEmpty` === %j', + (isDisplayEmpty) => { + const setDisplayEmpty = jest.fn(); + useReportState.mockReturnValue([ isDisplayEmpty, setDisplayEmpty ]); + const { container } = render( + + ); + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + expect(checkboxes).toHaveLength(1); + expect(checkboxes[0].checked).toBe(!isDisplayEmpty); + expect(container).toMatchSnapshot(); + }, + ); + + it('grabs correct slices from useReportState', () => { + const expectedHookArgs = [ + 'app.reports.batch.isDisplayEmpty', + 'setAppBatchReportIsDisplayEmpty', + ]; + useReportState.mockReturnValue([ + false, jest.fn().mockName(expectedHookArgs[1]) + ]); + render(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + it('clicking checkbox calls `setAppBatchReportIsDisplayEmpty`', () => { + const expectedHookArgs = [ + 'app.reports.batch.isDisplayEmpty', + 'setAppBatchReportIsDisplayEmpty', + ]; + const setDisplayEmpty = jest.fn().mockName(expectedHookArgs[1]); + const isDisplayEmpty = true; + useReportState.mockReturnValue([ isDisplayEmpty, setDisplayEmpty ]); + const { container } = render( + + ); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + const checkbox = container.querySelector('input[type="checkbox"]'); + fireEvent.click(checkbox); + expect(setDisplayEmpty).toHaveBeenCalledTimes(1); + expect(setDisplayEmpty).toHaveBeenLastCalledWith(!isDisplayEmpty); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DocumentationButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DocumentationButton.test.jsx new file mode 100644 index 000000000..a6f98c428 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/DocumentationButton.test.jsx @@ -0,0 +1,53 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from 'react-dom'; +import { act } from 'react-dom/test-utils'; +import { StyleSheetTestUtils } from 'aphrodite/es'; +import _shuffle from 'lodash/shuffle'; + +import useReportState from '../../hooks/useReportState'; +jest.mock('../../hooks/useReportState'); + +describe('DocumentationButton', () => { + + const expectedArgs = [ 'documentation.url.external', false ]; + + describe.each(_shuffle([ + [ 'http://www.example.com/behavior/base.aspx' ], + [ 'http://www.example.com/' ], + [ 'http://www.example.com/boot/acoustics.php?basin=arch' ], + [ 'http://example.com/' ], + [ 'https://attraction.example.com/' ], + [ 'http://www.example.com/' ], + [ 'https://www.randomlists.com/urls' ], + ]))( + `{ [ "%s" ] = useReportState(...${JSON.stringify(expectedArgs)}) }`, + (docURL) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + global.container = window.document.createElement('div'); + global.document.body.appendChild(global.container); + useReportState.mockReturnValue([ docURL ]) + .mockName('useReportState'); + jest.isolateModules(() => { + const { DocumentationButton } = require('../'); + act(() => { render(, global.container); }); + }); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + global.document.body.removeChild(global.container); + global.container = null; + jest.resetAllMocks(); + }); + + it("correctly renders + calls hook & runs no event handlers", () => { + expect(global.container.querySelector('a').href).toBe(docURL); + expect(global.container).toMatchSnapshot(); + }); + }, + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/EmptyListGroupItem.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/EmptyListGroupItem.test.jsx new file mode 100644 index 000000000..821c2a70f --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/EmptyListGroupItem.test.jsx @@ -0,0 +1,16 @@ +/** @jest-environment jsdom */ +import React from 'react'; +import { EmptyListGroupItem } from '../'; +import { render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('EmptyListGroupItem', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + // using 'render' since this component takes no children + expect(render()).toMatchSnapshot(); + // TODO: add test that clicks on the button and checks that the correct docs + // open in a separate tab once puppeteer is setup + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterButton.test.jsx new file mode 100644 index 000000000..a29f19e40 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterButton.test.jsx @@ -0,0 +1,34 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { shallow } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; +import { FilterButton } from '../'; +import { getUIToolbarStyle } from '../../state/uiSelectors'; +import { STATUS_CATEGORY } from '../../../../Common/defaults'; + +const SORTED_STATUS_STYLES = (() => { + const a = Array.from(new Set(Object.values(STATUS_CATEGORY)).values()); + a.sort(); + return a.map(status => [ status, getUIToolbarStyle({ status }) ]); +})(); + +describe.each(SORTED_STATUS_STYLES)( + 'FilterButton, status="%s"', + (status, style) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + }); + + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + }, +); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterRadioButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterRadioButton.test.jsx new file mode 100644 index 000000000..7c9a8aa27 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/FilterRadioButton.test.jsx @@ -0,0 +1,91 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; +import _zip from 'lodash/zip'; +import { FilterRadioButton } from '../'; +import useReportState from '../../hooks/useReportState'; + +jest.mock('../../hooks/useReportState'); + +const FILTER_RADIO_LABEL = 'FILTER_RADIO_LABEL'; +const AVAIL_FILTERS = [ 'a', 'b', 'c' ]; +const FILTER_VALUE_COMBOS = AVAIL_FILTERS.reduce( + (prev, filter) => prev.concat(_zip( + /* value */ Array.from({ length: AVAIL_FILTERS.length }, () => filter), + /* filter */ AVAIL_FILTERS, + )), + [] +); + +describe('FilterRadioButton', () => { + + beforeEach(() => { + useReportState.mockName('useReportState'); + StyleSheetTestUtils.suppressStyleInjection(); + }); + + it.each(_shuffle(FILTER_VALUE_COMBOS))( + 'is checked only when filter === value, given { value: %j, filter: %j }', + (value, filter) => { + useReportState.mockReturnValue([ filter, jest.fn() ]); + const { container } = render( + + ); + const checkboxes = container.querySelectorAll('input[type="radio"]'); + expect(checkboxes).toHaveLength(1); + expect(checkboxes[0].checked).toBe(value === filter); + }, + ); + + it.each(_shuffle(FILTER_VALUE_COMBOS))( + 'renders correctly when { value: %j, filter: %j }', + (value, filter) => { + useReportState.mockReturnValue([ filter, jest.fn() ]); + expect(render( + + ).container).toMatchSnapshot(); + }); + + it('grabs correct slices from useReportState', () => { + const expectedHookArgs = [ + 'app.reports.batch.filter', + 'setAppBatchReportFilter', + ]; + useReportState.mockReturnValue([ + false, jest.fn().mockName(expectedHookArgs[1]) + ]); + render(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + it.each(_shuffle(AVAIL_FILTERS))( + 'clicking radio calls `setAppBatchReportFilter(%j)`', + (value) => { + const expectedHookArgs = [ + 'app.reports.batch.filter', + 'setAppBatchReportFilter', + ]; + const setFilter = jest.fn().mockName(expectedHookArgs[1]); + useReportState.mockReturnValue([ 'x', setFilter ]); + const { container } = render( + + ); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + const radioBtn = container.querySelector('input[type="radio"]'); + fireEvent.click(radioBtn); + expect(setFilter).toHaveBeenCalledTimes(1); + expect(setFilter).toHaveBeenLastCalledWith(value); + }, + ); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpButton.test.jsx new file mode 100644 index 000000000..2f7ef0ff0 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpButton.test.jsx @@ -0,0 +1,71 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from 'react-dom'; +import { act, Simulate } from 'react-dom/test-utils'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; + +import useReportState from '../../hooks/useReportState'; +jest.mock('../../hooks/useReportState'); + +describe('HelpButton', () => { + + const expectedArgs = [ + 'app.reports.batch.isShowHelpModal', + 'setAppBatchReportShowHelpModal' + ]; + const mkSetShowHelpModal = () => jest.fn().mockName(expectedArgs[1]); + const clickTgtQuery = 'span'; + + describe.each(_shuffle([ + [ true, mkSetShowHelpModal() ], + [ false, mkSetShowHelpModal() ], + ]))( + `{ [ %O, %p ] = useReportState(...${JSON.stringify(expectedArgs)}) }`, + (isShowHelpModal, setShowHelpModal) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + global.container = global.document.createElement('div'); + global.document.body.appendChild(global.container); + useReportState.mockReturnValue([ isShowHelpModal, setShowHelpModal ]) + .mockName('useReportState'); + jest.isolateModules(() => { + const { HelpButton } = require('../'); + act(() => { + render(, global.container); + }); + }); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + global.document.body.removeChild(global.container); + global.container = null; + jest.resetAllMocks(); + }); + + it("correctly renders + calls hook & runs no event handlers", () => { + expect(global.container).toMatchSnapshot(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + expect(setShowHelpModal).not.toHaveBeenCalled(); + }); + + describe(`<${clickTgtQuery.toUpperCase()}> onClick event`, () => { + beforeEach(() => { + Simulate.click(global.container.querySelector(clickTgtQuery)); + }); + it('correctly renders & handles onClick', () => { + expect(global.container).toMatchSnapshot(); + expect(setShowHelpModal).toHaveBeenCalledTimes(1); + expect(setShowHelpModal).not + .toHaveBeenLastCalledWith(isShowHelpModal); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + }); + }); + }, + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpModal.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpModal.test.jsx new file mode 100644 index 000000000..9e8688aef --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/HelpModal.test.jsx @@ -0,0 +1,79 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { StyleSheetTestUtils } from 'aphrodite'; +import { HelpModal } from '../'; +import useReportState from '../../hooks/useReportState'; + +jest.mock('../../hooks/useReportState'); + +describe('HelpModal', () => { + + beforeEach(() => { + // Jest doesn't clean up the JSDOM between tests. In these tests, this + // results in us having either `...` or + // `...` depending upon the order of the snapshots, + // meaning that the snapshots are not idempotent. This `removeAttribute` is + // an inelegant patch for this. + window.document.body.removeAttribute('class'); + useReportState.mockName('useReportState'); + StyleSheetTestUtils.suppressStyleInjection(); + }); + + it('renders as expected when modal is showing', () => { + useReportState.mockReturnValue([ true, jest.fn() ]); + const { baseElement } = render(); + /** + * We snapshot `baseElement` instead of the `container` + * container because reactstrap/lib/Modal* uses createPortal which renders + * above our container element. + * @see https://reactjs.org/docs/portals.html + * @see https://github.com/testing-library/react-testing-library/issues/62 + */ + expect(baseElement).toMatchSnapshot(); + }); + + it('renders as expected when modal is NOT showing', () => { + useReportState.mockReturnValue([ false, jest.fn() ]); + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); + }); + + it('grabs correct slices from useReportState', () => { + const expectedHookArgs = [ + 'app.reports.batch.isShowHelpModal', + 'setAppBatchReportShowHelpModal', + ]; + useReportState.mockReturnValue([ + false, jest.fn().mockName(expectedHookArgs[1]) + ]); + render(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + it('"Close" button calls `setAppBatchReportShowHelpModal`', () => { + const expectedHookArgs = [ + 'app.reports.batch.isShowHelpModal', + 'setAppBatchReportShowHelpModal', + ]; + const setShowHelpModal = jest.fn().mockName(expectedHookArgs[1]); + const isShowHelpModal = true; + useReportState.mockReturnValue([ isShowHelpModal, setShowHelpModal ]); + const { getAllByText } = render(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + const closeBtns = getAllByText('Close', { selector: 'button' }); + expect(closeBtns).toHaveLength(1); + fireEvent.click(closeBtns[0]); + expect(setShowHelpModal).toHaveBeenCalledTimes(1); + expect(setShowHelpModal).toHaveBeenLastCalledWith(!isShowHelpModal); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoButton.test.jsx new file mode 100644 index 000000000..6c753802e --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoButton.test.jsx @@ -0,0 +1,68 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from 'react-dom'; +import { act, Simulate } from 'react-dom/test-utils'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; + +import useReportState from '../../hooks/useReportState'; +jest.mock('../../hooks/useReportState'); + +describe('InfoButton', () => { + + const expectedArgs = [ + 'app.reports.batch.isShowInfoModal', + 'setAppBatchReportShowInfoModal' + ]; + const mkSetShowInfoModal = () => jest.fn().mockName(expectedArgs[1]); + const clickTgtQuery = 'span'; + + describe.each(_shuffle([ + [ true, mkSetShowInfoModal() ], + [ false, mkSetShowInfoModal() ], + ]))( + `{ [ %O, %p ] = useReportState(...${JSON.stringify(expectedArgs)}) }`, + (isShowInfoModal, setShowInfoModal) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + global.container = global.document.createElement('div'); + global.document.body.appendChild(global.container); + useReportState.mockReturnValue([ isShowInfoModal, setShowInfoModal ]) + .mockName('useReportState'); + jest.isolateModules(() => { + const { InfoButton } = require('../'); + act(() => { render(, global.container); }); + }); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + global.document.body.removeChild(global.container); + global.container = null; + jest.resetAllMocks(); + }); + + it("correctly renders + calls hook & runs no event handlers", () => { + expect(global.container).toMatchSnapshot(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + expect(setShowInfoModal).not.toHaveBeenCalled(); + }); + + // this sub-suite is needed in order to have a second snapshot + describe(`<${clickTgtQuery.toUpperCase()}> onClick event`, () => { + it('correctly renders & handles onClick', () => { + Simulate.click(global.container.querySelector(clickTgtQuery)); + expect(global.container).toMatchSnapshot(); + expect(setShowInfoModal).toHaveBeenCalledTimes(1); + // eslint-disable-next-line max-len + expect(setShowInfoModal).not.toHaveBeenLastCalledWith(isShowInfoModal); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + }); + }); + }, + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoModal.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoModal.test.jsx new file mode 100644 index 000000000..45df961e8 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoModal.test.jsx @@ -0,0 +1,69 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { shallow } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; +import { InfoModal } from '../'; +import useReportState from '../../hooks/useReportState'; + +jest.mock('../../hooks/useReportState'); + +describe('InfoModal', () => { + + beforeAll(() => { + useReportState.mockName('useReportState'); + }); + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + }); + + it('renders as expected when modal is showing', () => { + useReportState.mockReturnValue([ true, jest.fn() ]); + // we're using `shallow` because we don't want the results of these tests + // to depend upon the child component + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when modal is NOT showing', () => { + useReportState.mockReturnValue([ false, jest.fn() ]); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('grabs correct slices from useReportState', () => { + const expectedHookArgs = [ + 'app.reports.batch.isShowInfoModal', + 'setAppBatchReportShowInfoModal', + ]; + useReportState.mockReturnValue([ + false, jest.fn().mockName(expectedHookArgs[1]) + ]); + shallow(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + it('"Close" button calls `setAppBatchReportShowInfoModal`', () => { + const expectedHookArgs = [ + 'app.reports.batch.isShowInfoModal', + 'setAppBatchReportShowInfoModal', + ]; + const setShowInfoModal = jest.fn().mockName(expectedHookArgs[1]); + const isShowInfoModal = true; + useReportState.mockReturnValue([ isShowInfoModal, setShowInfoModal ]); + const wrapper = shallow(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + wrapper.find('Button').simulate('click'); + expect(setShowInfoModal).toHaveBeenCalledTimes(1); + expect(setShowInfoModal).toHaveBeenLastCalledWith(!isShowInfoModal); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoTable.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoTable.test.jsx new file mode 100644 index 000000000..f95adcfc1 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/InfoTable.test.jsx @@ -0,0 +1,44 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from '@testing-library/react'; +import { StyleSheetTestUtils } from 'aphrodite'; +import { InfoTable } from '../'; +import useReportState from '../../hooks/useReportState'; +import { filterObjectDeep } from '../../../../__tests__/testUtils'; +import { TESTPLAN_REPORT_2 } from '../../../../__tests__/documents'; + +jest.mock('../../hooks/useReportState'); + +const TESTPLAN_REPORT_2_SLIM = filterObjectDeep( + TESTPLAN_REPORT_2, + [ 'information', 'timer', 'run', 'start', 'end' ] +); + +describe('InfoTable', () => { + + beforeEach(() => { + useReportState.mockName('useReportState'); + StyleSheetTestUtils.suppressStyleInjection(); + }); + + it('renders as expected', () => { + useReportState.mockReturnValue([ TESTPLAN_REPORT_2_SLIM ]); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('grabs correct slices from useReportState', () => { + const expectedHookArgs = [ 'app.reports.batch.jsonReport', false ]; + useReportState.mockReturnValue([ TESTPLAN_REPORT_2_SLIM ]); + render(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumb.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumb.test.jsx new file mode 100644 index 000000000..3290be812 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumb.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { NavBreadcrumb } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('NavBreadcrumb', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbContainer.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbContainer.test.jsx new file mode 100644 index 000000000..e7c75c4a0 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbContainer.test.jsx @@ -0,0 +1,30 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from '@testing-library/react'; +import { StyleSheetTestUtils } from 'aphrodite'; +import { NavBreadcrumbContainer } from '../'; + +describe('NavBreadcrumbContainer', () => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + }); + + it('renders correctly with dummy child', () => { + expect(render( + +
  • Dummy LI element
  • +
    + ).container).toMatchSnapshot(); + }); + + it('renders correctly without children', () => { + expect(render().container).toMatchSnapshot(); + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbWithNextRoute.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbWithNextRoute.test.jsx new file mode 100644 index 000000000..4e5683577 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavBreadcrumbWithNextRoute.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { NavBreadcrumbWithNextRoute } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('NavBreadcrumbWithNextRoute', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavPanes.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavPanes.test.jsx new file mode 100644 index 000000000..4794e9333 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavPanes.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { NavPanes } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('NavPanes', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebar.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebar.test.jsx new file mode 100644 index 000000000..356a711a8 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebar.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { NavSidebar } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('NavSidebar', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebarWithNextRoute.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebarWithNextRoute.test.jsx new file mode 100644 index 000000000..43a74213c --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/NavSidebarWithNextRoute.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { NavSidebarWithNextRoute } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('NavSidebarWithNextRoute', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/PrintButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/PrintButton.test.jsx new file mode 100644 index 000000000..fc97c0436 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/PrintButton.test.jsx @@ -0,0 +1,18 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { PrintButton } from '../'; +import { render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('PrintButton', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + // using 'render' since this component takes no children + expect(render()).toMatchSnapshot(); + }); + // TODO: add test that clicks on the button and checks that the user is + // prompted to print once puppeteer is setup + it.todo('prompts user to print'); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledListGroupItemLink.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledListGroupItemLink.test.jsx new file mode 100644 index 000000000..e8e7cb8eb --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledListGroupItemLink.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { StyledListGroupItemLink } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('StyledListGroupItemLink', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledNavLink.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledNavLink.test.jsx new file mode 100644 index 000000000..653e959ab --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/StyledNavLink.test.jsx @@ -0,0 +1,272 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import '@testing-library/react/dont-cleanup-after-each'; +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { StyleSheetTestUtils, css } from 'aphrodite'; +import { Router } from 'react-router-dom'; +import _shuffle from 'lodash/shuffle'; +import _zip from 'lodash/zip'; +import _isEqual from 'lodash/isEqual'; +import _get from 'lodash/get'; +import { StyledNavLink } from '../'; +import useReportState from '../../hooks/useReportState'; +import { + deriveURLPathsFromReport +} from '../../../../__tests__/testUtils'; +import { filterObjectDeep } from '../../../../__tests__/testUtils'; +import { findAllDeep } from '../../../../__tests__/testUtils'; +import { navUtilsStyles } from '../../style'; +import { actionTypes } from '../../state'; +import { createMemoryHistory, createLocation, createPath } from 'history'; +import { TESTPLAN_REPORT_1 } from '../../../../__tests__/documents'; +import { TESTPLAN_REPORT_2 } from '../../../../__tests__/documents'; + +const { APP_BATCHREPORT_SELECTED_TEST_CASE } = actionTypes; +jest.mock('../../hooks/useReportState'); + +function uidFromPathname2ObjPathMapCurrier(obj, pathname2ObjPathMap) { + return pathname => { + const maybeErr = new Error(`Can't get UID for report entry at ${pathname}`); + const pathnameObjPath = pathname2ObjPathMap.get(pathname); + if(!pathnameObjPath) throw maybeErr; + const uidObjPath = pathnameObjPath.slice(0, -1).concat(['uid']); + const uid = _get(obj, uidObjPath); + if(!uid) throw maybeErr; + return uid; + }; +} + +const IS_ACTIVE_CLASSES = css(navUtilsStyles.navButtonInteract), + TESTPLAN_REPORT_1_SLIM = filterObjectDeep( + TESTPLAN_REPORT_1, + [ 'name', 'uid', 'entries', 'category' ] + ), + TESTPLAN_REPORT_2_SLIM = filterObjectDeep( + TESTPLAN_REPORT_2, + [ 'name', 'uid', 'entries', 'category' ] + ), + pathname2ObjectPathMap_TPR1 = new Map(), + TESTPLAN_REPORT_1_SLIM_URLS = deriveURLPathsFromReport( + TESTPLAN_REPORT_1_SLIM, + null, + null, + pathname2ObjectPathMap_TPR1 + ), + TESTPLAN_REPORT_1_SLIM_UIDS = TESTPLAN_REPORT_1_SLIM_URLS.map( + uidFromPathname2ObjPathMapCurrier( + TESTPLAN_REPORT_1_SLIM, + pathname2ObjectPathMap_TPR1 + ) + ), + TESTPLAN_REPORT_1_SLIM_URLS_UIDS = _zip( + TESTPLAN_REPORT_1_SLIM_URLS, + TESTPLAN_REPORT_1_SLIM_UIDS, + ), + pathname2ObjectPathMap_TPR2 = new Map(), + TESTPLAN_REPORT_2_SLIM_URLS = deriveURLPathsFromReport( + TESTPLAN_REPORT_2_SLIM, + null, + null, + pathname2ObjectPathMap_TPR2 + ), + TESTPLAN_REPORT_2_SLIM_UIDS = TESTPLAN_REPORT_2_SLIM_URLS.map( + uidFromPathname2ObjPathMapCurrier( + TESTPLAN_REPORT_2_SLIM, + pathname2ObjectPathMap_TPR2 + ) + ), + TESTPLAN_REPORT_2_SLIM_URLS_UIDS = _zip( + TESTPLAN_REPORT_2_SLIM_URLS, + TESTPLAN_REPORT_2_SLIM_UIDS, + ), + TESTPLAN_REPORT_URL_UID_MAP = new Map( + TESTPLAN_REPORT_1_SLIM_URLS_UIDS.concat(TESTPLAN_REPORT_2_SLIM_URLS_UIDS) + ); + +/// make quintuplets: +/// [ , , , , <0-index> ] +// current location.pathname +const startPathnames = Array.from(TESTPLAN_REPORT_URL_UID_MAP.keys()); +// linked-to location.pathname: the last "to" location is '' +const destPathnames = startPathnames.slice(1).concat(['/']); +// random querystrings ('a'.charCodeAt(0) == 97), length is 1 less than others +const randomQSs = startPathnames.slice(1).map( + (_, i) => `?${String.fromCharCode(97 + (i % 26))}=${i}` +); +// join each query string with each startPathnames, skipping the first +const startURLs = _zip(startPathnames, randomQSs.concat([''])).map( + ([pth, qs]) => `${pth}${qs}` +); +const urlIdx = startURLs.map((_, i) => i); +const startUIDs = Array.from(TESTPLAN_REPORT_URL_UID_MAP.values()); +const destUIDs = startUIDs.slice(1).concat(['']); +const URL_QUINTUPLETS = _zip( + startURLs, // current pathname+querystring + destPathnames, // linked-to pathname + startUIDs, + destUIDs, + urlIdx, // 0-index +); + +// here we: +// > get all testcases from our mock reports +// > shuffle them with a null +// > remove duplicates +const SAMPLE_TESTCASES = _shuffle([ null ].concat( + findAllDeep( + TESTPLAN_REPORT_1_SLIM, + { category: 'testcase' }, + [ 'entries' ], + ) +).concat( + findAllDeep( + TESTPLAN_REPORT_2_SLIM, + { category: 'testcase' }, + [ 'entries' ], + ) +).filter( // remove duplicates + (el, i, arr) => + !arr.slice(i + 1).find(_el => _isEqual(_el, el)) +)); + +global.env = { + history: createMemoryHistory({ + initialEntries: [ '/' ], + initialIndex: 1, + }), + hasRendered: false, + wrapper: ({ children }) => ( + + {children} + + ), +}; + +describe.each(SAMPLE_TESTCASES)( + ' with report %j', + (selectedTestCase) => { + + const expectedHookArgs = [ APP_BATCHREPORT_SELECTED_TEST_CASE ]; + + beforeAll(() => { + useReportState + .mockName('useReportState') + .mockReturnValue([ selectedTestCase ]); + StyleSheetTestUtils.suppressStyleInjection(); + }); + + describe.each(URL_QUINTUPLETS)( + 'current URL => linked-to pathname: %j => %j', + (currURL, toPathname, currUID, toUID, iterationNum) => { + + beforeAll(() => { + if(!global.env.hasRendered) { + global.env.rendered = render(( + + ), { + wrapper: global.env.wrapper, + }); + global.env.hasRendered = true; + } else { + global.env.rendered.rerender(( + + ), { wrapper: global.env.wrapper }); + } + global.env.linkElem = null; + }); + + it('calls useReportState correctly', () => { + // we call jest.resetAllMocks() so this might just be 1 + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedHookArgs); + }); + + it('renders as expected', () => { + expect(global.env.rendered.container).toMatchSnapshot(); + }); + + it(`${ + iterationNum > 0 + ? 'sent us to the URL we linked to previously while ' + + 'maintaining the query params' + : '(skipping)' + }`, () => { + if(iterationNum > 0) { + const { pathname: expectedCurrPathname } = createLocation(currURL); + // the previous query string should have been carried over + const { search: prevQS } = global.env.history.entries.slice(-1)[0]; + expect(global.env.history.location).toEqual( + expect.objectContaining({ + pathname: expectedCurrPathname, + search: prevQS, + }) + ); + } + }); + + it('has an anchor element correct href', () => { + const anchors = global.env.rendered.container.querySelectorAll('a'); + expect(anchors).toHaveLength(1); + // the component should have picked up our current query string + // even though we didn't pass it in as props + const expectedFullHref = createPath({ + pathname: toPathname, + search: global.env.history.location.search, + }); + expect(anchors[0]).toHaveAttribute('href', expectedFullHref); + global.env.linkElem = anchors[0]; + }); + + it('should react appropriately to a change in query params', () => { + // it's expected that the change in location will trigger a rerender + // and thus a change of the query params in the anchor's href + expect(useReportState).toHaveBeenCalledTimes(1); // no change + global.env.history.push(currURL); + const { search: expectedNewQueryParams } = createLocation(currURL); + expect(global.env.linkElem).toHaveAttribute( + 'href', createPath({ + pathname: toPathname, + search: expectedNewQueryParams, + }) + ); + expect(useReportState).toHaveBeenCalledTimes(2); + }); + + const isCurrUID = + selectedTestCase !== null && toUID === selectedTestCase.uid, + repl1 = isCurrUID ? '' : 'in', + repl2 = isCurrUID ? '=' : '!'; + it(`has ${repl1}active classes since testcase UID ${repl2}== currUID`, + () => { + if(isCurrUID) { + expect(global.env.linkElem).toHaveClass(IS_ACTIVE_CLASSES); + } else { + expect(global.env.linkElem).not.toHaveClass(IS_ACTIVE_CLASSES); + } + }, + ); + + it("doesn't malfunction when we click the link", () => { + expect(useReportState).toHaveBeenCalledTimes(2); // no change + fireEvent.click(global.env.linkElem); + // should rerender since location changed twice + // and it's using `useLocation` + expect(useReportState).toHaveBeenCalledTimes(3); + }); + + afterAll(() => { + jest.clearAllMocks(); + delete global.env.linkElem; + }); + + }); + + afterAll(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + jest.resetAllMocks(); + global.env.rendered.cleanup(); + }); + + }, +); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TagsButton.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TagsButton.test.jsx new file mode 100644 index 000000000..964695ba2 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TagsButton.test.jsx @@ -0,0 +1,76 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { render } from 'react-dom'; +import { act, Simulate } from 'react-dom/test-utils'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; + +import useReportState from '../../hooks/useReportState'; +jest.mock('../../hooks/useReportState'); + +describe('TagsButton', () => { + + const expectedArgs = [ + 'app.reports.batch.isShowTags', + 'setAppBatchReportIsShowTags' + ]; + const mkSetShowTags = () => jest.fn().mockName(expectedArgs[1]); + const clickTgtQuery = 'span'; + + describe.each(_shuffle([ + [ true, mkSetShowTags() ], + [ false, mkSetShowTags() ], + ]))( + `{ [ %O, %p ] = useReportState(...${JSON.stringify(expectedArgs)}) }`, + (isShowTags, setShowTags) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + global.container = global.document.createElement('div'); + global.document.body.appendChild(global.container); + useReportState + .mockReturnValue([ isShowTags, setShowTags ]) + .mockName('useReportState'); + /** + * This is necessary because TagsButton uses FontAwesomeIcon which + * keeps internally a count of its uses and changes its + * 'aria-labelledby' attribute depending upon that count, meaning the + * tests using it are not independent, i.e. the order in which the tests + * run determine what the snapshot will look like. This 'isolateModules' + * effectively resets that internal count on each run. + * @type {import("../").TagsButton} + */ + jest.isolateModules(() => { + const { TagsButton } = require('../'); + act(() => { render(, global.container); }); + }); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + global.document.body.removeChild(global.container); + global.container = null; + jest.resetAllMocks(); + }); + + it("correctly renders + calls hook & runs no event handlers", () => { + expect(global.container).toMatchSnapshot(); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + expect(setShowTags).not.toHaveBeenCalled(); + }); + + describe(`<${clickTgtQuery.toUpperCase()}> onClick event`, () => { + it('correctly renders & handles onClick', () => { + Simulate.click(global.container.querySelector(clickTgtQuery)); + expect(global.container).toMatchSnapshot(); + expect(setShowTags).toHaveBeenCalledTimes(1); + expect(setShowTags).not.toHaveBeenLastCalledWith(isShowTags); + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith(...expectedArgs); + }); + }); + }, + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/Toolbar.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/Toolbar.test.jsx new file mode 100644 index 000000000..150c660a6 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/Toolbar.test.jsx @@ -0,0 +1,11 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { Toolbar } from '../'; +import { shallow } from 'enzyme'; + +describe('Toolbar', () => { + it('renders correctly', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TopNavbar.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TopNavbar.test.jsx new file mode 100644 index 000000000..4d84267ac --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/TopNavbar.test.jsx @@ -0,0 +1,59 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +/* eslint-disable max-len */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; +import _shuffle from 'lodash/shuffle'; +import { TopNavbar } from '../'; +import useReportState from '../../hooks/useReportState'; +import { fakeReportAssertions } from '../../../../__tests__/documents'; +import { TESTPLAN_REPORT_2 as REPORT } from '../../../../__tests__/documents'; + +jest.mock('../../hooks/useReportState'); + +describe('TopNavbar', () => { + + const expectedArgs = [ 'app.reports.batch.jsonReport', false ]; + + describe.each(_shuffle([ + [ fakeReportAssertions ], + [ REPORT ], + ]))( + `{ [ %j ] = useReportState(...${JSON.stringify(expectedArgs)}) }`, + (jsonReport) => { + + beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); + global.container = global.document.createElement('div'); + Object.assign( + global, + { console: { ...console, _err: console.error, error: jest.fn() } }, + ); + global.document.body.appendChild(global.container); + useReportState.mockReturnValue([ jsonReport ]) + .mockName('useReportState'); + }); + + afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + let [ { error: { mock: { calls: c } }, _err, warn: w }, l ] = [ + global.console, + 0, + ]; + c.forEach((...a) => a[0]?.match(/\buseContext\b/m) ? l++ : _err(...a)); + if(l) w(`${l} useContext warnings`); + Object.assign( + global, { console: { error: _err, _err: null, err: null } }); + global.document.body.removeChild(global.container); + global.container = null; + jest.resetAllMocks(); + }); + + it('renders correctly', () => { + const tree = shallow(); + expect(tree).toMatchSnapshot(); + }); + }, + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/UIRouter.test.jsx b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/UIRouter.test.jsx new file mode 100644 index 000000000..b938a1a79 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/components/__tests__/UIRouter.test.jsx @@ -0,0 +1,14 @@ +/// +import React from 'react'; +import { UIRouter } from '../'; +import { shallow, mount, render } from 'enzyme'; +import { StyleSheetTestUtils } from 'aphrodite'; + +describe('UIRouter', () => { + beforeEach(() => StyleSheetTestUtils.suppressStyleInjection()); + afterEach(() => StyleSheetTestUtils.clearBufferAndResumeStyleInjection()); + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/index.jsx b/testplan/web_ui/testing/src/Report/BatchReport/index.jsx new file mode 100644 index 000000000..bfdfdad0c --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/index.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { css } from 'aphrodite/es'; +import connect from 'react-redux/es/connect/connect'; +import { mkGetIsDevel } from '../../state/appSelectors'; +import { mkGetIsTesting } from '../../state/appSelectors'; +import { mkGetSkipFetch } from '../../state/appSelectors'; +import CenterPane from './components/CenterPane'; +import Toolbar from './components/Toolbar'; +import UIRouter from './state/UIRouter'; +import NavPanes from './components/NavPanes'; +import { batchReportStyles } from './style'; +import { fetchReport } from './state/reportActions'; + +const BATCH_REPORT_CLASSES = css(batchReportStyles.batchReport); + +const connector = connect( + () => { + const getIsDevel = mkGetIsDevel(); + const getIsTesting = mkGetIsTesting(); + const getSkipFetch = mkGetSkipFetch(); + return state => ({ + isDevel: getIsDevel(state), + isTesting: getIsTesting(state), + skipFetch: getSkipFetch(state), + }); + }, + { fetchReport }, + (stateProps, dispatchProps, ownProps) => { + const { isDevel, isTesting, skipFetch } = stateProps; + const { match: { params: { id: uid } } } = ownProps; + const { fetchReport } = dispatchProps; + return { isDevel, isTesting, fetchReport, uid, skipFetch }; + } +); + +export default connector(({ isDevel, isTesting, fetchReport, uid, skipFetch }) => { + React.useEffect(() => { + if(!skipFetch) { + return fetchReport(uid).abort; + } + }, [ uid, skipFetch, fetchReport ]); + return ( + +
    + + + +
    + + ); +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/UIRouter.jsx b/testplan/web_ui/testing/src/Report/BatchReport/state/UIRouter.jsx new file mode 100644 index 000000000..cb9b64e24 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/UIRouter.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { createHashHistory } from 'history'; +import { Router } from 'react-router'; +import ErrorCatch from '../../../Common/ErrorCatch'; + +export const uiHistory = createHashHistory({ basename: '/' }); + +export default function UIRouter({ children }) { + return ( + + + {children} + + + ); +} diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/reportSlice.test.js b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/reportSlice.test.js new file mode 100644 index 000000000..2b3430fac --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/reportSlice.test.js @@ -0,0 +1,29 @@ +import configureStore from 'redux-mock-store'; +import appSlice from '../appSlice'; +import * as appActions from '../appActions'; +import * as appSelectors from '../appSelectors'; +import appMiddleware from '../appMiddleware'; + +describe('PLACEHOLDER', () => { + + beforeAll(() => { + + }); + + beforeEach(() => { + + }); + + it('PLACEHOLDER', () => { + + }); + + afterEach(() => { + + }); + + afterAll(() => { + + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/uiSlice.test.js b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/uiSlice.test.js new file mode 100644 index 000000000..2b3430fac --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/uiSlice.test.js @@ -0,0 +1,29 @@ +import configureStore from 'redux-mock-store'; +import appSlice from '../appSlice'; +import * as appActions from '../appActions'; +import * as appSelectors from '../appSelectors'; +import appMiddleware from '../appMiddleware'; + +describe('PLACEHOLDER', () => { + + beforeAll(() => { + + }); + + beforeEach(() => { + + }); + + it('PLACEHOLDER', () => { + + }); + + afterEach(() => { + + }); + + afterAll(() => { + + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useFetchReport.test.js b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useFetchReport.test.js new file mode 100644 index 000000000..f95f4e869 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useFetchReport.test.js @@ -0,0 +1,186 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import moxios from 'moxios'; +import { cleanup, renderHook } from '@testing-library/react-hooks'; +import useReportState from '../useReportState'; + +jest.mock('../useReportState'); + +describe('useFetchReport', () => { + + beforeAll(() => { + global.env = { + reportUid: '0123456789abcdeffedcba9876543210', + setJsonReport: jest.fn().mockName('setAppBatchReportJsonReport'), + setLoading: jest.fn().mockName('setAppBatchReportIsLoading'), + setFetching: jest.fn().mockName('setAppBatchReportIsFetching'), + setFetchError: jest.fn().mockName('setAppBatchReportFetchError'), + 'api.baseURL': 'http://ptth.iq/api', + 'api.headers': { 'X-Useless-Header': '1' }, + }; + useReportState.mockReturnValue([ + [ + global.env['api.baseURL'], + global.env['api.headers'], + ], + [ + global.env.setJsonReport, + global.env.setLoading, + global.env.setFetching, + global.env.setFetchError, + ], + ]).mockName('useReportState'); + }); + + it('cancels pending fetches when unmounted', done => { + return new Promise((mockResolve, mockReject) => { + let mockUnmount = null; + jest.mock('axios', () => { + const __CancelToken_source_rv = { + token: 'MOCK_CANCEL_TOKEN', + cancel: jest.fn().mockName('cancel'), + }; + return { + __esModule: true, + __CancelToken_source_rv, + default: { + create: jest.fn(() => ({ + get: jest.fn(() => new Promise((resolve, reject) => { + // we use `setImmediate` to ensure all promises are + // resolved / rejected in exactly the right order + setImmediate(() => { + if(mockUnmount === null) { + mockReject( + "somehow we got to the mocked " + + "`Axios.create().get()` without `mockUnmount` " + + "getting set to the `renderHook` function's " + + "returned `unmount` function." + ); + reject(); + } else { + // ensure promise is resolved immediately after unmount + mockUnmount(); + mockResolve(); + resolve(); + } + }); + })).mockName('get'), + })).mockName('create'), + CancelToken: { + source: jest.fn(() => { + return __CancelToken_source_rv; + }).mockName('source'), + }, + isCancel: jest.fn(), + }, + }; + }); + const { default: useFetchReport } = require('../useFetchReport'); + mockUnmount = renderHook( + () => useFetchReport(global.env.reportUid) + ).unmount; + setTimeout(() => { // safeguard to fail after 15 sec + mockReject('Timed out waiting for unmount'); + }, 15000); + }).then(() => { + const { __CancelToken_source_rv } = require('axios'); + expect(__CancelToken_source_rv.cancel).toHaveBeenCalledTimes(1); + done(); + }).catch(errMsg => { + done(new Error(errMsg)); + }); + }); + + describe('interacting with moxios API', () => { + + beforeEach(() => { + const { default: Axios } = require('axios'); + global.env.axiosInstance = Axios.create(); + moxios.install(global.env.axiosInstance); + const { default: useFetchReport } = require('../useFetchReport'); + global.env.rendered = renderHook( + // global.env.rendered = renderHook( + ({ reportUid, isDev, skipFetch, axiosInstance }) => { + return useFetchReport(reportUid, isDev, skipFetch, axiosInstance); + }, + { + initialProps: { + reportUid: global.env.reportUid, + isDev: false, + skipFetch: false, + axiosInstance: global.env.axiosInstance + } + } + ); + }); + + it('passes received report to `setAppBatchReportJsonReport`', done => { + return moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { entries: [] }, + }).then(response => { + const { setJsonReport } = global.env; + expect(setJsonReport).toHaveBeenCalledTimes(1); + expect(setJsonReport).toHaveBeenLastCalledWith(response.data); + done(); + }).catch(err => { + done(err); + }); + }); + }); + + it('passes request errors to `setFetchErrorCb`', done => { + return moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 401, + response: { entries: [] }, + }).then(response => { + const { setFetchError } = global.env; + expect(setFetchError).toHaveBeenCalledTimes(1); + const call_0_arg_0 = setFetchError.mock.calls[0][0]; + expect(call_0_arg_0).toBeInstanceOf(Error); + expect(call_0_arg_0.response).toBe(response); + done(); + }).catch(err => { + done(err); + }); + }); + }); + + it('passes non-request errors to `setFetchErrorCb`', done => { + return moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { entries: null }, // error's thrown from PropagateIndices + }).then(() => { + const { setFetchError } = global.env; + expect(setFetchError).toHaveBeenCalledTimes(1); + const call_0_arg_0 = setFetchError.mock.calls[0][0]; + expect(call_0_arg_0).toBeInstanceOf(Error); + done(); + }).catch(err => { + done(err); + }); + }); + }); + + afterEach(() => { + cleanup(); + moxios.uninstall(global.env.axiosInstance); + }); + + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + delete global.env; + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useReportState.test.js b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useReportState.test.js new file mode 100644 index 000000000..a40de8be4 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useReportState.test.js @@ -0,0 +1,469 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { act, cleanup, renderHook } from '@testing-library/react-hooks'; +import _at from 'lodash/at'; +import _shuffle from 'lodash/shuffle'; +import uriComponentCodec from '../../../../Common/uriComponentCodec'; +import useReportState from '../useReportState'; +import { + defaultState, actionCreators, actionTypes, ReportStateProvider +} from '../../state'; +import { + randomSamples, getPaths, +} from '../../../../__tests__/testUtils'; + +const HookWrapper = props => (); + +describe("'useReportState' hook", () => { + + afterEach(cleanup); + const ALL_STATE_PATH_STRINGS = getPaths(defaultState); + const ALL_STATE_PATH_ARRAYS = getPaths(defaultState, true); + const ALL_ACTION_CREATOR_PATH_STRINGS = getPaths(actionCreators); + const ALL_ACTION_CREATOR_PATH_ARRAYS = getPaths(actionCreators, true); + + it("returns null state and dispatchers when passed (false, false)", () => { + const { result } = renderHook( + () => useReportState(false, false), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ state, actionDispatch ] = result.current; + expect(state).toBeNull(); + expect(actionDispatch).toBeNull(); + }); + + it( + "returns full state and dispatchers when passed (undefined, undefined)", + () => { + const { result } = renderHook( + () => useReportState(), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ state, actionDispatchers ] = result.current; + expect(state).toStrictEqual(defaultState); + + // the action dispatchers object has an extra raw "dispatch" function + const actionDispatcherSansDispatchNames = + Object.keys(actionDispatchers).filter(nm => nm !== 'dispatch'); + actionDispatcherSansDispatchNames.sort(); + const actionCreatorNames = Object.keys(actionCreators); + actionCreatorNames.sort(); + expect(actionDispatcherSansDispatchNames).toEqual(actionCreatorNames); + + // check that the dispatchers take the same number of args as the creators + const actionDispatcherSansDispatchNArgs = + actionDispatcherSansDispatchNames + .map(nm => actionDispatchers[nm].length); + const actionCreatorNArgs = + actionCreatorNames + .map(nm => actionDispatchers[nm].length); + expect(actionDispatcherSansDispatchNArgs).toEqual(actionCreatorNArgs); + }, + ); + + describe('first returned value (state)', () => { + + it("returns the entire state when passed 'undefined''", () => { + const { result } = renderHook( + () => useReportState(undefined, false), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ state, actionDispatchers ] = result.current; + expect(actionDispatchers).toBeNull(); + expect(state).toStrictEqual(defaultState); + }); + + describe.each([ + /** ALL_STATE_PATH_STRINGS looks like [ 'a.b', 'a.b.c', ...] */ + ['string', ALL_STATE_PATH_STRINGS], + /** ALL_STATE_PATH_ARRAYS looks like [ ['a', 'b'], ['a', 'b', 'c'], ... ] */ + ['array', ALL_STATE_PATH_ARRAYS], + ])('slicers passed as %s', + (tp, slicers) => { + + /** + * Jest will implicitly convert a depth-1 array into an array-of-arrays + * which will break our tests unless we ensure no depth-1 arrays are + * passed to our it.each() tests. + * @example + * // After this conversion we should have: + * [ + * [ + * 'a.b' // passed to 1st it.each + * ], + * [ + * 'a.b.c' // passed to 2nd it.each + * ], + * ... // and so on + * ] + * // or + * [ + * [ + * [['a','b']] // passed to 1st it.each + * ], + * [ + * [['a','b','c']] // passed to 2nd it.each + * ], + * ... // and so on + * ] + * @see https://jestjs.io/docs/en/api#testeachtablename-fn-timeout + * @type {Array | Array>} + */ + const RESOLVED_SLICERS = + slicers.map(v => [ tp === 'string' ? v : [v] ]); + + /** + * Each `stateSlicer` is: + * 'a.b.c' + * -or- + * [['a','b','c']] + */ + it.each(RESOLVED_SLICERS)( + 'yields correct state slice when passed a single path %j', + (stateSlicer) => { + + const { result, rerender } = renderHook( + () => useReportState(stateSlicer, false), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ stateSlice1, actionDispatchers1 ] = result.current; + expect(actionDispatchers1).toBeNull(); + + expect(stateSlice1).not.toBeInstanceOf(Array); + // nothings been dispatched so everything should be at its default + const defaultStateSlice = _at(defaultState, stateSlicer)[0]; + expect(stateSlice1).toStrictEqual(defaultStateSlice); + + rerender([ stateSlicer ], false); + expect(result.error).toBeFalsy(); + const [ stateSlice2, actionDispatchers2 ] = result.current; + expect(actionDispatchers2).toBeNull(); + + // passing a singleton array should yield the same value as just + // passing the array's single value itself + expect(stateSlice2).toStrictEqual(stateSlice1); + }, + ); + + /** + * For the same reasons as in `RESOLVED_SLICERS` we apply a + * transformation to yield: + * [ + * [ + * ['a.b', 'a.b.c', ...] // passed to 1st it.each + * ], + * [ + * ['a', 'a.b.c.d', ...] // passed to 2nd it.each + * ], + * ... // and so on + * ] + * -or- + * [ + * [ + * [['a','b'], ['a','b','c'], ...] // passed to 1st it.each + * ], + * [ + * [['a'], ['a','b','c','d'], ...] // passed to 2nd it.each + * ], + * ... // and so on + * ] + */ + const RESOLVED_SLICER_SAMPLES = + randomSamples(slicers, 10, 2).map(v => [ v ]); + + /** + * Each `stateSlicerArr` is: + * ['a.b', 'a.b.c', ...] + * -or- + * [['a','b'], ['a','b','c'], ...] + */ + it.each(RESOLVED_SLICER_SAMPLES)( + `yields correct state slices when passed multiple paths %j`, + (stateSlicerArr) => { + + const { result } = renderHook( + () => useReportState(stateSlicerArr, false), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ stateSlices, actionDispatchers ] = result.current; + expect(actionDispatchers).toBeNull(); + + expect(stateSlices).toBeInstanceOf(Array); + const defaultStateSlices = _at(defaultState, stateSlicerArr); + expect(stateSlices).toStrictEqual(defaultStateSlices); + }, + ); + + }, + ); + + }); + + describe('second returned value (action dispatchers)', () => { + + it("returns all action dispatchers when passed 'undefined''", () => { + const { result } = renderHook( + () => useReportState(false, undefined), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ state, actionDispatchers ] = result.current; + expect(state).toBeNull(); + const allACNames = Object.keys(actionCreators); + allACNames.sort(); + const returnedADNames = Object.keys(actionDispatchers); + returnedADNames.sort(); + // the action dispatchers have an extra "dispatch" function so we just + // check that the actioin creator names are a subset of the action + // dispatcher names + expect(returnedADNames).toEqual(expect.arrayContaining(allACNames)); + }); + + describe.each([ + ['string', ALL_ACTION_CREATOR_PATH_STRINGS], + ['array', ALL_ACTION_CREATOR_PATH_ARRAYS], + ])('slicers passed as %s', + (tp, slicers) => { + + /** + * see the previous 'slicers passed as %s' for description + * @type {Array | Array>} + */ + const RESOLVED_SLICERS = + slicers.map(v => [ tp === 'string' ? v : [v] ]); + + it.each(RESOLVED_SLICERS)( + 'yields correct action dispatch slice when passed a single path %j', + (acSlicer) => { + + const { result, rerender } = renderHook( + () => useReportState(false, acSlicer), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ stateSlice1, actionDispatcher1 ] = result.current; + expect(stateSlice1).toBeNull(); + + expect(actionDispatcher1).not.toBeInstanceOf(Array); + const acSlice = _at(actionCreators, acSlicer)[0]; + expect(typeof actionDispatcher1).toBe('function'); + expect(actionDispatcher1.name).toBe(acSlice.name); + expect(actionDispatcher1.length).toBe(acSlice.length); + + rerender(false, [ acSlicer ]); + expect(result.error).toBeFalsy(); + const [ stateSlice2, actionDispatcher2 ] = result.current; + expect(stateSlice2).toBeNull(); + + expect(actionDispatcher2.name).toBe(actionDispatcher1.name); + expect(actionDispatcher2.length).toBe(actionDispatcher1.length); + }, + ); + + /** see the previous 'slicers passed as %s' for description */ + const RESOLVED_SLICER_SAMPLES = + randomSamples(slicers, 10, 2).map(v => [ v ]); + + it.each(RESOLVED_SLICER_SAMPLES)( + `yields correct action dispatch slices when passed multiple paths %j`, + (acSlicerArr) => { + + const { result } = renderHook( + () => useReportState(false, acSlicerArr), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + const [ stateSlices, actionDispatchers ] = result.current; + expect(stateSlices).toBeNull(); + + expect(actionDispatchers).toBeInstanceOf(Array); + + const defaultACs = _at(actionCreators, acSlicerArr); + for(let i = 0; i < actionDispatchers.length; ++i) { + const ad = actionDispatchers[i]; + const defaultAC = defaultACs[i]; + expect(typeof ad).toBe('function'); + expect(typeof ad).toBe(typeof defaultAC); + expect(ad.name).toBe(defaultAC.name); + expect(ad.length).toBe(defaultAC.length); + } + }, + ); + + }, + ); + + }); + + describe("action dispatchers' effects on state", () => { + + it.each(_shuffle( + /** + * @type {Array} ActionCreatorTestParams + * @property {string} ActionCreatorTestParams[0] - actionCreatorName + * @property {string} ActionCreatorTestParams[1] - stateSlice + * @property {any[]} ActionCreatorTestParams[2] - actionCreatorParams + * @property {any} ActionCreatorTestParams[3] - expectedStateVal + */ + [ + [ + 'mapUriHashQueryToState', + actionTypes.URI_HASH_QUERY, + [ '?a=1&b=true&c=%7B"x"%3A+null%7D' ], + new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) + ], + [ + 'mapUriQueryToState', + actionTypes.URI_QUERY, + [ '?a=1&b=true&c=%7B"x"%3A+null%7D' ], + new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) + ], + [ + 'saveUriHashQuery', + actionTypes.URI_HASH_QUERY, + [ new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) ], + new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) + ], + [ + 'saveUriQuery', + actionTypes.URI_QUERY, + [ new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) ], + new Map([ + [ 'a', 1 ], + [ 'b', true ], + [ 'c', { x: null } ], + ]) + ], + [ + 'setAppBatchReportDoAutoSelect', + actionTypes.APP_BATCHREPORT_DO_AUTO_SELECT, + [ false ], + false + ], + [ + 'setAppBatchReportFetchError', + actionTypes.APP_BATCHREPORT_FETCH_ERROR, + [ new Error('some message') ], + new Error('some message') + ], + [ + 'setAppBatchReportFilter', + actionTypes.APP_BATCHREPORT_FILTER, + [ 'all' ], + 'all' + ], + [ + 'setAppBatchReportIsDisplayEmpty', + actionTypes.APP_BATCHREPORT_DISPLAY_EMPTY, + [ true ], + true + ], + [ + 'setAppBatchReportIsFetching', + actionTypes.APP_BATCHREPORT_FETCHING, + [ null ], + null + ], + [ + 'setAppBatchReportIsLoading', + actionTypes.APP_BATCHREPORT_LOADING, + [ false ], + false + ], + [ + 'setAppBatchReportIsShowTags', + actionTypes.APP_BATCHREPORT_SHOW_TAGS, + [ true ], + true + ], + [ + 'setAppBatchReportJsonReport', + actionTypes.APP_BATCHREPORT_JSON_REPORT, + [ { type: 'testplan', uid: 'aaa-bbb-ccc-...' } ], + { type: 'testplan', uid: 'aaa-bbb-ccc-...' } + ], + [ + 'setAppBatchReportSelectedTestCase', + actionTypes.APP_BATCHREPORT_SELECTED_TEST_CASE, + [ { type: 'testcase', uid: 'aaa-bbb-ccc-...' } ], + { type: 'testcase', uid: 'aaa-bbb-ccc-...' } + ], + [ + 'setAppBatchReportShowHelpModal', + actionTypes.APP_BATCHREPORT_SHOW_HELP_MODAL, + [ false ], + false + ], + [ + 'setAppBatchReportShowInfoModal', + actionTypes.APP_BATCHREPORT_SHOW_INFO_MODAL, + [ true ], + true + ], + [ + 'setIsTesting', + actionTypes.IS_TESTING, + [ false ], + false + ], + [ + 'setUriHashPathComponentAlias', + actionTypes.URI_HASH_ALIASES, + [ uriComponentCodec.encode('Pre / Post Test'), 'Pre / Post Test' ], + new Map([ + [ uriComponentCodec.encode('Pre / Post Test'), 'Pre / Post Test' ] + ]) + ], + ] + ))( + "tests that action creator '%s' changes the state at object path '%s'", + (actionCreatorName, stateSlicer, actionCreatorParams, expectedState) => { + + const { result } = renderHook( + () => useReportState(stateSlicer, actionCreatorName), + { wrapper: HookWrapper }, + ); + expect(result.error).toBeFalsy(); + + const defaultStateSlice = _at(defaultState, stateSlicer)[0]; + const [ stateSliceInit, actionDispatch ] = result.current; + expect(stateSliceInit).toStrictEqual(defaultStateSlice); + + act(() => { + actionDispatch(...actionCreatorParams); + }); + const [ stateSliceAfter ] = result.current; + expect(result.error).toBeFalsy(); + expect(stateSliceAfter).toStrictEqual(expectedState); + }, + ); + + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useTargetEntry.test.js b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useTargetEntry.test.js new file mode 100644 index 000000000..fdd10c620 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/__tests__/useTargetEntry.test.js @@ -0,0 +1,225 @@ +/** @jest-environment jsdom */ +// @ts-nocheck +import React from 'react'; +import { cleanup, renderHook } from '@testing-library/react-hooks'; +import { Redirect, Route, Router, Switch, useParams } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import useTargetEntry from '../useTargetEntry'; +import { + deriveURLPathsFromReport, +} from '../../../../__tests__/testUtils'; +import { TESTPLAN_REPORT_1 } from '../../../../__tests__/documents'; +import { TESTPLAN_REPORT_2 } from '../../../../__tests__/documents'; +import { SIMPLE_REPORT } from '../../../../__tests__/documents'; +import { fakeReportAssertions } from '../../../../__tests__/documents'; + +import useReportState from '../useReportState'; +import { actionTypes } from '../../state'; +jest.mock('../useReportState'); + +describe("useTargetEntry", () => { + + describe.each([ + [ TESTPLAN_REPORT_1.name, TESTPLAN_REPORT_1 ], + [ TESTPLAN_REPORT_2.name, TESTPLAN_REPORT_2 ], + [ SIMPLE_REPORT.name, SIMPLE_REPORT ], + [ fakeReportAssertions.name, fakeReportAssertions ], + ])('(%#) using report "%s"', (currentReportName, currentReport) => { + + afterAll(() => { + cleanup(); + jest.resetAllMocks(); + delete global.env; + }); + + it("checks that we're using a familiar report", () => { + expect(currentReport).toMatchSnapshot(); + }); + + beforeAll(() => { + global.env = { + history: createMemoryHistory({ + initialEntries: [ + { + pathname: '', + state: { + currEntry: currentReport, + currEntryIdx: 0, + }, + } + ], + initialIndex: 0, + }), + }; + + global.env.rendered = renderHook( + useTargetEntry, + { + initialProps: global.env.history.location.state.currEntry.entries, + wrapper: ({ children }) => { + const { history, initialPathname, currentEntry } = global.env; + const redirectDest = `${initialPathname}/${currentEntry.name}`; + const { + currEntry: { entries }, + currEntryIdx, + } = global.env.history.location.state; + const nextEntry = entries[currEntryIdx]; + return ( + + + + {children} + + + + + + + ); + } + } + ); + }); + + const urlComponentAliasMap = new Map(), + urlPathsToArraysMap = new Map(), + allPaths = deriveURLPathsFromReport( + currentReport, + urlComponentAliasMap, + urlPathsToArraysMap, + ); + + describe.each(allPaths)("(%#) mocking navigation to '%s'", (urlPath) => { + + beforeAll(() => { + global.env.setUriHashPathComponentAlias = jest.fn() + .mockImplementation((id, encodedId) => { + urlComponentAliasMap.set(id, encodedId); + }).mockName('setUriHashPathComponentAlias'); + useReportState.mockReturnValue([ + urlComponentAliasMap, + global.env.setUriHashPathComponentAlias, + ]); + }); + + beforeEach(() => { + const urlPathArr = urlPathsToArraysMap.get(urlPath); + global.env.expectedNextEntryName = urlPathArr.slice(-1)[0]; + global.env.history.push(urlPath); + }); + + afterEach(() => { + const { + rendered: { rerender, result: { current: nextEntry } }, + initialPathname, currentEntry, history, + } = global.env; + history.push(`${history.location.pathname}/${nextEntry.name}`); + rerender(nextEntry.entries); + }); + + it("checks that 'useParams' is called as expected", () => { + expect(useParams).toHaveBeenCalledTimes(1); + expect(useParams).toHaveBeenLastCalledWith(); + }); + + it("checks that 'useReportState' is called as expected", () => { + expect(useReportState).toHaveBeenCalledTimes(1); + expect(useReportState).toHaveBeenLastCalledWith( + actionTypes.URI_HASH_ALIASES, 'setUriHashPathComponentAlias' + ); + }); + + it( + 'checks that we can correctly traverse the report', + function navigate(done) { + + const { + history: { location: { pathname: currentPathname } }, + rendered: { rerender, result: { current: nextEntry } }, + initialPathname, mockHistory, currentEntry, + } = global.env; + + if(nextEntry === undefined) { + // we didn't find an entry in `global.env.currentEntry.entries` that + // matches ... + expect(null).toBe(null); + } + + if(nextEntry === null) { + // we've reached the bottom of the report tree + // `prevEntries` at this point are from the parent entry in the + // report + expect(null).toBe(null); + } + + if(currentPathname === initialPathname) { + expect(null).toBe(null); + } + + if(mockHistory.length <= 1) { + // we're at the end of the top-level entries list, i.e. we've + // navigated recursively through all entries in the report + //done(); + //return; + expect(null).toBe(null); + } + + if(rerender === -1) { + // we're at the top of the report tree and have not recursed yet + expect(currentEntry).toEqual(expect.anything()); + } + + done(); + + }, + ); + + // it('returns non-null only if passed an array', () => { + // const mockId = 'Sample%20Testplan'; + // jest.mock('react-router', () => ({ + // // useParams: ReactRouter.useParams, + // useParams: jest.fn().mockReturnValue({ id: mockId }), + // })); + // const { result, rerender } = renderHook( + // useTargetEntry, + // { + // initialProps: TESTPLAN_REPORT, + // wrapper: ({ children }) => ( + // + // ), + // }, + // ); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender('abc'); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender(123); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender(1.23); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender(90071992547409919007199254740991n); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender(Symbol('xyz')); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender({ abc: 123 }); + // expect(result.error).toBeFalsy(); + // expect(result.current).toBeNull(); + // rerender([ { name: mockId } ]); + // expect(result.error).toBeFalsy(); + // expect(result.current).not.toBeNull(); + // }); + + }); + + }); + +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/fetchReport.js b/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/fetchReport.js new file mode 100644 index 000000000..83006d648 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/fetchReport.js @@ -0,0 +1,55 @@ +import { createAsyncThunk } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import * as reportSelectors from '../reportSelectors'; +import * as appSelectors from '../../../../state/appSelectors'; +import { PropagateIndices } from '../../../reportUtils'; + +export const REPORT_FETCH_CANCEL = 'REPORT_FETCH_CANCEL'; + +export default createAsyncThunk( + 'FETCH_REPORT', + async (reportUid, { dispatch, getState, signal, rejectWithValue }) => { + const { setDownloadProgress, setReportUID } = await import('./'); + const { default: Axios } = await import('axios/lib/core/Axios'); + try { + // setup fetch + dispatch(setReportUID(reportUid)); + const cancelSource = Axios.CancelToken.source(); + const axiosInstance = Axios.create({ + ...reportSelectors.getReportAxiosPartialConfig(getState()), + cancelToken: cancelSource.token, + }); + signal.addEventListener('abort', () => { + cancelSource.cancel('The fetch was cancelled.'); + }); + // execute fetch + const response = await axiosInstance.request({ + url: `/${reportUid}`, + method: 'GET', + onDownloadProgress: progress => dispatch(setDownloadProgress(progress)), + }); + // process response + return PropagateIndices(response.data); + } catch(err) { + if(Axios.isCancel(err)) { + return rejectWithValue(REPORT_FETCH_CANCEL); + } else { + return rejectWithValue({ + name: err.name, + message: err.message, + stack: err.stack, + }); + } + } + }, + // @ts-ignore + { + condition(reportUid, { getState }) { + const currReportUid = reportSelectors.getReportUid(getState()); + const isFetching = reportSelectors.getReportIsFetching(getState()); + if(isFetching && reportUid === currReportUid) { + return false; + } + }, + dispatchConditionRejection: false, + } +); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/index.js b/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/index.js new file mode 100644 index 000000000..2ce361c7f --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/reportActions/index.js @@ -0,0 +1,10 @@ +import reportSlice from '../reportSlice'; + +export const { + setMaxContentLength, + setDownloadProgress, + setReportUID, + setFetchTimeout, +} = reportSlice.actions; + +export { default as fetchReport } from './fetchReport'; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/reportSelectors.js b/testplan/web_ui/testing/src/Report/BatchReport/state/reportSelectors.js new file mode 100644 index 000000000..50b4acb2a --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/reportSelectors.js @@ -0,0 +1,55 @@ +import { createSelector } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import * as appSelectors from '../../../state/appSelectors'; + +export const mkGetReportUid = () => st => st.uid; +export const getReportUid = mkGetReportUid(); + +export const mkGetReportDocument = () => st => st.document; +export const getReportDocument = mkGetReportDocument(); + +export const mkGetReportIsFetching = () => st => st.report.isFetching; +export const getReportIsFetching = mkGetReportIsFetching(); + +export const mkGetReportIsFetchCancelled = () => st => { + return st.report.isFetchCancelled; +}; +export const getReportIsFetchCancelled = mkGetReportIsFetchCancelled(); + +export const mkGetReportLastFetchError = () => st => st.report.fetchError; +export const getReportLastFetchError = mkGetReportLastFetchError(); + +export const mkGetReportMaxContentLength = () => st => { + return st.report.maxContentLength; +}; +export const getReportMaxContentLength = mkGetReportMaxContentLength(); + +export const mkGetReportFetchTimeout = () => st => st.report.fetchTimeout; +export const getReportFetchTimeout = mkGetReportFetchTimeout(); + +export const mkGetReportDownloadProgress = () => st => { + return st.report.downloadProgress; +}; +export const getReportDownloadProgress = mkGetReportDownloadProgress(); + +export const mkGetReportApiBaseURL = () => createSelector( + appSelectors.mkGetApiBaseURL(), + baseURL => { + const baseURLShaven = (baseURL || '').replace(/\/$/, ''); + return new URL(`${baseURLShaven}/reports`).href; + } +); +export const getReportApiBaseURL = mkGetReportApiBaseURL(); + +export const mkGetMaxContentLength = () => st => st.report.maxContentLength; +export const getMaxContentLength = mkGetMaxContentLength(); + +export const mkGetReportAxiosPartialConfig = () => createSelector( + appSelectors.getApiBaseURL, + getReportFetchTimeout, + appSelectors.getApiHeaders, + getMaxContentLength, + (baseURL, timeout, headers, maxContentLength) => ({ + baseURL, timeout, headers, maxContentLength, + }) +); +export const getReportAxiosPartialConfig = mkGetReportAxiosPartialConfig(); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/reportSlice.js b/testplan/web_ui/testing/src/Report/BatchReport/state/reportSlice.js new file mode 100644 index 000000000..3d0f2ade5 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/reportSlice.js @@ -0,0 +1,68 @@ +// @ts-nocheck +import _isObject from 'lodash/isObject'; +import { createSlice } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import fetchReport, { REPORT_FETCH_CANCEL } from './reportActions/fetchReport'; + +const DEFAULT_TIMEOUT = 3_000; +const DEFAULT_MAX_CONTENT_LENGTH = Infinity; +const INIT_PROGRESS = { loaded: 0, total: -1, lengthComputable: false }; + +export default createSlice({ + name: 'report', + initialState: { + uid: null, + document: null, + isFetching: false, + isFetchCancelled: false, + fetchError: null, + maxContentLength: DEFAULT_MAX_CONTENT_LENGTH, + fetchTimeout: DEFAULT_TIMEOUT, + downloadProgress: INIT_PROGRESS, + }, + reducers: { + setMaxContentLength: { + reducer(state, { payload }) { state.maxContentLength = payload; }, + prepare: (length = DEFAULT_MAX_CONTENT_LENGTH) => ({ payload: length }), + }, + setDownloadProgress: { + reducer(state, { payload }) { state.downloadProgress = payload; }, + prepare: (progress = INIT_PROGRESS) => ({ payload: progress }), + }, + setReportUID: { + reducer(state, { payload }) { state.uid = payload; }, + prepare: (uid = null) => ({ payload: uid }), + }, + setFetchTimeout: { + reducer(state, { payload }) { state.fetchTimeout = payload; }, + prepare: (timeout = DEFAULT_TIMEOUT) => ({ payload: timeout }), + }, + }, + extraReducers: { + [fetchReport.pending.type](state) { + state.isFetching = true; + state.isFetchCancelled = false; + }, + [fetchReport.fulfilled.type](state, action) { + state.isFetching = false; + state.isFetchCancelled = false; + state.fetchError = null; + state.document = action.payload; + }, + [fetchReport.rejected.type](state, action) { + state.isFetching = false; + if(_isObject(action.error) && action.error.message === 'Rejected') { + // handled with rejectWithValue + const { payload: rejectValue } = action; + if(rejectValue === REPORT_FETCH_CANCEL) { + state.isFetchCancelled = true; + } else { + state.fetchError = rejectValue; + state.isFetchCancelled = false; + } + } else { + state.fetchError = action.error; + state.isFetchCancelled = false; + } + }, + }, +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/uiActions.js b/testplan/web_ui/testing/src/Report/BatchReport/state/uiActions.js new file mode 100644 index 000000000..b9dd44289 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/uiActions.js @@ -0,0 +1,14 @@ +import uiSlice from './uiSlice'; + +export const { + setShowTags, + setShowInfoModal, + setDoAutoSelect, + setFilter, + setDisplayEmpty, + setShowHelpModal, + setHashComponentAlias, + unsetHashComponentAliasByAlias, + unsetHashComponentAliasByComponent, + setSelectedTestCase, +} = uiSlice.actions; diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/uiMiddleware.js b/testplan/web_ui/testing/src/Report/BatchReport/state/uiMiddleware.js new file mode 100644 index 000000000..2f034b964 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/uiMiddleware.js @@ -0,0 +1,17 @@ +import URLParamRegistry from '../../../Common/URLParamRegistry'; +import { uiHistory } from './UIRouter'; +import { setShowTags } from './uiActions'; +import { setShowInfoModal } from './uiActions'; +import { setDoAutoSelect } from './uiActions'; +import { setFilter } from './uiActions'; +import { setDisplayEmpty } from './uiActions'; +import { setShowHelpModal } from './uiActions'; + +export default new URLParamRegistry(uiHistory) + .registerBidirectionalListener('showTags', setShowTags) + .registerBidirectionalListener('showInfoModal', setShowInfoModal) + .registerBidirectionalListener('doAutoSelect', setDoAutoSelect) + .registerBidirectionalListener('filter', setFilter) + .registerBidirectionalListener('displayEmpty', setDisplayEmpty) + .registerBidirectionalListener('showHelpModal', setShowHelpModal) + .createMiddleware(); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/uiSelectors.js b/testplan/web_ui/testing/src/Report/BatchReport/state/uiSelectors.js new file mode 100644 index 000000000..83df59990 --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/uiSelectors.js @@ -0,0 +1,57 @@ +import { createSelector } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import _has from 'lodash/has'; +import { mkGetReportDocument } from './reportSelectors'; +import navStyles from '../../../Toolbar/navStyles'; +import { STATUS_CATEGORY } from '../../../Common/defaults'; + +export const mkGetUIHashAliasToComponent = () => st => { + return st.ui.hashAliasToComponent; +}; +export const getUIHashAliasToComponent = mkGetUIHashAliasToComponent(); + +export const mkGetUIHashComponentToAlias = () => st => { + return st.ui.hashComponentToAlias; +}; +export const getUIHashComponentToAlias = mkGetUIHashComponentToAlias(); + +export const mkGetUIIsShowHelpModal = () => st => st.ui.isShowHelpModal; +export const getUIIsShowHelpModal = mkGetUIIsShowHelpModal(); + +export const mkGetUIIsDisplayEmpty = () => st => st.ui.isDisplayEmpty; +export const getUIIsDisplayEmpty = mkGetUIIsDisplayEmpty(); + +export const mkGetUIFilter = () => st => st.ui.filter; +export const getUIFilter = mkGetUIFilter(); + +export const mkGetUIIsShowTags = () => st => st.ui.isShowTags; +export const getUIIsShowTags = mkGetUIIsShowTags(); + +export const mkGetUIIsShowInfoModal = () => st => st.ui.isShowInfoModal; +export const getUIIsShowInfoModal = mkGetUIIsShowInfoModal(); + +export const mkGetUISelectedTestCase = () => st => st.ui.selectedTestCase; +export const getUISelectedTestCase = mkGetUISelectedTestCase(); + +export const mkGetUIDoAutoSelect = () => st => st.ui.doAutoSelect; +export const getUIDoAutoSelect = mkGetUIDoAutoSelect(); + +export const mkGetUIToolbarStyle = () => createSelector( + mkGetReportDocument(), + document => { + const status = _has(document, 'status') ? document.status : null; + const category = STATUS_CATEGORY[status]; + if(category) { + const style = { + passed: navStyles.toolbarPassed, + failed: navStyles.toolbarFailed, + error: navStyles.toolbarFailed, + unstable: navStyles.toolbarUnstable, + }[category]; + if(style) { + return style; + } + } + return navStyles.toolbarUnknown; + } +); +export const getUIToolbarStyle = mkGetUIToolbarStyle(); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/state/uiSlice.js b/testplan/web_ui/testing/src/Report/BatchReport/state/uiSlice.js new file mode 100644 index 000000000..3cdf6241c --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/state/uiSlice.js @@ -0,0 +1,89 @@ +// @ts-nocheck +import { createSlice } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import * as filterStates from '../../../Common/filterStates'; + +/** This state slice contains information specific to how the UI should look */ +export default createSlice({ + name: 'ui', + initialState: { + hashAliasToComponent: {}, + hashComponentToAlias: {}, + isShowHelpModal: false, + isDisplayEmpty: true, + filter: filterStates.ALL, + isShowTags: false, + isShowInfoModal: false, + selectedTestCase: null, + doAutoSelect: true, + }, + reducers: { + setHashComponentAlias: { + reducer(state, { payload: aliasToComponentMap }) { + for(const [ alias, component ] of Object.entries(aliasToComponentMap)) { + state.hashAliasToComponent[alias] = component; + state.hashComponentToAlias[component] = alias; + } + }, + prepare: aliasToComponentMap => ({ payload: aliasToComponentMap }), + }, + unsetHashComponentAliasByAlias: { + reducer(state, { payload: aliases }) { + for(const _alias of aliases) { + const component = state.hashAliasToComponent[_alias]; + delete state.hashComponentToAlias[component]; + delete state.hashAliasToComponent[_alias]; + } + }, + prepare: (aliases = []) => ({ + payload: Array.isArray(aliases) ? aliases : [ aliases ], + }), + }, + unsetHashComponentAliasByComponent: { + reducer(state, { payload: components }) { + for(const _component of components) { + const alias = state.hashComponentToAlias[_component]; + delete state.hashAliasToComponent[alias]; + delete state.hashComponentToAlias[_component]; + } + }, + prepare: (components = []) => ({ + payload: Array.isArray(components) ? components : [ components ], + }), + }, + setSelectedTestCase: { + reducer(state, { payload }) { state.selectedTestCase = payload; }, + prepare: message => ({ payload: message }), + }, + setShowTags: { + reducer(state, { payload }) { state.isShowTags = payload; }, + prepare: (showTags = false) => ({ payload: !!showTags }), + }, + setShowInfoModal: { + reducer(state, { payload }) { state.isShowInfoModal = payload; }, + prepare: (showInfoModal = false) => ({ payload: !!showInfoModal }), + }, + setDoAutoSelect: { + reducer(state, { payload }) { state.doAutoSelect = payload; }, + prepare: (doAutoSelect = true) => ({ payload: !!doAutoSelect }), + }, + setFilter: { + reducer(state, { payload }) { state.filter = payload; }, + prepare: (filter = filterStates.ALL) => { + if(!(filter in Object.values(filterStates))) { + filter = filterStates.ALL; + } + return { + payload: `${filter}` + }; + }, + }, + setDisplayEmpty: { + reducer(state, { payload }) { state.isDisplayEmpty = payload; }, + prepare: (displayEmpty = true) => ({ payload: !!displayEmpty }), + }, + setShowHelpModal: { + reducer(state, { payload }) { state.isShowHelpModal = payload; }, + prepare: (showHelpModal = false) => ({ payload: !!showHelpModal }), + }, + }, +}); diff --git a/testplan/web_ui/testing/src/Report/BatchReport/style.js b/testplan/web_ui/testing/src/Report/BatchReport/style.js new file mode 100644 index 000000000..16947c11e --- /dev/null +++ b/testplan/web_ui/testing/src/Report/BatchReport/style.js @@ -0,0 +1,20 @@ +import { StyleSheet } from 'aphrodite/es'; + +export { default as CommonStyles } from '../../Common/Styles'; +export { styles as navBreadcrumbStyles } from '../../Nav/NavBreadcrumbs'; +export { styles as navUtilsStyles } from '../../Nav/navUtils'; +export { COLUMN_WIDTH } from '../../Common/defaults'; + +export const navListStyles = StyleSheet.create({ + buttonList: { + 'overflow-y': 'auto', + 'height': '100%', + } +}); + +export const batchReportStyles = StyleSheet.create({ + batchReport: { + /** overflow will hide dropdown div */ + // overflow: 'hidden' + } +}); diff --git a/testplan/web_ui/testing/src/Report/InteractiveReport.js b/testplan/web_ui/testing/src/Report/InteractiveReport.js index a91c808a6..2ececfc9a 100644 --- a/testplan/web_ui/testing/src/Report/InteractiveReport.js +++ b/testplan/web_ui/testing/src/Report/InteractiveReport.js @@ -1,3 +1,5 @@ +// @ts-nocheck +/* eslint-disable @typescript-eslint/unbound-method */ /** * InteractiveReport: Renders an Interactive report, which is used to control * test environments and run tests interactively. Requires the Testplan @@ -7,18 +9,18 @@ import React from 'react'; import { StyleSheet, css } from 'aphrodite'; import axios from 'axios'; -import Toolbar from '../Toolbar/Toolbar.js'; +import Toolbar from '../Toolbar/Toolbar'; import { ResetButton } from '../Toolbar/InteractiveButtons'; -import InteractiveNav from '../Nav/InteractiveNav.js'; +import InteractiveNav from '../Nav/InteractiveNav'; import { INTERACTIVE_COL_WIDTH } from "../Common/defaults"; -import { FakeInteractiveReport } from '../Common/sampleReports.js'; +import { FakeInteractiveReport } from '../__tests__/documents'; import { PropagateIndices, UpdateSelectedState, GetReportState, GetCenterPane, GetSelectedEntries, -} from './reportUtils.js'; +} from './reportUtils'; /** * Interactive report viewer. As opposed to a batch report, an interactive @@ -60,7 +62,7 @@ class InteractiveReport extends React.Component { * If running in dev mode we just display a fake report. */ getReport() { - if (this.props.dev) { + if(this.props.dev) { setTimeout( () => this.setState({ report: FakeInteractiveReport, @@ -72,8 +74,8 @@ class InteractiveReport extends React.Component { } else { axios.get('/api/v1/interactive/report') .then(response => { - if (!this.state.report || - this.state.report.hash !== response.data.hash) { + if(!this.state.report || + this.state.report.hash !== response.data.hash) { this.getTests().then(tests => { const rawReport = { ...response.data, entries: tests }; const processedReport = PropagateIndices(rawReport); @@ -94,7 +96,7 @@ class InteractiveReport extends React.Component { }); // We poll for updates to the report every second. - if (this.props.poll_ms) { + if(this.props.poll_ms) { setTimeout(this.getReport, this.props.poll_ms); } } @@ -371,8 +373,8 @@ class InteractiveReport extends React.Component { * Handle an environment toggle button being clicked on a Nav entry. * * @param {object} e - Click event - * @param {ReportNode} reportEntry - entry in the report whose environment - * has been toggled. + * @param {object} reportEntry - entry in the report whose environment + * has been toggled. * @param {string} action - What action to take on the environment, expected * to be one of "start" or "stop". */ diff --git a/testplan/web_ui/testing/src/Report/__tests__/BatchReport.test.js b/testplan/web_ui/testing/src/Report/__tests__/BatchReport.test.js index 2dd6fda21..e0f3e57c5 100644 --- a/testplan/web_ui/testing/src/Report/__tests__/BatchReport.test.js +++ b/testplan/web_ui/testing/src/Report/__tests__/BatchReport.test.js @@ -2,10 +2,10 @@ import React from 'react'; import { shallow } from 'enzyme'; import { StyleSheetTestUtils } from "aphrodite"; import moxios from 'moxios'; - import BatchReport from '../BatchReport'; import Message from '../../Common/Message'; -import { TESTPLAN_REPORT, SIMPLE_REPORT } from "../../Common/sampleReports"; +import { TESTPLAN_REPORT_1 } from '../../__tests__/documents'; +import { SIMPLE_REPORT } from '../../__tests__/documents'; describe('BatchReport', () => { const renderBatchReport = (uid = "123") => { @@ -35,7 +35,7 @@ describe('BatchReport', () => { it('shallow renders the correct HTML structure when report loaded', () => { const batchReport = renderBatchReport(); - batchReport.setState({ report: TESTPLAN_REPORT }); + batchReport.setState({ report: TESTPLAN_REPORT_1 }); batchReport.update(); expect(batchReport).toMatchSnapshot(); }); @@ -88,7 +88,7 @@ describe('BatchReport', () => { expect(request.url).toBe("/api/v1/reports/123"); request.respondWith({ status: 200, - response: TESTPLAN_REPORT, + response: TESTPLAN_REPORT_1, }).then(() => { batchReport.update(); const selection = batchReport.state("selectedUIDs"); @@ -112,8 +112,8 @@ describe('BatchReport', () => { const expectedMessage = 'Error: Request failed with status code 404'; expect(message.props().message).toEqual(expectedMessage); done(); - }) - }) + }); + }); }); }); diff --git a/testplan/web_ui/testing/src/Report/__tests__/InteractiveReport.test.js b/testplan/web_ui/testing/src/Report/__tests__/InteractiveReport.test.js index d5a35b521..c277828ac 100644 --- a/testplan/web_ui/testing/src/Report/__tests__/InteractiveReport.test.js +++ b/testplan/web_ui/testing/src/Report/__tests__/InteractiveReport.test.js @@ -4,8 +4,7 @@ import { shallow } from 'enzyme'; import { StyleSheetTestUtils } from "aphrodite"; import moxios from 'moxios'; -import InteractiveReport from '../InteractiveReport.js'; -import { FakeInteractiveReport } from '../../Common/sampleReports.js'; +import InteractiveReport from '../InteractiveReport'; const initialReport = () => ({ "category": "testplan", diff --git a/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js b/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js index c210ef841..14627a493 100644 --- a/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js +++ b/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js @@ -1,7 +1,5 @@ -import React from 'react'; - -import {TESTPLAN_REPORT} from "../../Common/sampleReports"; -import {PropagateIndices} from "../reportUtils"; +import { PropagateIndices } from "../reportUtils"; +import { TESTPLAN_REPORT_1 } from '../../__tests__/documents'; describe('Report/reportUtils', () => { @@ -15,7 +13,7 @@ describe('Report/reportUtils', () => { let testplanEntries = {}; beforeEach(() => { - report = PropagateIndices(TESTPLAN_REPORT); + report = PropagateIndices(TESTPLAN_REPORT_1); multitest = report.entries[0]; suiteA = multitest.entries[0]; suiteB = multitest.entries[1]; diff --git a/testplan/web_ui/testing/src/Report/reportUtils.js b/testplan/web_ui/testing/src/Report/reportUtils.js index a19536a0a..d8179c168 100644 --- a/testplan/web_ui/testing/src/Report/reportUtils.js +++ b/testplan/web_ui/testing/src/Report/reportUtils.js @@ -1,8 +1,7 @@ /** * Report utility functions. */ -import React from "react"; - +import React from 'react'; import AssertionPane from '../AssertionPane/AssertionPane'; import Message from '../Common/Message'; @@ -16,7 +15,7 @@ import Message from '../Common/Message'; */ function _mergeTags(tagsA, tagsB) { // Don't edit one of the objects in place, copy to new object. - let mergedTags = {}; + const mergedTags = {}; for (const tagName in tagsA) { if (tagsA.hasOwnProperty(tagName)) { mergedTags[tagName] = tagsA[tagName]; @@ -28,8 +27,8 @@ function _mergeTags(tagsA, tagsB) { if (tagsB.hasOwnProperty(tagName)) { const tags = tagsB[tagName]; if (tagsA.hasOwnProperty(tagName)) { - let tagsArray = tags.concat(tagsA[tagName]); - let tagsSet = new Set(tagsArray); + const tagsArray = tags.concat(tagsA[tagName]); + const tagsSet = new Set(tagsArray); mergedTags[tagName] = [...tagsSet]; } else { mergedTags[tagName] = tags; @@ -61,7 +60,7 @@ const propagateIndicesRecur = (entries, parentIndices) => { name_type_index: new Set(), }; } - let indices = { + const indices = { tags_index: {}, name_type_index: new Set(), counter: { @@ -70,8 +69,8 @@ const propagateIndicesRecur = (entries, parentIndices) => { }, }; - for (let entry of entries) { - let entryType = entry.category; + for (const entry of entries) { + const entryType = entry.category; // Initialize indices. let tagsIndex = {}; const entryNameType = entry.name + '|' + entryType; @@ -88,7 +87,7 @@ const propagateIndicesRecur = (entries, parentIndices) => { if (entryType !== 'testcase') { // Propagate indices to children. - let descendantsIndices = propagateIndicesRecur( + const descendantsIndices = propagateIndicesRecur( entry.entries, { tags_index: tags, name_type_index: nameTypeIndex } ); @@ -122,8 +121,8 @@ const propagateIndicesRecur = (entries, parentIndices) => { * * name_type_index - its, its ancestors & its descendents names & types. * * counter - number of passing & failing descendent testcases. * - * @param {Array} entries - A single Testplan report in an Array. - * @returns {Array} - The Testplan report with indices, in an Array. + * @param {object} report - A single Testplan report. + * @returns {object[]} - The Testplan report with indices, in an Array. */ const PropagateIndices = (report) => { propagateIndicesRecur([report], undefined); @@ -134,7 +133,8 @@ const PropagateIndices = (report) => { * Return the updated state after a new entry is selected from the Nav * component. * - * @param {Object} entry - Nav entry metadata. + * @param {object} state + * @param {object} entry - Nav entry metadata. * @param {number} depth - depth of Nav entry in Testplan report. * @public */ @@ -231,7 +231,7 @@ const GetCenterPane = ( // eslint-disable -next-line const formatDate = (date, fmt) => { - var o = { + const o = { "M+" : date.getUTCMonth() + 1, "d+" : date.getUTCDate(), "h+" : date.getUTCHours(), @@ -247,7 +247,7 @@ const formatDate = (date, fmt) => { ); } - for (var k in o) { + for (const k in o) { if (new RegExp("(" + k + ")").test(fmt)) { fmt = fmt.replace( RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ( @@ -276,7 +276,7 @@ const getAssertions = (selectedEntries, displayTime) => { const selectedEntry = selectedEntries[selectedEntries.length - 1]; if (selectedEntry && selectedEntry.category === 'testcase') { - let links = []; + const links = []; getAssertionsRecursively(links, selectedEntry.entries); // get time information of each assertion if needed diff --git a/testplan/web_ui/testing/src/Toolbar/Buttons.js b/testplan/web_ui/testing/src/Toolbar/Buttons.js index 912ced750..df104c2b0 100644 --- a/testplan/web_ui/testing/src/Toolbar/Buttons.js +++ b/testplan/web_ui/testing/src/Toolbar/Buttons.js @@ -7,7 +7,7 @@ import {css} from 'aphrodite'; import {NavItem} from 'reactstrap'; import {library} from '@fortawesome/fontawesome-svg-core'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import {faClock} from '@fortawesome/free-solid-svg-icons'; import styles from './navStyles'; diff --git a/testplan/web_ui/testing/src/Toolbar/InteractiveButtons.js b/testplan/web_ui/testing/src/Toolbar/InteractiveButtons.js index 8b3718013..3df5d43d4 100644 --- a/testplan/web_ui/testing/src/Toolbar/InteractiveButtons.js +++ b/testplan/web_ui/testing/src/Toolbar/InteractiveButtons.js @@ -3,7 +3,7 @@ */ import React from 'react'; import {NavItem} from 'reactstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import {faBackspace} from '@fortawesome/free-solid-svg-icons'; import {css} from 'aphrodite'; diff --git a/testplan/web_ui/testing/src/Toolbar/Toolbar.js b/testplan/web_ui/testing/src/Toolbar/Toolbar.js index 0b96eb71e..cc3fe28fa 100644 --- a/testplan/web_ui/testing/src/Toolbar/Toolbar.js +++ b/testplan/web_ui/testing/src/Toolbar/Toolbar.js @@ -24,7 +24,7 @@ import FilterBox from "../Toolbar/FilterBox"; import {STATUS, STATUS_CATEGORY} from "../Common/defaults"; import {library} from '@fortawesome/fontawesome-svg-core'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome/index.es'; import { faInfo, diff --git a/testplan/web_ui/testing/src/Toolbar/navStyles.js b/testplan/web_ui/testing/src/Toolbar/navStyles.js index ce0bc64b9..d839e659c 100644 --- a/testplan/web_ui/testing/src/Toolbar/navStyles.js +++ b/testplan/web_ui/testing/src/Toolbar/navStyles.js @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ }, filterLabel: { width: '100%', - display: 'inlinde-block', + display: 'inline-block', cursor: 'pointer', padding: '0.2em', 'margin-left': '2em', diff --git a/testplan/web_ui/testing/src/__tests__/documents/.gitignore b/testplan/web_ui/testing/src/__tests__/documents/.gitignore new file mode 100644 index 000000000..f9be8dfe0 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/.gitignore @@ -0,0 +1 @@ +!* diff --git a/testplan/web_ui/testing/src/__tests__/documents/FakeInteractiveReport.js b/testplan/web_ui/testing/src/__tests__/documents/FakeInteractiveReport.js new file mode 100644 index 000000000..0386556cb --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/FakeInteractiveReport.js @@ -0,0 +1,72 @@ +/** + * Fake interactive report. All entries start with a status of "ready". + */ +module.exports = { + counter: { passed: 0, failed: 0 }, + entries: [ + { + counter: { passed: 0, failed: 0 }, + category: "multitest", + description: null, + entries: [ + { + counter: { passed: 0, failed: 0 }, + category: "testsuite", + description: null, + entries: [ + { + counter: { passed: 0, failed: 0 }, + description: null, + entries: [], + logs: [], + name: "test_interactive", + name_type_index: new Set(), + status: 'unknown', + runtime_status: 'ready', + status_override: null, + suite_related: false, + tags: {}, + tags_index: {}, + timer: {}, + type: "TestCaseReport", + uid: "ddd", + }, + ], + logs: [], + name: "Interactive Suite", + name_type_index: new Set(), + part: null, + status: 'unknown', + runtime_status: 'ready', + status_override: null, + tags: {}, + tags_index: {}, + timer: {}, + type: "TestGroupReport", + uid: "ccc", + }, + ], + logs: [], + name: "Interactive MTest", + name_type_index: new Set(), + part: null, + status: 'unknown', + runtime_status: 'ready', + status_override: null, + tags: {}, + tags_index: {}, + timer: {}, + type: "TestGroupReport", + uid: "bbb", + }, + ], + meta: {}, + name: "Fake Interactive Report", + name_type_index: new Set(), + status: 'unknown', + runtime_status: 'ready', + status_override: null, + tags_index: {}, + timer: null, + uid: "aaa", +}; diff --git a/testplan/web_ui/testing/src/__tests__/documents/README.md b/testplan/web_ui/testing/src/__tests__/documents/README.md new file mode 100644 index 000000000..a29a807c0 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/README.md @@ -0,0 +1,4 @@ +# Documents + +[SIMPLE_REPORT.json](SIMPLE_REPORT.json) - Simple report that only contains one MultiTest and one suite +[FakeInteractiveReport.json](FakeInteractiveReport.json) - Fake interactive report. All entries start with a status of "ready". diff --git a/testplan/web_ui/testing/src/__tests__/documents/SIMPLE_REPORT.json b/testplan/web_ui/testing/src/__tests__/documents/SIMPLE_REPORT.json new file mode 100644 index 000000000..374ef5737 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/SIMPLE_REPORT.json @@ -0,0 +1,99 @@ +{ + "category": "testplan", + "name": "Sample Testplan", + "status": "failed", + "uid": "520a92e4-325e-4077-93e6-55d7091a3f83", + "tags_index": {}, + "status_override": null, + "meta": {}, + "timer": { + "run": { + "start": "2018-10-15T14:30:10.998071+00:00", + "end": "2018-10-15T14:30:11.296158+00:00" + } + }, + "entries": [ + { + "name": "Primary", + "status": "failed", + "category": "multitest", + "description": null, + "status_override": null, + "uid": "21739167-b30f-4c13-a315-ef6ae52fd1f7", + "type": "TestGroupReport", + "logs": [], + "tags": { + "simple": [ + "server" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.009705+00:00", + "end": "2018-10-15T14:30:11.159661+00:00" + } + }, + "entries": [ + { + "status": "failed", + "category": "testsuite", + "name": "AlphaSuite", + "status_override": null, + "description": null, + "uid": "cb144b10-bdb0-44d3-9170-d8016dd19ee7", + "type": "TestGroupReport", + "logs": [], + "tags": { + "simple": [ + "server" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.009872+00:00", + "end": "2018-10-15T14:30:11.158224+00:00" + } + }, + "entries": [ + { + "category": "testcase", + "name": "test_equality_passing", + "status": "passed", + "status_override": null, + "description": null, + "uid": "736706ef-ba65-475d-96c5-f2855f431028", + "type": "TestCaseReport", + "logs": [], + "tags": { + "colour": [ + "white" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.010072+00:00", + "end": "2018-10-15T14:30:11.132214+00:00" + } + }, + "entries": [ + { + "category": "DEFAULT", + "machine_time": "2018-10-15T15:30:11.010098+00:00", + "description": "passing equality", + "line_no": 24, + "label": "==", + "second": 1, + "meta_type": "assertion", + "passed": true, + "type": "Equal", + "utc_time": "2018-10-15T14:30:11.010094+00:00", + "first": 1 + } + ] + } + ] + } + ] + } + ] +} diff --git a/testplan/web_ui/testing/src/Common/sampleReports.js b/testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_1.json similarity index 61% rename from testplan/web_ui/testing/src/Common/sampleReports.js rename to testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_1.json index 8adbfeb72..74bbf5f7e 100644 --- a/testplan/web_ui/testing/src/Common/sampleReports.js +++ b/testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_1.json @@ -1,7 +1,4 @@ -/** - * Sample Testplan reports to be used in development & testing. - */ -const TESTPLAN_REPORT = { +{ "category": "testplan", "name": "Sample Testplan", "status": "failed", @@ -26,7 +23,9 @@ const TESTPLAN_REPORT = { "type": "TestGroupReport", "logs": [], "tags": { - "simple": ["server"] + "simple": [ + "server" + ] }, "timer": { "run": { @@ -45,7 +44,9 @@ const TESTPLAN_REPORT = { "type": "TestGroupReport", "logs": [], "tags": { - "simple": ["server"] + "simple": [ + "server" + ] }, "timer": { "run": { @@ -55,7 +56,7 @@ const TESTPLAN_REPORT = { }, "entries": [ { - "category": 'testcase', + "category": "testcase", "name": "test_equality_passing", "status": "passed", "status_override": null, @@ -64,7 +65,9 @@ const TESTPLAN_REPORT = { "type": "TestCaseReport", "logs": [], "tags": { - "colour": ["white"] + "colour": [ + "white" + ] }, "timer": { "run": { @@ -86,10 +89,10 @@ const TESTPLAN_REPORT = { "utc_time": "2018-10-15T14:30:11.010094+00:00", "first": 1 } - ], + ] }, { - "category": 'testcase', + "category": "testcase", "name": "test_equality_passing2", "status": "failed", "tags": {}, @@ -118,9 +121,9 @@ const TESTPLAN_REPORT = { "utc_time": "2018-10-15T14:30:11.510094+00:00", "first": 1 } - ], - }, - ], + ] + } + ] }, { "status": "passed", @@ -132,7 +135,9 @@ const TESTPLAN_REPORT = { "type": "TestGroupReport", "logs": [], "tags": { - "simple": ["client"] + "simple": [ + "client" + ] }, "timer": { "run": { @@ -142,7 +147,7 @@ const TESTPLAN_REPORT = { }, "entries": [ { - "category": 'testcase', + "category": "testcase", "name": "test_equality_passing", "status": "passed", "tags": {}, @@ -171,11 +176,11 @@ const TESTPLAN_REPORT = { "utc_time": "2018-10-15T14:30:11.010094+00:00", "first": 1 } - ], - }, - ], - }, - ], + ] + } + ] + } + ] }, { "name": "Secondary", @@ -212,7 +217,7 @@ const TESTPLAN_REPORT = { }, "entries": [ { - "category": 'testcase', + "category": "testcase", "name": "test_equality_passing", "status": "passed", "tags": {}, @@ -241,171 +246,11 @@ const TESTPLAN_REPORT = { "utc_time": "2018-10-15T14:30:12.010094+00:00", "first": 1 } - ], - }, - ], + ] + } + ] } - ], - }, - ], -}; - -// Simple report that only contains one MultiTest and one suite. -const SIMPLE_REPORT = { - "category": "testplan", - "name": "Sample Testplan", - "status": "failed", - "uid": "520a92e4-325e-4077-93e6-55d7091a3f83", - "tags_index": {}, - "status_override": null, - "meta": {}, - "timer": { - "run": { - "start": "2018-10-15T14:30:10.998071+00:00", - "end": "2018-10-15T14:30:11.296158+00:00" + ] } - }, - "entries": [{ - "name": "Primary", - "status": "failed", - "category": "multitest", - "description": null, - "status_override": null, - "uid": "21739167-b30f-4c13-a315-ef6ae52fd1f7", - "type": "TestGroupReport", - "logs": [], - "tags": { - "simple": ["server"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.009705+00:00", - "end": "2018-10-15T14:30:11.159661+00:00" - } - }, - "entries": [{ - "status": "failed", - "category": "testsuite", - "name": "AlphaSuite", - "status_override": null, - "description": null, - "uid": "cb144b10-bdb0-44d3-9170-d8016dd19ee7", - "type": "TestGroupReport", - "logs": [], - "tags": { - "simple": ["server"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.009872+00:00", - "end": "2018-10-15T14:30:11.158224+00:00" - } - }, - "entries": [{ - "category": 'testcase', - "name": "test_equality_passing", - "status": "passed", - "status_override": null, - "description": null, - "uid": "736706ef-ba65-475d-96c5-f2855f431028", - "type": "TestCaseReport", - "logs": [], - "tags": { - "colour": ["white"] - }, - "timer": { - "run": { - "start": "2018-10-15T14:30:11.010072+00:00", - "end": "2018-10-15T14:30:11.132214+00:00" - } - }, - "entries": [{ - "category": "DEFAULT", - "machine_time": "2018-10-15T15:30:11.010098+00:00", - "description": "passing equality", - "line_no": 24, - "label": "==", - "second": 1, - "meta_type": "assertion", - "passed": true, - "type": "Equal", - "utc_time": "2018-10-15T14:30:11.010094+00:00", - "first": 1 - }], - }], - }], - }], -}; - -/** - * Fake interactive report. All entries start with a status of "ready". - */ -const FakeInteractiveReport = { - counter: {passed: 0, failed: 0}, - entries: [{ - counter: {passed: 0, failed: 0}, - category: "multitest", - description: null, - entries: [{ - counter: {passed: 0, failed: 0}, - category: "testsuite", - description: null, - entries: [{ - counter: {passed: 0, failed: 0}, - description: null, - entries: [], - logs: [], - name: "test_interactive", - name_type_index: new Set(), - status: 'unknown', - runtime_status: 'ready', - status_override: null, - suite_related: false, - tags: {}, - tags_index: {}, - timer: {}, - type: "TestCaseReport", - uid: "ddd", - }], - logs: [], - name: "Interactive Suite", - name_type_index: new Set(), - part: null, - status: 'unknown', - runtime_status: 'ready', - status_override: null, - tags: {}, - tags_index: {}, - timer: {}, - type: "TestGroupReport", - uid: "ccc", - }], - logs: [], - name: "Interactive MTest", - name_type_index: new Set(), - part: null, - status: 'unknown', - runtime_status: 'ready', - status_override: null, - tags: {}, - tags_index: {}, - timer: {}, - type: "TestGroupReport", - uid: "bbb", - }], - meta: {}, - name: "Fake Interactive Report", - name_type_index: new Set(), - status: 'unknown', - runtime_status: 'ready', - status_override: null, - tags_index: {}, - timer: null, - uid: "aaa", -}; - -export { - TESTPLAN_REPORT, - SIMPLE_REPORT, - FakeInteractiveReport, + ] } diff --git a/testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_2.json b/testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_2.json new file mode 100644 index 000000000..c83d99feb --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/TESTPLAN_REPORT_2.json @@ -0,0 +1,265 @@ +{ + "name": "Sample Testplan", + "status": "failed", + "uid": "520a92e4-325e-4077-93e6-55d7091a3f83", + "tags_index": {}, + "information": [ + [ + "user", + "unknown" + ], + [ + "command_line_string", + "/home/unknown/path_to_testplan_script/testplan.py" + ] + ], + "status_override": null, + "meta": {}, + "timer": { + "run": { + "start": "2018-10-15T14:30:10.998071+00:00", + "end": "2018-10-15T14:30:11.296158+00:00" + } + }, + "entries": [ + { + "name": "Primary", + "status": "failed", + "category": "multitest", + "description": null, + "status_override": null, + "uid": "21739167-b30f-4c13-a315-ef6ae52fd1f7", + "type": "TestGroupReport", + "logs": [], + "tags": { + "simple": [ + "server" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.009705+00:00", + "end": "2018-10-15T14:30:11.159661+00:00" + } + }, + "entries": [ + { + "status": "failed", + "category": "testsuite", + "name": "AlphaSuite", + "status_override": null, + "description": "This is a failed testsuite", + "uid": "cb144b10-bdb0-44d3-9170-d8016dd19ee7", + "type": "TestGroupReport", + "logs": [], + "tags": { + "simple": [ + "server" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.009872+00:00", + "end": "2018-10-15T14:30:11.158224+00:00" + } + }, + "entries": [ + { + "name": "test_equality_passing", + "category": "testcase", + "status": "passed", + "status_override": null, + "description": "A testcase example", + "uid": "736706ef-ba65-475d-96c5-f2855f431028", + "type": "TestCaseReport", + "logs": [], + "tags": { + "colour": [ + "white" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.010072+00:00", + "end": "2018-10-15T14:30:11.132214+00:00" + } + }, + "entries": [ + { + "category": "DEFAULT", + "machine_time": "2018-10-15T15:30:11.010098+00:00", + "description": "passing equality", + "line_no": 24, + "label": "==", + "second": 1, + "meta_type": "assertion", + "passed": true, + "type": "Equal", + "utc_time": "2018-10-15T14:30:11.010094+00:00", + "first": 1 + } + ] + }, + { + "name": "test_equality_passing2", + "category": "testcase", + "status": "failed", + "tags": {}, + "status_override": null, + "description": null, + "uid": "78686a4d-7b94-4ae6-ab50-d9960a7fb714", + "type": "TestCaseReport", + "logs": [], + "timer": { + "run": { + "start": "2018-10-15T14:30:11.510072+00:00", + "end": "2018-10-15T14:30:11.632214+00:00" + } + }, + "entries": [ + { + "category": "DEFAULT", + "machine_time": "2018-10-15T15:30:11.510098+00:00", + "description": "passing equality", + "line_no": 24, + "label": "==", + "second": 1, + "meta_type": "assertion", + "passed": true, + "type": "Equal", + "utc_time": "2018-10-15T14:30:11.510094+00:00", + "first": 1 + } + ] + } + ] + }, + { + "status": "passed", + "category": "testsuite", + "name": "BetaSuite", + "status_override": null, + "description": null, + "uid": "6fc5c008-4d1a-454e-80b6-74bdc9bca49e", + "type": "TestGroupReport", + "logs": [], + "tags": { + "simple": [ + "client" + ] + }, + "timer": { + "run": { + "start": "2018-10-15T14:30:11.009872+00:00", + "end": "2018-10-15T14:30:11.158224+00:00" + } + }, + "entries": [ + { + "name": "test_equality_passing", + "category": "testcase", + "status": "passed", + "tags": {}, + "status_override": null, + "description": null, + "uid": "8865a23d-1823-4c8d-ab37-58d24fc8ac05", + "type": "TestCaseReport", + "logs": [], + "timer": { + "run": { + "start": "2018-10-15T14:30:11.010072+00:00", + "end": "2018-10-15T14:30:11.132214+00:00" + } + }, + "entries": [ + { + "category": "DEFAULT", + "machine_time": "2018-10-15T15:30:11.010098+00:00", + "description": "passing equality", + "line_no": 24, + "label": "==", + "second": 1, + "meta_type": "assertion", + "passed": true, + "type": "Equal", + "utc_time": "2018-10-15T14:30:11.010094+00:00", + "first": 1 + } + ] + } + ] + } + ] + }, + { + "name": "Secondary", + "status": "passed", + "category": "multitest", + "tags": {}, + "description": null, + "status_override": null, + "uid": "8c3c7e6b-48e8-40cd-86db-8c8aed2592c8", + "type": "TestGroupReport", + "logs": [], + "timer": { + "run": { + "start": "2018-10-15T14:30:12.009705+00:00", + "end": "2018-10-15T14:30:12.159661+00:00" + } + }, + "entries": [ + { + "status": "passed", + "category": "testsuite", + "name": "GammaSuite", + "tags": {}, + "status_override": null, + "description": null, + "uid": "08d4c671-d55d-49d4-96ba-dc654d12be26", + "type": "TestGroupReport", + "logs": [], + "timer": { + "run": { + "start": "2018-10-15T14:30:12.009872+00:00", + "end": "2018-10-15T14:30:12.158224+00:00" + } + }, + "entries": [ + { + "name": "test_equality_passing", + "category": "testcase", + "status": "passed", + "tags": {}, + "status_override": null, + "description": null, + "uid": "f73bd6ea-d378-437b-a5db-00d9e427f36a", + "type": "TestCaseReport", + "logs": [], + "timer": { + "run": { + "start": "2018-10-15T14:30:12.010072+00:00", + "end": "2018-10-15T14:30:12.132214+00:00" + } + }, + "entries": [ + { + "category": "DEFAULT", + "machine_time": "2018-10-15T15:30:12.010098+00:00", + "description": "passing equality", + "line_no": 24, + "label": "==", + "second": 1, + "meta_type": "assertion", + "passed": true, + "type": "Equal", + "utc_time": "2018-10-15T14:30:12.010094+00:00", + "first": 1 + } + ] + } + ] + } + ] + } + ] +} diff --git a/testplan/web_ui/testing/src/__tests__/documents/fakeReportAssertions.json b/testplan/web_ui/testing/src/__tests__/documents/fakeReportAssertions.json new file mode 100644 index 000000000..f2d9957fb --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/fakeReportAssertions.json @@ -0,0 +1,3671 @@ +{ + "category": "testplan", + "tags_index": {}, + "meta": {}, + "information": [ + [ + "user", + "yifan" + ], + [ + "command_line_string", + "oss/examples/Assertions/Basic/test_plan.py --json example.json" + ], + [ + "python_version", + "3.7.1" + ] + ], + "counter": { + "passed": 2, + "failed": 6, + "total": 8 + }, + "uid": "c648a283-22f3-4503-ae6d-c982b4c7cca0", + "attachments": {}, + "status": "failed", + "timer": { + "run": { + "end": "2020-01-10T03:06:59.348924+00:00", + "start": "2020-01-10T03:06:58.537339+00:00" + } + }, + "runtime_status": "finished", + "name": "Assertions Example", + "status_override": null, + "entries": [ + { + "description": null, + "counter": { + "passed": 2, + "failed": 6, + "total": 8 + }, + "name": "Assertions Test", + "tags": {}, + "env_status": "STOPPED", + "type": "TestGroupReport", + "status_reason": null, + "runtime_status": "finished", + "fix_spec_path": null, + "part": null, + "uid": "99aef9f5-6957-4842-a6fa-e0cd9e358473", + "status": "failed", + "parent_uids": [ + "Assertions Example" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.141338+00:00", + "start": "2020-01-10T03:06:58.629871+00:00" + } + }, + "hash": 3697482064019099674, + "status_override": null, + "logs": [], + "category": "multitest", + "entries": [ + { + "description": null, + "counter": { + "passed": 2, + "failed": 6, + "total": 8 + }, + "name": "SampleSuite", + "tags": {}, + "env_status": null, + "type": "TestGroupReport", + "status_reason": null, + "runtime_status": "finished", + "fix_spec_path": null, + "part": null, + "uid": "9f98c732-d040-4a13-84e1-563adcd9dd32", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.135813+00:00", + "start": "2020-01-10T03:06:58.629972+00:00" + } + }, + "hash": -4958192469702756289, + "status_override": null, + "logs": [], + "category": "testsuite", + "entries": [ + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "25d0115f-91c4-481b-ad0f-37382d95fabd", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:58.939142+00:00", + "start": "2020-01-10T03:06:58.630091+00:00" + } + }, + "hash": 4069384282795794238, + "runtime_status": "finished", + "name": "test_basic_assertions", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "==", + "type": "Equal", + "utc_time": "2020-01-10T03:06:58.630121+00:00", + "second": "foo", + "passed": true, + "first": "foo", + "machine_time": "2020-01-10T11:06:58.630129+00:00", + "line_no": 25 + }, + { + "category": "DEFAULT", + "description": "Description for failing equality", + "meta_type": "assertion", + "label": "==", + "type": "Equal", + "utc_time": "2020-01-10T03:06:58.893461+00:00", + "second": 2, + "passed": false, + "first": 1, + "machine_time": "2020-01-10T11:06:58.893477+00:00", + "line_no": 28 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "!=", + "type": "NotEqual", + "utc_time": "2020-01-10T03:06:58.895795+00:00", + "second": "bar", + "passed": true, + "first": "foo", + "machine_time": "2020-01-10T11:06:58.895806+00:00", + "line_no": 30 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">", + "type": "Greater", + "utc_time": "2020-01-10T03:06:58.898075+00:00", + "second": 2, + "passed": true, + "first": 5, + "machine_time": "2020-01-10T11:06:58.898084+00:00", + "line_no": 31 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">=", + "type": "GreaterEqual", + "utc_time": "2020-01-10T03:06:58.899619+00:00", + "second": 2, + "passed": true, + "first": 2, + "machine_time": "2020-01-10T11:06:58.899627+00:00", + "line_no": 32 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">=", + "type": "GreaterEqual", + "utc_time": "2020-01-10T03:06:58.901156+00:00", + "second": 1, + "passed": true, + "first": 2, + "machine_time": "2020-01-10T11:06:58.901163+00:00", + "line_no": 33 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "<", + "type": "Less", + "utc_time": "2020-01-10T03:06:58.902604+00:00", + "second": 20, + "passed": true, + "first": 10, + "machine_time": "2020-01-10T11:06:58.902613+00:00", + "line_no": 34 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "<=", + "type": "LessEqual", + "utc_time": "2020-01-10T03:06:58.904109+00:00", + "second": 10, + "passed": true, + "first": 10, + "machine_time": "2020-01-10T11:06:58.904117+00:00", + "line_no": 35 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "<=", + "type": "LessEqual", + "utc_time": "2020-01-10T03:06:58.905543+00:00", + "second": 30, + "passed": true, + "first": 10, + "machine_time": "2020-01-10T11:06:58.905550+00:00", + "line_no": 36 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "==", + "type": "Equal", + "utc_time": "2020-01-10T03:06:58.906994+00:00", + "second": 15, + "passed": true, + "first": 15, + "machine_time": "2020-01-10T11:06:58.907002+00:00", + "line_no": 41 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "!=", + "type": "NotEqual", + "utc_time": "2020-01-10T03:06:58.908433+00:00", + "second": 20, + "passed": true, + "first": 10, + "machine_time": "2020-01-10T11:06:58.908440+00:00", + "line_no": 42 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "<", + "type": "Less", + "utc_time": "2020-01-10T03:06:58.909946+00:00", + "second": 3, + "passed": true, + "first": 2, + "machine_time": "2020-01-10T11:06:58.909954+00:00", + "line_no": 43 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">", + "type": "Greater", + "utc_time": "2020-01-10T03:06:58.911441+00:00", + "second": 2, + "passed": true, + "first": 3, + "machine_time": "2020-01-10T11:06:58.911449+00:00", + "line_no": 44 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": "<=", + "type": "LessEqual", + "utc_time": "2020-01-10T03:06:58.912920+00:00", + "second": 15, + "passed": true, + "first": 10, + "machine_time": "2020-01-10T11:06:58.912928+00:00", + "line_no": 45 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">=", + "type": "GreaterEqual", + "utc_time": "2020-01-10T03:06:58.914465+00:00", + "second": 10, + "passed": true, + "first": 15, + "machine_time": "2020-01-10T11:06:58.914473+00:00", + "line_no": 46 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "rel_tol": 0.1, + "label": "~=", + "type": "IsClose", + "utc_time": "2020-01-10T03:06:58.915976+00:00", + "second": 95, + "abs_tol": 0.0, + "passed": true, + "first": 100, + "machine_time": "2020-01-10T11:06:58.915984+00:00", + "line_no": 50 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "rel_tol": 0.01, + "label": "~=", + "type": "IsClose", + "utc_time": "2020-01-10T03:06:58.917481+00:00", + "second": 95, + "abs_tol": 0.0, + "passed": false, + "first": 100, + "machine_time": "2020-01-10T11:06:58.917489+00:00", + "line_no": 51 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "entry", + "type": "Log", + "utc_time": "2020-01-10T03:06:58.919181+00:00", + "machine_time": "2020-01-10T11:06:58.919189+00:00", + "line_no": 56, + "message": "This is a log message, it will be displayed along with other assertion details." + }, + { + "category": "DEFAULT", + "description": "Boolean Truthiness check", + "meta_type": "assertion", + "type": "IsTrue", + "utc_time": "2020-01-10T03:06:58.921013+00:00", + "expr": true, + "passed": true, + "machine_time": "2020-01-10T11:06:58.921021+00:00", + "line_no": 61 + }, + { + "category": "DEFAULT", + "description": "Boolean Falseness check", + "meta_type": "assertion", + "type": "IsFalse", + "utc_time": "2020-01-10T03:06:58.923056+00:00", + "expr": false, + "passed": true, + "machine_time": "2020-01-10T11:06:58.923064+00:00", + "line_no": 62 + }, + { + "category": "DEFAULT", + "description": "This is an explicit failure.", + "meta_type": "assertion", + "type": "Fail", + "utc_time": "2020-01-10T03:06:58.924595+00:00", + "passed": false, + "machine_time": "2020-01-10T11:06:58.924621+00:00", + "line_no": 64 + }, + { + "category": "DEFAULT", + "description": "Passing membership", + "meta_type": "assertion", + "type": "Contain", + "utc_time": "2020-01-10T03:06:58.926405+00:00", + "container": "foobar", + "passed": true, + "machine_time": "2020-01-10T11:06:58.926413+00:00", + "line_no": 67, + "member": "foo" + }, + { + "category": "DEFAULT", + "description": "Failing membership", + "meta_type": "assertion", + "type": "NotContain", + "utc_time": "2020-01-10T03:06:58.928507+00:00", + "container": "{'a': 1, 'b': 2}", + "passed": true, + "machine_time": "2020-01-10T11:06:58.928515+00:00", + "line_no": 71, + "member": 10 + }, + { + "category": "DEFAULT", + "description": "Comparison of slices", + "meta_type": "assertion", + "type": "EqualSlices", + "utc_time": "2020-01-10T03:06:58.930479+00:00", + "data": [ + [ + "slice(2, 4, None)", + [ + 2, + 3 + ], + [], + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + "slice(6, 8, None)", + [ + 6, + 7 + ], + [], + [ + 7, + 8 + ], + [ + 7, + 8 + ] + ] + ], + "passed": true, + "included_indices": [], + "machine_time": "2020-01-10T11:06:58.930488+00:00", + "expected": [ + "a", + "b", + 3, + 4, + "c", + "d", + 7, + 8 + ], + "line_no": 79, + "actual": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "category": "DEFAULT", + "description": "Comparison of slices (exclusion)", + "meta_type": "assertion", + "type": "EqualExcludeSlices", + "utc_time": "2020-01-10T03:06:58.932694+00:00", + "data": [ + [ + "slice(0, 2, None)", + [ + 2, + 3, + 4, + 5, + 6, + 7 + ], + [ + 4, + 5, + 6, + 7 + ], + [ + 3, + 4, + 5, + 6, + 7, + 8 + ], + [ + 3, + 4, + "c", + "d", + "e", + "f" + ] + ], + [ + "slice(4, 8, None)", + [ + 0, + 1, + 2, + 3 + ], + [ + 0, + 1 + ], + [ + 1, + 2, + 3, + 4 + ], + [ + "a", + "b", + 3, + 4 + ] + ] + ], + "passed": true, + "included_indices": [ + 2, + 3 + ], + "machine_time": "2020-01-10T11:06:58.932703+00:00", + "expected": [ + "a", + "b", + 3, + 4, + "c", + "d", + "e", + "f" + ], + "line_no": 91, + "actual": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "unified": false, + "category": "DEFAULT", + "ignore_blank_lines": true, + "description": null, + "meta_type": "assertion", + "type": "LineDiff", + "utc_time": "2020-01-10T03:06:58.934779+00:00", + "delta": [], + "second": [ + "abc\n", + "xyz\n", + "\n" + ], + "context": false, + "passed": true, + "first": [ + "abc\n", + "xyz\n" + ], + "machine_time": "2020-01-10T11:06:58.934786+00:00", + "ignore_space_change": false, + "line_no": 98, + "ignore_whitespaces": false + }, + { + "unified": 3, + "category": "DEFAULT", + "ignore_blank_lines": false, + "description": null, + "meta_type": "assertion", + "type": "LineDiff", + "utc_time": "2020-01-10T03:06:58.936975+00:00", + "delta": [], + "second": [ + "1\n", + "1\n", + "1\n", + "abc \n", + "xy\t\tz\n", + "2\n", + "2\n", + "2\n" + ], + "context": false, + "passed": true, + "first": [ + "1\r\n", + "1\r\n", + "1\r\n", + "abc\r\n", + "xy z\r\n", + "2\r\n", + "2\r\n", + "2\r\n" + ], + "machine_time": "2020-01-10T11:06:58.936983+00:00", + "ignore_space_change": true, + "line_no": 102, + "ignore_whitespaces": false + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 1, + "failed": 0, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "cd31b565-3702-4540-a140-ff9fd480e8ce", + "status": "passed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:58.963478+00:00", + "start": "2020-01-10T03:06:58.954190+00:00" + } + }, + "hash": -6066149844839810607, + "runtime_status": "finished", + "name": "test_raised_exceptions", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": null, + "type": "ExceptionRaised", + "utc_time": "2020-01-10T03:06:58.954270+00:00", + "func_match": true, + "raised_exception": [ + "", + "'bar'" + ], + "exception_match": true, + "expected_exceptions": [ + "KeyError" + ], + "passed": true, + "pattern_match": true, + "machine_time": "2020-01-10T11:06:58.954275+00:00", + "func": null, + "line_no": 112 + }, + { + "category": "DEFAULT", + "description": "Exception raised with custom pattern.", + "meta_type": "assertion", + "pattern": "foobar", + "type": "ExceptionRaised", + "utc_time": "2020-01-10T03:06:58.955863+00:00", + "func_match": true, + "raised_exception": [ + "", + "abc foobar xyz" + ], + "exception_match": true, + "expected_exceptions": [ + "ValueError" + ], + "passed": true, + "pattern_match": true, + "machine_time": "2020-01-10T11:06:58.955871+00:00", + "func": null, + "line_no": 121 + }, + { + "category": "DEFAULT", + "description": "Exception raised with custom func.", + "meta_type": "assertion", + "pattern": null, + "type": "ExceptionRaised", + "utc_time": "2020-01-10T03:06:58.957489+00:00", + "func_match": true, + "raised_exception": [ + ".MyException'>", + "4" + ], + "exception_match": true, + "expected_exceptions": [ + "MyException" + ], + "passed": true, + "pattern_match": true, + "machine_time": "2020-01-10T11:06:58.957497+00:00", + "func": ".custom_func at 0x7f9cfc64fea0>", + "line_no": 139 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": null, + "type": "ExceptionNotRaised", + "utc_time": "2020-01-10T03:06:58.958956+00:00", + "func_match": true, + "raised_exception": [ + "", + "'bar'" + ], + "exception_match": false, + "expected_exceptions": [ + "TypeError" + ], + "passed": true, + "pattern_match": true, + "machine_time": "2020-01-10T11:06:58.958964+00:00", + "func": null, + "line_no": 146 + }, + { + "category": "DEFAULT", + "description": "Exception not raised with custom pattern.", + "meta_type": "assertion", + "pattern": "foobar", + "type": "ExceptionNotRaised", + "utc_time": "2020-01-10T03:06:58.960503+00:00", + "func_match": true, + "raised_exception": [ + "", + "abc" + ], + "exception_match": true, + "expected_exceptions": [ + "ValueError" + ], + "passed": true, + "pattern_match": null, + "machine_time": "2020-01-10T11:06:58.960510+00:00", + "func": null, + "line_no": 157 + }, + { + "category": "DEFAULT", + "description": "Exception not raised with custom func.", + "meta_type": "assertion", + "pattern": null, + "type": "ExceptionNotRaised", + "utc_time": "2020-01-10T03:06:58.962023+00:00", + "func_match": false, + "raised_exception": [ + ".MyException'>", + "5" + ], + "exception_match": true, + "expected_exceptions": [ + "MyException" + ], + "passed": true, + "pattern_match": true, + "machine_time": "2020-01-10T11:06:58.962031+00:00", + "func": ".custom_func at 0x7f9cfc64fea0>", + "line_no": 165 + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "fca0596d-c220-4267-9a38-57968aca92d5", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:58.979777+00:00", + "start": "2020-01-10T03:06:58.971424+00:00" + } + }, + "hash": -2707574492059523373, + "runtime_status": "finished", + "name": "test_assertion_group", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": "Equality assertion outside the group", + "meta_type": "assertion", + "label": "==", + "type": "Equal", + "utc_time": "2020-01-10T03:06:58.971447+00:00", + "second": 1, + "passed": true, + "first": 1, + "machine_time": "2020-01-10T11:06:58.971451+00:00", + "line_no": 173 + }, + { + "description": "Custom group description", + "meta_type": "assertion", + "type": "Group", + "passed": false, + "entries": [ + { + "category": "DEFAULT", + "description": "Assertion within a group", + "meta_type": "assertion", + "label": "!=", + "type": "NotEqual", + "utc_time": "2020-01-10T03:06:58.973038+00:00", + "second": 3, + "passed": true, + "first": 2, + "machine_time": "2020-01-10T11:06:58.973047+00:00", + "line_no": 176 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "label": ">", + "type": "Greater", + "utc_time": "2020-01-10T03:06:58.974577+00:00", + "second": 3, + "passed": true, + "first": 5, + "machine_time": "2020-01-10T11:06:58.974586+00:00", + "line_no": 177 + }, + { + "description": "This is a sub group", + "meta_type": "assertion", + "type": "Group", + "passed": false, + "entries": [ + { + "category": "DEFAULT", + "description": "Assertion within sub group", + "meta_type": "assertion", + "label": "<", + "type": "Less", + "utc_time": "2020-01-10T03:06:58.976376+00:00", + "second": 3, + "passed": false, + "first": 6, + "machine_time": "2020-01-10T11:06:58.976384+00:00", + "line_no": 181 + } + ] + } + ] + }, + { + "category": "DEFAULT", + "description": "Final assertion outside all groups", + "meta_type": "assertion", + "label": "==", + "type": "Equal", + "utc_time": "2020-01-10T03:06:58.978219+00:00", + "second": "foo", + "passed": true, + "first": "foo", + "machine_time": "2020-01-10T11:06:58.978227+00:00", + "line_no": 184 + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "a3fd1023-b150-487a-bc7d-c0f64e326e63", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.006101+00:00", + "start": "2020-01-10T03:06:58.987035+00:00" + } + }, + "hash": -8719069130512673532, + "runtime_status": "finished", + "name": "test_regex_namespace", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": "string pattern match", + "meta_type": "assertion", + "pattern": "foo", + "type": "RegexMatch", + "utc_time": "2020-01-10T03:06:58.987140+00:00", + "match_indexes": [ + [ + 0, + 3 + ] + ], + "passed": true, + "string": "foobar", + "machine_time": "2020-01-10T11:06:58.987146+00:00", + "line_no": 196 + }, + { + "category": "DEFAULT", + "description": "SRE match", + "meta_type": "assertion", + "pattern": "foo", + "type": "RegexMatch", + "utc_time": "2020-01-10T03:06:58.988905+00:00", + "match_indexes": [ + [ + 0, + 3 + ] + ], + "passed": true, + "string": "foobar", + "machine_time": "2020-01-10T11:06:58.988913+00:00", + "line_no": 201 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "first line.*second", + "type": "RegexMatch", + "utc_time": "2020-01-10T03:06:58.991277+00:00", + "match_indexes": [ + [ + 0, + 17 + ] + ], + "passed": true, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:58.991285+00:00", + "line_no": 212 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "baz", + "type": "RegexMatchNotExists", + "utc_time": "2020-01-10T03:06:58.992937+00:00", + "match_indexes": [], + "passed": true, + "string": "foobar", + "machine_time": "2020-01-10T11:06:58.992945+00:00", + "line_no": 217 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "foobar", + "type": "RegexMatchNotExists", + "utc_time": "2020-01-10T03:06:58.994520+00:00", + "match_indexes": [], + "passed": true, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:58.994527+00:00", + "line_no": 222 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "second", + "type": "RegexSearch", + "utc_time": "2020-01-10T03:06:58.996148+00:00", + "match_indexes": [ + [ + 11, + 17 + ] + ], + "passed": true, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:58.996156+00:00", + "line_no": 225 + }, + { + "category": "DEFAULT", + "description": "Passing search empty", + "meta_type": "assertion", + "pattern": "foobar", + "type": "RegexSearchNotExists", + "utc_time": "2020-01-10T03:06:58.997760+00:00", + "match_indexes": [], + "passed": true, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:58.997768+00:00", + "line_no": 230 + }, + { + "category": "DEFAULT", + "description": "Failing search_empty", + "meta_type": "assertion", + "pattern": "second", + "type": "RegexSearchNotExists", + "utc_time": "2020-01-10T03:06:58.999296+00:00", + "match_indexes": [ + [ + 11, + 17 + ] + ], + "passed": false, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:58.999303+00:00", + "line_no": 233 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "foo", + "type": "RegexFindIter", + "utc_time": "2020-01-10T03:06:59.000852+00:00", + "match_indexes": [ + [ + 0, + 3 + ], + [ + 4, + 7 + ], + [ + 8, + 11 + ], + [ + 20, + 23 + ] + ], + "condition": "", + "passed": true, + "string": "foo foo foo bar bar foo bar", + "machine_time": "2020-01-10T11:06:59.000860+00:00", + "condition_match": true, + "line_no": 243 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "foo", + "type": "RegexFindIter", + "utc_time": "2020-01-10T03:06:59.002669+00:00", + "match_indexes": [ + [ + 0, + 3 + ], + [ + 4, + 7 + ], + [ + 8, + 11 + ], + [ + 20, + 23 + ] + ], + "condition": "(VAL > 2 and VAL < 5)", + "passed": true, + "string": "foo foo foo bar bar foo bar", + "machine_time": "2020-01-10T11:06:59.002676+00:00", + "condition_match": true, + "line_no": 250 + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "pattern": "\\w+ line$", + "type": "RegexMatchLine", + "utc_time": "2020-01-10T03:06:59.004622+00:00", + "match_indexes": [ + [ + 0, + 0, + 10 + ], + [ + 1, + 0, + 11 + ], + [ + 2, + 0, + 10 + ] + ], + "passed": true, + "string": "first line\nsecond line\nthird line", + "machine_time": "2020-01-10T11:06:59.004630+00:00", + "line_no": 257 + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "e8fb2848-cc83-4df3-83e0-82fe839d6526", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.072704+00:00", + "start": "2020-01-10T03:06:59.016322+00:00" + } + }, + "hash": -8829886055223884393, + "runtime_status": "finished", + "name": "test_table_namespace", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": "Table Match: list of list vs list of list", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.016418+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 0, + [ + "Bob", + 32 + ], + {}, + {}, + {} + ], + [ + 1, + [ + "Susan", + 24 + ], + {}, + {}, + {} + ], + [ + 2, + [ + "Rick", + 67 + ], + {}, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.016424+00:00", + "line_no": 284, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: list of dict vs list of dict", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.018525+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 0, + [ + "Bob", + 32 + ], + {}, + {}, + {} + ], + [ + 1, + [ + "Susan", + 24 + ], + {}, + {}, + {} + ], + [ + 2, + [ + "Rick", + 67 + ], + {}, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.018533+00:00", + "line_no": 289, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: list of dict vs list of list", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.020629+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 0, + [ + "Bob", + 32 + ], + {}, + {}, + {} + ], + [ + 1, + [ + "Susan", + 24 + ], + {}, + {}, + {} + ], + [ + 2, + [ + "Rick", + 67 + ], + {}, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.020640+00:00", + "line_no": 294, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: list of list vs list of list", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.023695+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [], + "report_fails_only": true, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.023703+00:00", + "line_no": 299, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: list of dict vs list of dict", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.026093+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [], + "report_fails_only": true, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.026102+00:00", + "line_no": 304, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: list of dict vs list of list", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.027835+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [], + "report_fails_only": true, + "passed": true, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.027843+00:00", + "line_no": 309, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: simple comparators", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.029541+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 0, + [ + "Bob", + 32 + ], + {}, + {}, + { + "name": "REGEX(\\w{3})", + "age": "" + } + ], + [ + 1, + [ + "Susan", + 24 + ], + {}, + {}, + {} + ], + [ + 2, + [ + "Rick", + 67 + ], + { + "name": "" + }, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.029549+00:00", + "line_no": 338, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: simple comparators", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.031666+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 2, + [ + "Rick", + 67 + ], + { + "name": "" + }, + {}, + {} + ] + ], + "report_fails_only": true, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.031674+00:00", + "line_no": 343, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: readable comparators", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.034598+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 0, + [ + "Bob", + 32 + ], + {}, + {}, + { + "name": "REGEX(\\w{3})", + "age": "(VAL > 30 and VAL < 40)" + } + ], + [ + 1, + [ + "Susan", + 24 + ], + {}, + {}, + {} + ], + [ + 2, + [ + "Rick", + 67 + ], + { + "name": "VAL in ['David', 'Helen', 'Pablo']" + }, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.034625+00:00", + "line_no": 361, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: readable comparators", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.037495+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "name", + "age" + ], + "data": [ + [ + 2, + [ + "Rick", + 67 + ], + { + "name": "VAL in ['David', 'Helen', 'Pablo']" + }, + {}, + {} + ] + ], + "report_fails_only": true, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.037502+00:00", + "line_no": 366, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: Trimmed columns", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.040045+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "column_1", + "column_2" + ], + "data": [ + [ + 0, + [ + 0, + 0 + ], + {}, + {}, + {} + ], + [ + 1, + [ + 1, + 2 + ], + {}, + {}, + {} + ], + [ + 2, + [ + 2, + 4 + ], + {}, + {}, + {} + ], + [ + 3, + [ + 3, + 6 + ], + {}, + {}, + {} + ], + [ + 4, + [ + 4, + 8 + ], + {}, + {}, + {} + ], + [ + 5, + [ + 5, + 10 + ], + {}, + {}, + {} + ], + [ + 6, + [ + 6, + 12 + ], + {}, + {}, + {} + ], + [ + 7, + [ + 7, + 14 + ], + {}, + {}, + {} + ], + [ + 8, + [ + 8, + 16 + ], + {}, + {}, + {} + ], + [ + 9, + [ + 9, + 18 + ], + {}, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": true, + "include_columns": [ + "column_1", + "column_2" + ], + "machine_time": "2020-01-10T11:06:59.040052+00:00", + "line_no": 383, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: Trimmed columns", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.042860+00:00", + "exclude_columns": null, + "fail_limit": 0, + "columns": [ + "column_1", + "column_2" + ], + "data": [], + "report_fails_only": true, + "passed": true, + "include_columns": [ + "column_1", + "column_2" + ], + "machine_time": "2020-01-10T11:06:59.042869+00:00", + "line_no": 391, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Match: Trimmed rows", + "meta_type": "assertion", + "type": "TableMatch", + "utc_time": "2020-01-10T03:06:59.046590+00:00", + "exclude_columns": null, + "fail_limit": 2, + "columns": [ + "amount", + "product_id" + ], + "data": [ + [ + 0, + [ + 0, + 4240 + ], + {}, + {}, + {} + ], + [ + 1, + [ + 10, + 3961 + ], + {}, + {}, + {} + ], + [ + 2, + [ + 20, + 1627 + ], + {}, + {}, + {} + ], + [ + 3, + [ + 30, + 1351 + ], + {}, + {}, + {} + ], + [ + 4, + [ + 40, + 2123 + ], + {}, + {}, + {} + ], + [ + 5, + [ + 25, + 1111 + ], + { + "amount": 35 + }, + {}, + {} + ], + [ + 6, + [ + 20, + 2222 + ], + { + "product_id": 1234 + }, + {}, + {} + ] + ], + "report_fails_only": false, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.046598+00:00", + "line_no": 428, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": "Table Diff: Trimmed rows", + "meta_type": "assertion", + "type": "TableDiff", + "utc_time": "2020-01-10T03:06:59.049590+00:00", + "exclude_columns": null, + "fail_limit": 2, + "columns": [ + "amount", + "product_id" + ], + "data": [ + [ + 5, + [ + 25, + 1111 + ], + { + "amount": 35 + }, + {}, + {} + ], + [ + 6, + [ + 20, + 2222 + ], + { + "product_id": 1234 + }, + {}, + {} + ] + ], + "report_fails_only": true, + "passed": false, + "include_columns": null, + "machine_time": "2020-01-10T11:06:59.049598+00:00", + "line_no": 437, + "message": null, + "strict": false + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "column": "symbol", + "limit": null, + "type": "ColumnContain", + "utc_time": "2020-01-10T03:06:59.051390+00:00", + "data": [ + [ + 0, + "AAPL", + true + ], + [ + 1, + "GOOG", + false + ], + [ + 2, + "FB", + false + ], + [ + 3, + "AMZN", + true + ], + [ + 4, + "MSFT", + false + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.051397+00:00", + "values": [ + "AAPL", + "AMZN" + ], + "line_no": 454, + "report_fails_only": false + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "column": "symbol", + "limit": 20, + "type": "ColumnContain", + "utc_time": "2020-01-10T03:06:59.057037+00:00", + "data": [ + [ + 1, + "GOOG", + false + ], + [ + 2, + "FB", + false + ], + [ + 4, + "MSFT", + false + ], + [ + 6, + "GOOG", + false + ], + [ + 7, + "FB", + false + ], + [ + 9, + "MSFT", + false + ], + [ + 11, + "GOOG", + false + ], + [ + 12, + "FB", + false + ], + [ + 14, + "MSFT", + false + ], + [ + 16, + "GOOG", + false + ], + [ + 17, + "FB", + false + ], + [ + 19, + "MSFT", + false + ], + [ + 21, + "GOOG", + false + ], + [ + 22, + "FB", + false + ], + [ + 24, + "MSFT", + false + ], + [ + 26, + "GOOG", + false + ], + [ + 27, + "FB", + false + ], + [ + 29, + "MSFT", + false + ], + [ + 31, + "GOOG", + false + ], + [ + 32, + "FB", + false + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.057048+00:00", + "values": [ + "AAPL", + "AMZN" + ], + "line_no": 467, + "report_fails_only": true + }, + { + "category": "DEFAULT", + "description": "Table Log: list of dicts", + "meta_type": "entry", + "type": "TableLog", + "utc_time": "2020-01-10T03:06:59.060012+00:00", + "table": [ + { + "name": "Bob", + "age": 32 + }, + { + "name": "Susan", + "age": 24 + }, + { + "name": "Rick", + "age": 67 + } + ], + "display_index": false, + "columns": [ + "name", + "age" + ], + "machine_time": "2020-01-10T11:06:59.060020+00:00", + "line_no": 472, + "indices": [ + 0, + 1, + 2 + ] + }, + { + "category": "DEFAULT", + "description": "Table Log: list of lists", + "meta_type": "entry", + "type": "TableLog", + "utc_time": "2020-01-10T03:06:59.061711+00:00", + "table": [ + { + "name": "Bob", + "age": 32 + }, + { + "name": "Susan", + "age": 24 + }, + { + "name": "Rick", + "age": 67 + } + ], + "display_index": false, + "columns": [ + "name", + "age" + ], + "machine_time": "2020-01-10T11:06:59.061718+00:00", + "line_no": 473, + "indices": [ + 0, + 1, + 2 + ] + }, + { + "category": "DEFAULT", + "description": "Table Log: many rows", + "meta_type": "entry", + "type": "TableLog", + "utc_time": "2020-01-10T03:06:59.063421+00:00", + "table": [ + { + "symbol": "AAPL", + "amount": 12 + }, + { + "symbol": "GOOG", + "amount": 21 + }, + { + "symbol": "FB", + "amount": 32 + }, + { + "symbol": "AMZN", + "amount": 5 + }, + { + "symbol": "MSFT", + "amount": 42 + }, + { + "symbol": "AAPL", + "amount": 12 + }, + { + "symbol": "GOOG", + "amount": 21 + }, + { + "symbol": "FB", + "amount": 32 + }, + { + "symbol": "AMZN", + "amount": 5 + }, + { + "symbol": "MSFT", + "amount": 42 + }, + { + "symbol": "AAPL", + "amount": 12 + }, + { + "symbol": "GOOG", + "amount": 21 + }, + { + "symbol": "FB", + "amount": 32 + }, + { + "symbol": "AMZN", + "amount": 5 + }, + { + "symbol": "MSFT", + "amount": 42 + }, + { + "symbol": "AAPL", + "amount": 12 + }, + { + "symbol": "GOOG", + "amount": 21 + }, + { + "symbol": "FB", + "amount": 32 + }, + { + "symbol": "AMZN", + "amount": 5 + }, + { + "symbol": "MSFT", + "amount": 42 + } + ], + "display_index": false, + "columns": [ + "symbol", + "amount" + ], + "machine_time": "2020-01-10T11:06:59.063429+00:00", + "line_no": 479, + "indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19 + ] + }, + { + "category": "DEFAULT", + "description": "Table Log: many columns", + "meta_type": "entry", + "type": "TableLog", + "utc_time": "2020-01-10T03:06:59.065884+00:00", + "table": [ + { + "col_0": "row 0 col 0", + "col_1": "row 0 col 1", + "col_2": "row 0 col 2", + "col_3": "row 0 col 3", + "col_4": "row 0 col 4", + "col_5": "row 0 col 5", + "col_6": "row 0 col 6", + "col_7": "row 0 col 7", + "col_8": "row 0 col 8", + "col_9": "row 0 col 9", + "col_10": "row 0 col 10", + "col_11": "row 0 col 11", + "col_12": "row 0 col 12", + "col_13": "row 0 col 13", + "col_14": "row 0 col 14", + "col_15": "row 0 col 15", + "col_16": "row 0 col 16", + "col_17": "row 0 col 17", + "col_18": "row 0 col 18", + "col_19": "row 0 col 19" + }, + { + "col_0": "row 1 col 0", + "col_1": "row 1 col 1", + "col_2": "row 1 col 2", + "col_3": "row 1 col 3", + "col_4": "row 1 col 4", + "col_5": "row 1 col 5", + "col_6": "row 1 col 6", + "col_7": "row 1 col 7", + "col_8": "row 1 col 8", + "col_9": "row 1 col 9", + "col_10": "row 1 col 10", + "col_11": "row 1 col 11", + "col_12": "row 1 col 12", + "col_13": "row 1 col 13", + "col_14": "row 1 col 14", + "col_15": "row 1 col 15", + "col_16": "row 1 col 16", + "col_17": "row 1 col 17", + "col_18": "row 1 col 18", + "col_19": "row 1 col 19" + }, + { + "col_0": "row 2 col 0", + "col_1": "row 2 col 1", + "col_2": "row 2 col 2", + "col_3": "row 2 col 3", + "col_4": "row 2 col 4", + "col_5": "row 2 col 5", + "col_6": "row 2 col 6", + "col_7": "row 2 col 7", + "col_8": "row 2 col 8", + "col_9": "row 2 col 9", + "col_10": "row 2 col 10", + "col_11": "row 2 col 11", + "col_12": "row 2 col 12", + "col_13": "row 2 col 13", + "col_14": "row 2 col 14", + "col_15": "row 2 col 15", + "col_16": "row 2 col 16", + "col_17": "row 2 col 17", + "col_18": "row 2 col 18", + "col_19": "row 2 col 19" + }, + { + "col_0": "row 3 col 0", + "col_1": "row 3 col 1", + "col_2": "row 3 col 2", + "col_3": "row 3 col 3", + "col_4": "row 3 col 4", + "col_5": "row 3 col 5", + "col_6": "row 3 col 6", + "col_7": "row 3 col 7", + "col_8": "row 3 col 8", + "col_9": "row 3 col 9", + "col_10": "row 3 col 10", + "col_11": "row 3 col 11", + "col_12": "row 3 col 12", + "col_13": "row 3 col 13", + "col_14": "row 3 col 14", + "col_15": "row 3 col 15", + "col_16": "row 3 col 16", + "col_17": "row 3 col 17", + "col_18": "row 3 col 18", + "col_19": "row 3 col 19" + }, + { + "col_0": "row 4 col 0", + "col_1": "row 4 col 1", + "col_2": "row 4 col 2", + "col_3": "row 4 col 3", + "col_4": "row 4 col 4", + "col_5": "row 4 col 5", + "col_6": "row 4 col 6", + "col_7": "row 4 col 7", + "col_8": "row 4 col 8", + "col_9": "row 4 col 9", + "col_10": "row 4 col 10", + "col_11": "row 4 col 11", + "col_12": "row 4 col 12", + "col_13": "row 4 col 13", + "col_14": "row 4 col 14", + "col_15": "row 4 col 15", + "col_16": "row 4 col 16", + "col_17": "row 4 col 17", + "col_18": "row 4 col 18", + "col_19": "row 4 col 19" + }, + { + "col_0": "row 5 col 0", + "col_1": "row 5 col 1", + "col_2": "row 5 col 2", + "col_3": "row 5 col 3", + "col_4": "row 5 col 4", + "col_5": "row 5 col 5", + "col_6": "row 5 col 6", + "col_7": "row 5 col 7", + "col_8": "row 5 col 8", + "col_9": "row 5 col 9", + "col_10": "row 5 col 10", + "col_11": "row 5 col 11", + "col_12": "row 5 col 12", + "col_13": "row 5 col 13", + "col_14": "row 5 col 14", + "col_15": "row 5 col 15", + "col_16": "row 5 col 16", + "col_17": "row 5 col 17", + "col_18": "row 5 col 18", + "col_19": "row 5 col 19" + }, + { + "col_0": "row 6 col 0", + "col_1": "row 6 col 1", + "col_2": "row 6 col 2", + "col_3": "row 6 col 3", + "col_4": "row 6 col 4", + "col_5": "row 6 col 5", + "col_6": "row 6 col 6", + "col_7": "row 6 col 7", + "col_8": "row 6 col 8", + "col_9": "row 6 col 9", + "col_10": "row 6 col 10", + "col_11": "row 6 col 11", + "col_12": "row 6 col 12", + "col_13": "row 6 col 13", + "col_14": "row 6 col 14", + "col_15": "row 6 col 15", + "col_16": "row 6 col 16", + "col_17": "row 6 col 17", + "col_18": "row 6 col 18", + "col_19": "row 6 col 19" + }, + { + "col_0": "row 7 col 0", + "col_1": "row 7 col 1", + "col_2": "row 7 col 2", + "col_3": "row 7 col 3", + "col_4": "row 7 col 4", + "col_5": "row 7 col 5", + "col_6": "row 7 col 6", + "col_7": "row 7 col 7", + "col_8": "row 7 col 8", + "col_9": "row 7 col 9", + "col_10": "row 7 col 10", + "col_11": "row 7 col 11", + "col_12": "row 7 col 12", + "col_13": "row 7 col 13", + "col_14": "row 7 col 14", + "col_15": "row 7 col 15", + "col_16": "row 7 col 16", + "col_17": "row 7 col 17", + "col_18": "row 7 col 18", + "col_19": "row 7 col 19" + }, + { + "col_0": "row 8 col 0", + "col_1": "row 8 col 1", + "col_2": "row 8 col 2", + "col_3": "row 8 col 3", + "col_4": "row 8 col 4", + "col_5": "row 8 col 5", + "col_6": "row 8 col 6", + "col_7": "row 8 col 7", + "col_8": "row 8 col 8", + "col_9": "row 8 col 9", + "col_10": "row 8 col 10", + "col_11": "row 8 col 11", + "col_12": "row 8 col 12", + "col_13": "row 8 col 13", + "col_14": "row 8 col 14", + "col_15": "row 8 col 15", + "col_16": "row 8 col 16", + "col_17": "row 8 col 17", + "col_18": "row 8 col 18", + "col_19": "row 8 col 19" + }, + { + "col_0": "row 9 col 0", + "col_1": "row 9 col 1", + "col_2": "row 9 col 2", + "col_3": "row 9 col 3", + "col_4": "row 9 col 4", + "col_5": "row 9 col 5", + "col_6": "row 9 col 6", + "col_7": "row 9 col 7", + "col_8": "row 9 col 8", + "col_9": "row 9 col 9", + "col_10": "row 9 col 10", + "col_11": "row 9 col 11", + "col_12": "row 9 col 12", + "col_13": "row 9 col 13", + "col_14": "row 9 col 14", + "col_15": "row 9 col 15", + "col_16": "row 9 col 16", + "col_17": "row 9 col 17", + "col_18": "row 9 col 18", + "col_19": "row 9 col 19" + } + ], + "display_index": false, + "columns": [ + "col_0", + "col_1", + "col_2", + "col_3", + "col_4", + "col_5", + "col_6", + "col_7", + "col_8", + "col_9", + "col_10", + "col_11", + "col_12", + "col_13", + "col_14", + "col_15", + "col_16", + "col_17", + "col_18", + "col_19" + ], + "machine_time": "2020-01-10T11:06:59.065891+00:00", + "line_no": 490, + "indices": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + }, + { + "category": "DEFAULT", + "description": "Table Log: long cells", + "meta_type": "entry", + "type": "TableLog", + "utc_time": "2020-01-10T03:06:59.070773+00:00", + "table": [ + { + "Name": "Bob Stevens", + "Age": "33", + "Address": "89 Trinsdale Avenue, LONDON, E8 0XW" + }, + { + "Name": "Susan Evans", + "Age": "21", + "Address": "100 Loop Road, SWANSEA, U8 12JK" + }, + { + "Name": "Trevor Dune", + "Age": "88", + "Address": "28 Kings Lane, MANCHESTER, MT16 2YT" + }, + { + "Name": "Belinda Baggins", + "Age": "38", + "Address": "31 Prospect Hill, DOYNTON, BS30 9DN" + }, + { + "Name": "Cosimo Hornblower", + "Age": "89", + "Address": "65 Prospect Hill, SURREY, PH33 4TY" + }, + { + "Name": "Sabine Wurfel", + "Age": "31", + "Address": "88 Clasper Way, HEXWORTHY, PL20 4BG" + } + ], + "display_index": false, + "columns": [ + "Name", + "Age", + "Address" + ], + "machine_time": "2020-01-10T11:06:59.070780+00:00", + "line_no": 504, + "indices": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "ca8979be-8eb3-4ff4-8c18-aba4c8348bac", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.102866+00:00", + "start": "2020-01-10T03:06:59.087638+00:00" + } + }, + "hash": -6007544999293600650, + "runtime_status": "finished", + "name": "test_dict_namespace", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": "Simple dict match", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.087672+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Passed", + [ + "int", + "1" + ], + [ + "int", + "1" + ] + ], + [ + 0, + "bar", + "Failed", + [ + "int", + "2" + ], + [ + "int", + "5" + ] + ], + [ + 0, + "extra-key", + "Failed", + [ + null, + "ABSENT" + ], + [ + "int", + "10" + ] + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.087677+00:00", + "exclude_keys": null, + "line_no": 524 + }, + { + "category": "DEFAULT", + "description": "Nested dict match", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.089583+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Failed", + "", + "" + ], + [ + 1, + "alpha", + "Failed", + "", + "" + ], + [ + 1, + "", + "Passed", + [ + "int", + "1" + ], + [ + "int", + "1" + ] + ], + [ + 1, + "", + "Passed", + [ + "int", + "2" + ], + [ + "int", + "2" + ] + ], + [ + 1, + "", + "Failed", + [ + "int", + "3" + ], + [ + null, + null + ] + ], + [ + 1, + "beta", + "Failed", + "", + "" + ], + [ + 2, + "color", + "Failed", + [ + "str", + "red" + ], + [ + "str", + "blue" + ] + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.089619+00:00", + "exclude_keys": null, + "line_no": 542 + }, + { + "category": "DEFAULT", + "description": "Dict match: Custom comparators", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.091710+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Passed", + "", + "" + ], + [ + 0, + "", + "Passed", + [ + "int", + "1" + ], + [ + "int", + "1" + ] + ], + [ + 0, + "", + "Passed", + [ + "int", + "2" + ], + [ + "int", + "2" + ] + ], + [ + 0, + "", + "Passed", + [ + "int", + "3" + ], + [ + "func", + "" + ] + ], + [ + 0, + "bar", + "Passed", + "", + "" + ], + [ + 1, + "color", + "Passed", + [ + "str", + "blue" + ], + [ + "func", + "VAL in ['blue', 'red', 'yellow']" + ] + ], + [ + 0, + "baz", + "Passed", + [ + "str", + "hello world" + ], + [ + "REGEX", + "\\w+ world" + ] + ] + ], + "passed": true, + "machine_time": "2020-01-10T11:06:59.091718+00:00", + "exclude_keys": null, + "line_no": 560 + }, + { + "category": "DEFAULT", + "description": "default assertion passes because the values are numerically equal", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.093424+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Passed", + [ + "int", + "1" + ], + [ + "float", + 1.0 + ] + ], + [ + 0, + "bar", + "Passed", + [ + "int", + "2" + ], + [ + "float", + 2.0 + ] + ], + [ + 0, + "baz", + "Passed", + [ + "int", + "3" + ], + [ + "float", + 3.0 + ] + ] + ], + "passed": true, + "machine_time": "2020-01-10T11:06:59.093432+00:00", + "exclude_keys": null, + "line_no": 572 + }, + { + "category": "DEFAULT", + "description": "when we check types the assertion will fail", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.094973+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Failed", + [ + "int", + "1" + ], + [ + "float", + 1.0 + ] + ], + [ + 0, + "bar", + "Failed", + [ + "int", + "2" + ], + [ + "float", + 2.0 + ] + ], + [ + 0, + "baz", + "Failed", + [ + "int", + "3" + ], + [ + "float", + 3.0 + ] + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.094981+00:00", + "exclude_keys": null, + "line_no": 578 + }, + { + "category": "DEFAULT", + "description": "use a custom comparison function to check within a tolerance", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.096547+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "foo", + "Passed", + [ + "float", + 1.02 + ], + [ + "float", + 0.98 + ] + ], + [ + 0, + "bar", + "Passed", + [ + "float", + 2.28 + ], + [ + "float", + 2.33 + ] + ], + [ + 0, + "baz", + "Passed", + [ + "float", + 3.5 + ], + [ + "float", + 3.46 + ] + ] + ], + "passed": true, + "machine_time": "2020-01-10T11:06:59.096554+00:00", + "exclude_keys": null, + "line_no": 587 + }, + { + "category": "DEFAULT", + "description": "only report the failing comparison", + "meta_type": "assertion", + "type": "DictMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.098102+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + "bad_key", + "Failed", + [ + "str", + "actual" + ], + [ + "str", + "expected" + ] + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.098109+00:00", + "exclude_keys": null, + "line_no": 601 + }, + { + "absent_keys_diff": [ + "bar" + ], + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "has_keys_diff": [ + "alpha" + ], + "type": "DictCheck", + "utc_time": "2020-01-10T03:06:59.099751+00:00", + "passed": false, + "absent_keys": [ + "bar", + "beta" + ], + "machine_time": "2020-01-10T11:06:59.099760+00:00", + "line_no": 611, + "has_keys": [ + "foo", + "alpha" + ] + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "entry", + "type": "DictLog", + "utc_time": "2020-01-10T03:06:59.101282+00:00", + "flattened_dict": [ + [ + 0, + "foo", + "" + ], + [ + 0, + "", + [ + "int", + "1" + ] + ], + [ + 0, + "", + [ + "int", + "2" + ] + ], + [ + 0, + "", + [ + "int", + "3" + ] + ], + [ + 0, + "bar", + "" + ], + [ + 1, + "color", + [ + "str", + "blue" + ] + ], + [ + 0, + "baz", + [ + "str", + "hello world" + ] + ] + ], + "machine_time": "2020-01-10T11:06:59.101290+00:00", + "line_no": 620 + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 0, + "failed": 1, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "826ee3d4-0dea-412b-9652-86f5847706d9", + "status": "failed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.116938+00:00", + "start": "2020-01-10T03:06:59.111312+00:00" + } + }, + "hash": 3253704292606433761, + "runtime_status": "finished", + "name": "test_fix_namespace", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "type": "FixMatch", + "include_keys": null, + "utc_time": "2020-01-10T03:06:59.111446+00:00", + "actual_description": null, + "expected_description": null, + "comparison": [ + [ + 0, + 36, + "Passed", + [ + "int", + "6" + ], + [ + "int", + "6" + ] + ], + [ + 0, + 22, + "Passed", + [ + "int", + "5" + ], + [ + "int", + "5" + ] + ], + [ + 0, + 55, + "Passed", + [ + "int", + "2" + ], + [ + "int", + "2" + ] + ], + [ + 0, + 38, + "Passed", + [ + "int", + "5" + ], + [ + "func", + "VAL >= 4" + ] + ], + [ + 0, + 555, + "Failed", + "", + "" + ], + [ + 0, + "", + "Failed", + "", + "" + ], + [ + 1, + 600, + "Passed", + [ + "str", + "A" + ], + [ + "str", + "A" + ] + ], + [ + 1, + 601, + "Failed", + [ + "str", + "A" + ], + [ + "str", + "B" + ] + ], + [ + 1, + 683, + "Passed", + "", + "" + ], + [ + 1, + "", + "Passed", + "", + "" + ], + [ + 2, + 688, + "Passed", + [ + "str", + "a" + ], + [ + "str", + "a" + ] + ], + [ + 2, + 689, + "Passed", + [ + "str", + "a" + ], + [ + "REGEX", + "[a-z]" + ] + ], + [ + 1, + "", + "Passed", + "", + "" + ], + [ + 2, + 688, + "Passed", + [ + "str", + "b" + ], + [ + "str", + "b" + ] + ], + [ + 2, + 689, + "Passed", + [ + "str", + "b" + ], + [ + "str", + "b" + ] + ], + [ + 0, + "", + "Failed", + "", + "" + ], + [ + 1, + 600, + "Failed", + [ + "str", + "B" + ], + [ + "str", + "C" + ] + ], + [ + 1, + 601, + "Passed", + [ + "str", + "B" + ], + [ + "str", + "B" + ] + ], + [ + 1, + 683, + "Passed", + "", + "" + ], + [ + 1, + "", + "Passed", + "", + "" + ], + [ + 2, + 688, + "Passed", + [ + "str", + "c" + ], + [ + "str", + "c" + ] + ], + [ + 2, + 689, + "Passed", + [ + "str", + "c" + ], + [ + "func", + "VAL in ('c', 'd')" + ] + ], + [ + 1, + "", + "Passed", + "", + "" + ], + [ + 2, + 688, + "Passed", + [ + "str", + "d" + ], + [ + "str", + "d" + ] + ], + [ + 2, + 689, + "Passed", + [ + "str", + "d" + ], + [ + "str", + "d" + ] + ] + ], + "passed": false, + "machine_time": "2020-01-10T11:06:59.111452+00:00", + "exclude_keys": null, + "line_no": 708 + }, + { + "absent_keys_diff": [ + 555 + ], + "category": "DEFAULT", + "description": null, + "meta_type": "assertion", + "has_keys_diff": [ + 26, + 11 + ], + "type": "FixCheck", + "utc_time": "2020-01-10T03:06:59.113689+00:00", + "passed": false, + "absent_keys": [ + 444, + 555 + ], + "machine_time": "2020-01-10T11:06:59.113697+00:00", + "line_no": 716, + "has_keys": [ + 26, + 22, + 11 + ] + }, + { + "category": "DEFAULT", + "description": null, + "meta_type": "entry", + "type": "FixLog", + "utc_time": "2020-01-10T03:06:59.115483+00:00", + "flattened_dict": [ + [ + 0, + 36, + [ + "int", + "6" + ] + ], + [ + 0, + 22, + [ + "int", + "5" + ] + ], + [ + 0, + 55, + [ + "int", + "2" + ] + ], + [ + 0, + 38, + [ + "int", + "5" + ] + ], + [ + 0, + 555, + "" + ], + [ + 0, + "", + "" + ], + [ + 1, + 556, + [ + "str", + "USD" + ] + ], + [ + 1, + 624, + [ + "int", + "1" + ] + ], + [ + 0, + "", + "" + ], + [ + 1, + 556, + [ + "str", + "EUR" + ] + ], + [ + 1, + 624, + [ + "int", + "2" + ] + ] + ], + "machine_time": "2020-01-10T11:06:59.115490+00:00", + "line_no": 729 + } + ] + }, + { + "category": "testcase", + "logs": [], + "description": null, + "suite_related": false, + "counter": { + "passed": 1, + "failed": 0, + "total": 1 + }, + "status_reason": null, + "type": "TestCaseReport", + "uid": "52a8a7d9-80e6-4f7f-8eef-065bb25d38f8", + "status": "passed", + "parent_uids": [ + "Assertions Example", + "Assertions Test", + "SampleSuite" + ], + "timer": { + "run": { + "end": "2020-01-10T03:06:59.129247+00:00", + "start": "2020-01-10T03:06:59.123570+00:00" + } + }, + "hash": -5041530229790182508, + "runtime_status": "finished", + "name": "test_xml_namespace", + "status_override": null, + "tags": {}, + "entries": [ + { + "category": "DEFAULT", + "description": "Simple XML check for existence of xpath.", + "meta_type": "assertion", + "type": "XMLCheck", + "utc_time": "2020-01-10T03:06:59.123813+00:00", + "namespaces": null, + "data": [], + "passed": true, + "xml": "\n Foo\n \n", + "machine_time": "2020-01-10T11:06:59.123821+00:00", + "tags": null, + "line_no": 751, + "message": "xpath: `/Root/Test` exists in the XML.", + "xpath": "/Root/Test" + }, + { + "category": "DEFAULT", + "description": "XML check for tags in the given xpath.", + "meta_type": "assertion", + "type": "XMLCheck", + "utc_time": "2020-01-10T03:06:59.125438+00:00", + "namespaces": null, + "data": [ + [ + "Value1", + null, + null, + null + ], + [ + "Value2", + null, + null, + null + ] + ], + "passed": true, + "xml": "\n Value1\n Value2\n \n", + "machine_time": "2020-01-10T11:06:59.125447+00:00", + "tags": [ + "Value1", + "Value2" + ], + "line_no": 765, + "message": null, + "xpath": "/Root/Test" + }, + { + "category": "DEFAULT", + "description": "XML check with namespace matching.", + "meta_type": "assertion", + "type": "XMLCheck", + "utc_time": "2020-01-10T03:06:59.127250+00:00", + "namespaces": { + "a": "http://testplan" + }, + "data": [ + [ + "Hello world!", + null, + null, + "REGEX(Hello*)" + ] + ], + "passed": true, + "xml": "\n \n \n Hello world!\n \n \n", + "machine_time": "2020-01-10T11:06:59.127259+00:00", + "tags": [ + "re.compile('Hello*')" + ], + "line_no": 784, + "message": null, + "xpath": "//*/a:message" + } + ] + } + ] + } + ] + } + ] +} diff --git a/testplan/web_ui/testing/src/__tests__/documents/index.js b/testplan/web_ui/testing/src/__tests__/documents/index.js new file mode 100644 index 000000000..28d20aa41 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/documents/index.js @@ -0,0 +1,11 @@ +if(process.env.NODE_ENV !== 'production') { + module.exports = { + TESTPLAN_REPORT_1: require('./TESTPLAN_REPORT_1.json'), + TESTPLAN_REPORT_2: require('./TESTPLAN_REPORT_2.json'), + SIMPLE_REPORT: require('./SIMPLE_REPORT.json'), + fakeReportAssertions: require('./fakeReportAssertions.json'), + FakeInteractiveReport: require('./FakeInteractiveReport'), + }; +} else { + module.exports = {}; +} diff --git a/testplan/web_ui/testing/src/__tests__/testUtils.js b/testplan/web_ui/testing/src/__tests__/testUtils.js new file mode 100644 index 000000000..0fd52b323 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/testUtils.js @@ -0,0 +1,501 @@ +/** + * Use this module to store utility functions / types that are *only* used in + * tests. This helps prevent unnecessary bloating of the production bundle. + * + * To use one of these functions in production, move it to + * {@link './../Common/utils.js'} and reexport it from here. + */ +import _ from 'lodash'; +import uriComponentCodec from '../Common/uriComponentCodec'; +export { reverseMap } from '../Common/utils'; + +// `react-scripts test` sets NODE_ENV to "test". This module shouldn't be +// used at any other time. Thus we'll throw an error to ensure this. +if(process.env.NODE_ENV !== 'test') { + throw new Error('This module is only to be used during testing'); +} + +/** + * Generate random samples from an array. + * @param {any[]} arr - The array to sample from + * @param {number} [n=1] - The number of samples to take + * @param {number} [minSz=1] - The minimum sample size to take from `arr` + * @param {number} [maxSz=`arr.length`] - The max sample size to take from `arr` + * @returns {Array} + */ +export const randomSamples = (arr, n = 1, minSz = 1, maxSz = arr.length) => + Array.from({ length: n }, () => _.sampleSize(arr, _.random(minSz, maxSz))); + +/** + * Returns a new object with only `keepKeys`. If the corresponding value to one + * of `keepKeys` is an object, that object is similarly filtered down to + * only `keepKeys`. If the corresponding value to one of `keepKeys` is an + * array, + * an attempt is made to run the same filtering operation on each element. + * @example + * > const TESTPLAN_REPORT = + * require('../mocks/documents/TESTPLAN_REPORT_2.json'); + * > const TESTPLAN_REPORT_SLIM = filterObjectDeep(TESTPLAN_REPORT, ['name', + * 'entries', 'category']) + * > TESTPLAN_REPORT_SLIM + * { + * name: "Sample Testplan", + * entries: [ + * { + * name: "Primary", + * category: "multitest", + * entries: [ + * { + * category: "testsuite", + * name: "AlphaSuite", + * entries: [ + * { + * name: "test_equality_passing", + * category: "testcase", + * entries: [ + * { + * category: "DEFAULT" + * } + * ] + * }, + * { + * name: "test_equality_passing2", + * category: "testcase", + * entries: [ + * { + * category: "DEFAULT" + * } + * ] + * } + * ] + * }, + * { + * category: "testsuite", + * name: "BetaSuite", + * entries: [ + * { + * name: "test_equality_passing", + * category: "testcase", + * entries: [ + * { + * category: "DEFAULT" + * } + * ] + * } + * ] + * } + * ] + * }, + * { + * name: "Secondary", + * category: "multitest", + * entries: [ + * { + * category: "testsuite", + * name: "GammaSuite", + * entries: [ + * { + * name: "test_equality_passing", + * category: "testcase", + * entries: [ + * { + * category: "DEFAULT" + * } + * ] + * } + * ] + * } + * ] + * } + * ] + * } + * + * @param {Object.} obj - Object to filter + * @param {string[]} keepKeys - Keys to keep from `obj` + * @returns {Object.} A copy of `obj` that contains only + * `keepKeys` + */ +export const filterObjectDeep = (obj, keepKeys) => + Object.fromEntries(Object.entries(obj) + .filter(([prop]) => keepKeys.includes(prop)) + .map(([prop, val]) => [ + prop, (function handle(v) { + return Array.isArray(v) + ? v.map(_v => handle(_v)) + : _.isObject(v) + ? filterObjectDeep(v, keepKeys) + : v; + })(val) + ]) + ); + +/** + * Adapted from {@link https://stackoverflow.com/a/36128759|this SO answer} - + * Returns an array of all possible paths for an object such that any element + * can be used as the 2nd argument the {@link _.at} funtion. + * @example + * > // using TESTPLAN_REPORT_SLIM from the `filterObjectDeep` jsdoc example + * > const TESTPLAN_REPORT_SLIM_PATH_STRINGS = getPaths(TESTPLAN_REPORT_SLIM); + * > const TESTPLAN_REPORT_SLIM_PATH_ARRAYS = getPaths(TESTPLAN_REPORT_SLIM, + * true); + * > TESTPLAN_REPORT_SLIM_PATH_STRINGS + * [ + * 'name', + * 'entries', + * 'entries[0]', + * 'entries[0].name', + * 'entries[0].category', + * 'entries[0].entries', + * 'entries[0].entries[0]', + * 'entries[0].entries[0].category', + * 'entries[0].entries[0].name', + * 'entries[0].entries[0].entries', + * 'entries[0].entries[0].entries[0]', + * 'entries[0].entries[0].entries[0].name', + * 'entries[0].entries[0].entries[0].category', + * 'entries[0].entries[0].entries[0].entries', + * 'entries[0].entries[0].entries[0].entries[0]', + * 'entries[0].entries[0].entries[0].entries[0].category', + * 'entries[0].entries[0].entries[1]', + * 'entries[0].entries[0].entries[1].name', + * 'entries[0].entries[0].entries[1].category', + * 'entries[0].entries[0].entries[1].entries', + * 'entries[0].entries[0].entries[1].entries[0]', + * 'entries[0].entries[0].entries[1].entries[0].category', + * 'entries[0].entries[1]', + * 'entries[0].entries[1].category', + * 'entries[0].entries[1].name', + * 'entries[0].entries[1].entries', + * 'entries[0].entries[1].entries[0]', + * 'entries[0].entries[1].entries[0].name', + * 'entries[0].entries[1].entries[0].category', + * 'entries[0].entries[1].entries[0].entries', + * 'entries[0].entries[1].entries[0].entries[0]', + * 'entries[0].entries[1].entries[0].entries[0].category', + * 'entries[1]', + * 'entries[1].name', + * 'entries[1].category', + * 'entries[1].entries', + * 'entries[1].entries[0]', + * 'entries[1].entries[0].category', + * 'entries[1].entries[0].name', + * 'entries[1].entries[0].entries', + * 'entries[1].entries[0].entries[0]', + * 'entries[1].entries[0].entries[0].name', + * 'entries[1].entries[0].entries[0].category', + * 'entries[1].entries[0].entries[0].entries', + * 'entries[1].entries[0].entries[0].entries[0]', + * 'entries[1].entries[0].entries[0].entries[0].category' + * ] + * + * > TESTPLAN_REPORT_SLIM_PATH_ARRAYS + * + * [ + * [ 'name' ], + * [ 'entries' ], + * [ 'entries', '0' ], + * [ 'entries', '0', 'name' ], + * [ 'entries', '0', 'category' ], + * [ 'entries', '0', 'entries' ], + * [ 'entries', '0', 'entries', '0' ], + * [ 'entries', '0', 'entries', '0', 'category' ], + * [ 'entries', '0', 'entries', '0', 'name' ], + * [ 'entries', '0', 'entries', '0', 'entries' ], + * [ 'entries', '0', 'entries', '0', 'entries', '0' ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'name', + * ], + * [ 'entries', '0', 'entries', '0', 'entries', '0', 'category' ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'entries', + * ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'category', + * ], + * [ 'entries', '0', 'entries', '0', 'entries', '1' ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '1', + * 'name', + * ], + * [ 'entries', '0', 'entries', '0', 'entries', '1', 'category' ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '1', + * 'entries', + * ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * ], + * [ + * 'entries', '0', + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * 'category', + * ], + * [ 'entries', '0', 'entries', '1' ], + * [ 'entries', '0', 'entries', '1', 'category' ], + * [ 'entries', '0', 'entries', '1', 'name' ], + * [ 'entries', '0', 'entries', '1', 'entries' ], + * [ 'entries', '0', 'entries', '1', 'entries', '0' ], + * [ + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * 'name', + * ], + * [ 'entries', '0', 'entries', '1', 'entries', '0', 'category' ], + * [ + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * 'entries', + * ], + * [ + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * ], + * [ + * 'entries', '0', + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * 'category', + * ], + * [ 'entries', '1' ], + * [ 'entries', '1', 'name' ], + * [ 'entries', '1', 'category' ], + * [ 'entries', '1', 'entries' ], + * [ 'entries', '1', 'entries', '0' ], + * [ 'entries', '1', 'entries', '0', 'category' ], + * [ 'entries', '1', 'entries', '0', 'name' ], + * [ 'entries', '1', 'entries', '0', 'entries' ], + * [ 'entries', '1', 'entries', '0', 'entries', '0' ], + * [ + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * 'name', + * ], + * [ 'entries', '1', 'entries', '0', 'entries', '0', 'category' ], + * [ + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * 'entries', + * ], + * [ + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * ], + * [ + * 'entries', '1', + * 'entries', '0', + * 'entries', '0', + * 'entries', '0', + * 'category', + * ], + * ] + * + * @param {object} obj - A plain non-instance object + * @param {boolean} [asArrays=false] - Pass `true` to return array-form paths + * @returns {string[] | Array} + */ +export const getPaths = (obj, asArrays = false) => + asArrays ? getPathArrays(obj) : getPathStrings(obj); + +const getPathStrings = _.memoize(obj => { + const pathStrings = []; + (function walk(subObj, prevPathStr = '') { + for(const [ prop, val ] of Object.entries(subObj)) { + const propPathString = !prevPathStr ? prop : `${prevPathStr}.${prop}`; + pathStrings.push(propPathString); + if(_.isPlainObject(val)) { + walk(val, propPathString); + } else if(Array.isArray(val)) { + val.forEach((v, i) => { + const elementPathString = `${propPathString}[${i}]`; + pathStrings.push(elementPathString); + const elementVal = val[i]; + if(_.isPlainObject(elementVal)) { + walk(elementVal, elementPathString); + } + }); + } + } + })(obj); + return pathStrings; +}); + +const getPathArrays = _.memoize( + obj => getPathStrings(obj).map(v => _.toPath(v)) +); + +/** + * Get the URL paths that could be traversed in a given report + * @example + > const aliasMap = new Map(); + > const path2PathArrayMap = new Map(); + > const expectedPaths = deriveURLPathsFromReport(TESTPLAN_REPORT_1, aliasMap, path2PathArrayMap); + > expectedPaths + [ + '/Sample Testplan', + '/Sample Testplan/Primary', + '/Sample Testplan/Primary/AlphaSuite', + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing', + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing2', + '/Sample Testplan/Primary/BetaSuite', + '/Sample Testplan/Primary/BetaSuite/test_equality_passing', + '/Sample Testplan/Secondary', + '/Sample Testplan/Secondary/GammaSuite', + '/Sample Testplan/Secondary/GammaSuite/test_equality_passing' + ] + > aliasMap + Map { + 'Sample Testplan' => 'Sample Testplan', + 'Primary' => 'Primary', + 'AlphaSuite' => 'AlphaSuite', + 'test_equality_passing' => 'test_equality_passing', + 'test_equality_passing2' => 'test_equality_passing2', + 'BetaSuite' => 'BetaSuite', + 'Secondary' => 'Secondary', + 'GammaSuite' => 'GammaSuite' +} + > path2PathArrayMap + Map { + '/Sample Testplan' => [ 'Sample Testplan' ], + '/Sample Testplan/Primary' => [ 'Sample Testplan', 'Primary' ], + '/Sample Testplan/Primary/AlphaSuite' => [ 'Sample Testplan', 'Primary', 'AlphaSuite' ], + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing' => [ + 'Sample Testplan', + 'Primary', + 'AlphaSuite', + 'test_equality_passing' + ], + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing2' => [ + 'Sample Testplan', + 'Primary', + 'AlphaSuite', + 'test_equality_passing2' + ], + '/Sample Testplan/Primary/BetaSuite' => [ 'Sample Testplan', 'Primary', 'BetaSuite' ], + '/Sample Testplan/Primary/BetaSuite/test_equality_passing' => [ + 'Sample Testplan', + 'Primary', + 'BetaSuite', + 'test_equality_passing' + ], + '/Sample Testplan/Secondary' => [ 'Sample Testplan', 'Secondary' ], + '/Sample Testplan/Secondary/GammaSuite' => [ 'Sample Testplan', 'Secondary', 'GammaSuite' ], + '/Sample Testplan/Secondary/GammaSuite/test_equality_passing' => [ + 'Sample Testplan', + 'Secondary', + 'GammaSuite', + 'test_equality_passing' + ] +} + * @param {object} report + * @param {null | Map} [aliasMap=null] + * @param {null | Map} [path2PathArrayMap=null] + * @param {null | Map} [path2ObjectPathMap=null] + * @returns {string[]} + */ +export function deriveURLPathsFromReport( + report, + aliasMap = null, + path2PathArrayMap = null, + path2ObjectPathMap = null, +) { + const pathMap = new Map(); + return getPaths(report, true) + // @ts-ignore + .filter(arrayPath => arrayPath.slice(-1)[0] === 'name') + .map(arrayPath => { + const + fullPathKey = arrayPath.slice(0, -1).join('.'), + pathBasename = _.get(report, arrayPath), + pathBasenameEncoded = uriComponentCodec.encode(pathBasename), + parentPathKey = arrayPath.slice(0, -3).join('.'), + parentPath = pathMap.get(parentPathKey) || '', + fullPathVal = `${parentPath}/${pathBasenameEncoded}`; + pathMap.set(fullPathKey, fullPathVal); + if(aliasMap !== null) { + aliasMap.set(pathBasenameEncoded, pathBasename); + } + if(path2PathArrayMap !== null) { + const parentPathArr = path2PathArrayMap.get(parentPath) || []; + path2PathArrayMap.set(fullPathVal, parentPathArr.concat(pathBasename)); + } + if(path2ObjectPathMap !== null) { + path2ObjectPathMap.set(fullPathVal, arrayPath); + } + return fullPathVal; + }); +} + +/** + * Like {@link https://lodash.com/docs/4.17.15#matches|this} but recursively. + * @example + > const obj = { + a: 11, + b: 2, + c: { a: 1, x: 'a' }, + d: [ + { a: 1, b: 22 }, + { a: 11, c: 33 }, + { a: 111, d: [ { a: 1, y: 'aa' } ]} + ] + } + > findAllDeep(obj, { a: 1 }, [ 'c', 'd' ]) + [ + { "a": 1, "x": "a" }, + { "a": 1, "b": 22 }, + { "a": 1, "y": "aa" }, + ] + > findAllDeep(obj, { a: 1 }, 'c') + [ { "a": 1, "x": "a" } ] + + * @param {Object.} srcObj - object to run the find on + * @param {Object.} matchObj - partial object to match against + * @param {null | string | string[]} [diveProps=null] - properties that will be searched recursively for matches + * @returns {object[]} array of all found objects + */ +export const findAllDeep = (srcObj, matchObj, diveProps = null) => + [ _.find([ srcObj ], matchObj) ].concat( + [ diveProps ].flat().filter(Boolean).flatMap( + prop => + srcObj[prop] + ? Array.isArray(srcObj[prop]) + ? srcObj[prop].flatMap(el => findAllDeep(el, matchObj, prop)) + : _.isPlainObject(srcObj[prop]) + ? [ _.find([ srcObj[prop] ], matchObj) ] + : [] + : [], + ), + ).filter(Boolean); diff --git a/testplan/web_ui/testing/src/__tests__/testUtils.test.js b/testplan/web_ui/testing/src/__tests__/testUtils.test.js new file mode 100644 index 000000000..fc92aa484 --- /dev/null +++ b/testplan/web_ui/testing/src/__tests__/testUtils.test.js @@ -0,0 +1,408 @@ +/** @jest-environment node */ +import { randomSamples } from './testUtils'; +import { getPaths } from './testUtils'; +import { reverseMap } from './testUtils'; +import { filterObjectDeep } from './testUtils'; +import { deriveURLPathsFromReport } from './testUtils'; +import { TESTPLAN_REPORT_2 as REPORT } from './documents'; + +describe('randomSamples', () => { + + const arr = [ 111, 'bBb', new Map([ [ 'CcC', 33 ] ]), { 'd-d': 4 } ]; + + it('takes the correct number of samples', () => { + // default n=1 + expect(randomSamples(arr)).toHaveLength(1); + expect(randomSamples(arr, 3)).toHaveLength(3); + }); + + it('takes samples of the correct size', () => { + // default n=1, minSz=1, maxSz=`arr.length` + const randArr1 = randomSamples(arr); + expect(randArr1).toHaveLength(1); + expect(randArr1[0]).toBeInstanceOf(Array); + expect(randArr1[0].length).toBeGreaterThanOrEqual(1); + expect(randArr1[0].length).toBeLessThanOrEqual(arr.length); + + const n = 2, minSz = 2, maxSz = 3; + const randArr2 = randomSamples(arr, n, minSz, maxSz); + expect(randArr2).toHaveLength(2); + for(const sample of randArr2) { + expect(sample).toBeInstanceOf(Array); + expect(sample.length).toBeGreaterThanOrEqual(minSz); + expect(sample.length).toBeLessThanOrEqual(maxSz); + } + + }); + +}); + +const TESTPLAN_REPORT_SLIM = filterObjectDeep( + REPORT, + [ 'name', 'entries', 'category' ], +); + +it('`filterObjectDeep` can replicate the jsdoc example', () => { + expect(TESTPLAN_REPORT_SLIM).toEqual({ + name: "Sample Testplan", + entries: [ + { + name: "Primary", + category: "multitest", + entries: [ + { + category: "testsuite", + name: "AlphaSuite", + entries: [ + { + name: "test_equality_passing", + category: "testcase", + entries: [ + { + category: "DEFAULT", + }, + ], + }, + { + name: "test_equality_passing2", + category: "testcase", + entries: [ + { + category: "DEFAULT", + }, + ], + }, + ], + }, + { + category: "testsuite", + name: "BetaSuite", + entries: [ + { + name: "test_equality_passing", + category: "testcase", + entries: [ + { + category: "DEFAULT", + }, + ], + }, + ], + }, + ], + }, + { + name: "Secondary", + category: "multitest", + entries: [ + { + category: "testsuite", + name: "GammaSuite", + entries: [ + { + name: "test_equality_passing", + category: "testcase", + entries: [ + { + category: "DEFAULT", + }, + ], + }, + ], + }, + ], + }, + ], + }); +}); + +it('`getPaths` can replicate the jsdoc example', () => { + const TESTPLAN_REPORT_SLIM_PATH_STRINGS = getPaths(TESTPLAN_REPORT_SLIM); + expect(TESTPLAN_REPORT_SLIM_PATH_STRINGS).toEqual([ + 'name', + 'entries', + 'entries[0]', + 'entries[0].name', + 'entries[0].category', + 'entries[0].entries', + 'entries[0].entries[0]', + 'entries[0].entries[0].category', + 'entries[0].entries[0].name', + 'entries[0].entries[0].entries', + 'entries[0].entries[0].entries[0]', + 'entries[0].entries[0].entries[0].name', + 'entries[0].entries[0].entries[0].category', + 'entries[0].entries[0].entries[0].entries', + 'entries[0].entries[0].entries[0].entries[0]', + 'entries[0].entries[0].entries[0].entries[0].category', + 'entries[0].entries[0].entries[1]', + 'entries[0].entries[0].entries[1].name', + 'entries[0].entries[0].entries[1].category', + 'entries[0].entries[0].entries[1].entries', + 'entries[0].entries[0].entries[1].entries[0]', + 'entries[0].entries[0].entries[1].entries[0].category', + 'entries[0].entries[1]', + 'entries[0].entries[1].category', + 'entries[0].entries[1].name', + 'entries[0].entries[1].entries', + 'entries[0].entries[1].entries[0]', + 'entries[0].entries[1].entries[0].name', + 'entries[0].entries[1].entries[0].category', + 'entries[0].entries[1].entries[0].entries', + 'entries[0].entries[1].entries[0].entries[0]', + 'entries[0].entries[1].entries[0].entries[0].category', + 'entries[1]', + 'entries[1].name', + 'entries[1].category', + 'entries[1].entries', + 'entries[1].entries[0]', + 'entries[1].entries[0].category', + 'entries[1].entries[0].name', + 'entries[1].entries[0].entries', + 'entries[1].entries[0].entries[0]', + 'entries[1].entries[0].entries[0].name', + 'entries[1].entries[0].entries[0].category', + 'entries[1].entries[0].entries[0].entries', + 'entries[1].entries[0].entries[0].entries[0]', + 'entries[1].entries[0].entries[0].entries[0].category', + ]); + + const TESTPLAN_REPORT_SLIM_PATH_ARRAYS = getPaths(TESTPLAN_REPORT_SLIM, true); + expect(TESTPLAN_REPORT_SLIM_PATH_ARRAYS).toEqual([ + [ 'name' ], + [ 'entries' ], + [ 'entries', '0' ], + [ 'entries', '0', 'name' ], + [ 'entries', '0', 'category' ], + [ 'entries', '0', 'entries' ], + [ 'entries', '0', 'entries', '0' ], + [ 'entries', '0', 'entries', '0', 'category' ], + [ 'entries', '0', 'entries', '0', 'name' ], + [ 'entries', '0', 'entries', '0', 'entries' ], + [ 'entries', '0', 'entries', '0', 'entries', '0' ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'name', + ], + [ 'entries', '0', 'entries', '0', 'entries', '0', 'category' ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'entries', + ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'entries', '0', + ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'category', + ], + [ 'entries', '0', 'entries', '0', 'entries', '1' ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '1', + 'name', + ], + [ 'entries', '0', 'entries', '0', 'entries', '1', 'category' ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '1', + 'entries', + ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '1', + 'entries', '0', + ], + [ + 'entries', '0', + 'entries', '0', + 'entries', '1', + 'entries', '0', + 'category', + ], + [ 'entries', '0', 'entries', '1' ], + [ 'entries', '0', 'entries', '1', 'category' ], + [ 'entries', '0', 'entries', '1', 'name' ], + [ 'entries', '0', 'entries', '1', 'entries' ], + [ 'entries', '0', 'entries', '1', 'entries', '0' ], + [ + 'entries', '0', + 'entries', '1', + 'entries', '0', + 'name', + ], + [ 'entries', '0', 'entries', '1', 'entries', '0', 'category' ], + [ + 'entries', '0', + 'entries', '1', + 'entries', '0', + 'entries', + ], + [ + 'entries', '0', + 'entries', '1', + 'entries', '0', + 'entries', '0', + ], + [ + 'entries', '0', + 'entries', '1', + 'entries', '0', + 'entries', '0', + 'category', + ], + [ 'entries', '1' ], + [ 'entries', '1', 'name' ], + [ 'entries', '1', 'category' ], + [ 'entries', '1', 'entries' ], + [ 'entries', '1', 'entries', '0' ], + [ 'entries', '1', 'entries', '0', 'category' ], + [ 'entries', '1', 'entries', '0', 'name' ], + [ 'entries', '1', 'entries', '0', 'entries' ], + [ 'entries', '1', 'entries', '0', 'entries', '0' ], + [ + 'entries', '1', + 'entries', '0', + 'entries', '0', + 'name', + ], + [ 'entries', '1', 'entries', '0', 'entries', '0', 'category' ], + [ + 'entries', '1', + 'entries', '0', + 'entries', '0', + 'entries', + ], + [ + 'entries', '1', + 'entries', '0', + 'entries', '0', + 'entries', '0', + ], + [ + 'entries', '1', + 'entries', '0', + 'entries', '0', + 'entries', '0', + 'category', + ], + ]); +}); + +describe('reverseMap', () => { + + const aMap = new Map([ + [ 'a', 1 ], + [ 'b', 2 ], + [ 'c', 3 ], + ]); + + const aRevMap = new Map([ + [ 1, 'a' ], + [ 2, 'b' ], + [ 3, 'c' ], + ]); + + it('reverses a map', () => { + expect(reverseMap(aMap)).toStrictEqual(aRevMap); + }); +}); + +describe('deriveURLPathsFromReport', () => { + + const TESTPLAN_REPORT_1 = require('./documents/TESTPLAN_REPORT_1.json'); + + it('does the jsdoc example', () => { + const aliasMap = new Map(); + const path2PathArrayMap = new Map(); + const expectedPaths = deriveURLPathsFromReport( + TESTPLAN_REPORT_1, aliasMap, path2PathArrayMap, + ); + expect(expectedPaths).toEqual([ + '/Sample Testplan', + '/Sample Testplan/Primary', + '/Sample Testplan/Primary/AlphaSuite', + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing', + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing2', + '/Sample Testplan/Primary/BetaSuite', + '/Sample Testplan/Primary/BetaSuite/test_equality_passing', + '/Sample Testplan/Secondary', + '/Sample Testplan/Secondary/GammaSuite', + '/Sample Testplan/Secondary/GammaSuite/test_equality_passing', + ]); + expect(aliasMap).toStrictEqual(new Map([ + [ 'Sample Testplan', 'Sample Testplan' ], + [ 'Primary', 'Primary' ], + [ 'AlphaSuite', 'AlphaSuite' ], + [ 'test_equality_passing', 'test_equality_passing' ], + [ 'test_equality_passing2', 'test_equality_passing2' ], + [ 'BetaSuite', 'BetaSuite' ], + [ 'Secondary', 'Secondary' ], + [ 'GammaSuite', 'GammaSuite' ], + ])); + expect(path2PathArrayMap).toEqual(new Map([ + [ '/Sample Testplan', [ 'Sample Testplan' ] ], + [ '/Sample Testplan/Primary', [ 'Sample Testplan', 'Primary' ] ], + [ + '/Sample Testplan/Primary/AlphaSuite', + [ 'Sample Testplan', 'Primary', 'AlphaSuite' ], + ], + [ + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing', [ + 'Sample Testplan', + 'Primary', + 'AlphaSuite', + 'test_equality_passing', + ], + ], + [ + '/Sample Testplan/Primary/AlphaSuite/test_equality_passing2', [ + 'Sample Testplan', + 'Primary', + 'AlphaSuite', + 'test_equality_passing2', + ], + ], + [ + '/Sample Testplan/Primary/BetaSuite', + [ 'Sample Testplan', 'Primary', 'BetaSuite' ], + ], + [ + '/Sample Testplan/Primary/BetaSuite/test_equality_passing', [ + 'Sample Testplan', + 'Primary', + 'BetaSuite', + 'test_equality_passing', + ], + ], + [ '/Sample Testplan/Secondary', [ 'Sample Testplan', 'Secondary' ] ], + [ + '/Sample Testplan/Secondary/GammaSuite', + [ 'Sample Testplan', 'Secondary', 'GammaSuite' ], + ], + [ + '/Sample Testplan/Secondary/GammaSuite/test_equality_passing', [ + 'Sample Testplan', + 'Secondary', + 'GammaSuite', + 'test_equality_passing', + ], + ], + ])); + + }); + +}); diff --git a/testplan/web_ui/testing/src/index.js b/testplan/web_ui/testing/src/index.js deleted file mode 100644 index 1fc7c4ae3..000000000 --- a/testplan/web_ui/testing/src/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import BatchReport from './Report/BatchReport'; -import InteractiveReport from './Report/InteractiveReport'; -import EmptyReport from './Report/EmptyReport'; -import {POLL_MS} from './Common/defaults.js'; - -// import registerServiceWorker from './registerServiceWorker'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; - -/** - * This single App provides multiple functions controlled via the URL path - * accessed. We are using React-Router to control which type of report is - * rendered and to extract the report UID from the URL when necessary. - */ -const AppRouter = () => ( - - - - - - - - - - - - -); - -ReactDOM.render(, document.getElementById('root')); -// registerServiceWorker(); diff --git a/testplan/web_ui/testing/src/index.jsx b/testplan/web_ui/testing/src/index.jsx new file mode 100644 index 000000000..6b7eb130d --- /dev/null +++ b/testplan/web_ui/testing/src/index.jsx @@ -0,0 +1,41 @@ +import 'bootstrap/dist/css/bootstrap.min.css'; +import React, { lazy } from 'react'; +import ReactDOM from 'react-dom'; +import { Route } from 'react-router'; + +import { POLL_MS } from './Common/defaults'; +import SwitchRequireSlash from './Common/SwitchRequireSlash'; +import AppWrapper from './state/AppWrapper'; + +// Don't make users download scripts that they won't use. +// see: https://reactjs.org/docs/code-splitting.html#route-based-code-splitting +const BatchReport = lazy(() => import('./Report/BatchReport')); +const InteractiveReport = lazy(() => import('./Report/InteractiveReport')); +const EmptyReport = lazy(() => import('./Report/EmptyReport')); +const Home = lazy(() => import('./Common/Home')); + +/** + * This single App provides multiple functions controlled via the URL path + * accessed. We are using React-Router to control which type of report is + * rendered and to extract the report UID from the URL when necessary. + */ +const App = () => ( + + + + + + } /> + + + + + + + {/* Must be last */} + + + +); + +ReactDOM.render(, document.getElementById('root')); diff --git a/testplan/web_ui/testing/src/setupTests.js b/testplan/web_ui/testing/src/setupTests.js index 7a1fee7e8..7900ecd19 100644 --- a/testplan/web_ui/testing/src/setupTests.js +++ b/testplan/web_ui/testing/src/setupTests.js @@ -1,4 +1,10 @@ +/** + * This file follows the rules of Jest's "setupFilesAfterEnv". + * @see https://jestjs.io/docs/en/configuration#setupfilesafterenv-array + */ +import 'expect-puppeteer'; +import '@testing-library/jest-dom/extend-expect'; import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; -configure({ adapter: new Adapter() }); \ No newline at end of file +configure({ adapter: new Adapter() }); diff --git a/testplan/web_ui/testing/src/state/AppProvider.jsx b/testplan/web_ui/testing/src/state/AppProvider.jsx new file mode 100644 index 000000000..4c1bccf02 --- /dev/null +++ b/testplan/web_ui/testing/src/state/AppProvider.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import Provider from 'react-redux/es/components/Provider'; +import ErrorCatch from '../Common/ErrorCatch'; +import store from './store'; + +export default function AppProvider({ children }) { + return ( + + + {children} + + + ); +} + diff --git a/testplan/web_ui/testing/src/state/AppRouter.jsx b/testplan/web_ui/testing/src/state/AppRouter.jsx new file mode 100644 index 000000000..aceaf8159 --- /dev/null +++ b/testplan/web_ui/testing/src/state/AppRouter.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { createBrowserHistory } from 'history'; +import { Router } from 'react-router'; +import ErrorCatch from '../Common/ErrorCatch'; + +export const appHistory = createBrowserHistory({ basename: '/' }); + +export default function AppRouter({ children }) { + return ( + + + {children} + + + ); +} diff --git a/testplan/web_ui/testing/src/state/AppWrapper.jsx b/testplan/web_ui/testing/src/state/AppWrapper.jsx new file mode 100644 index 000000000..f3bbba676 --- /dev/null +++ b/testplan/web_ui/testing/src/state/AppWrapper.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import LoadingAnimation from '../Common/LoadingAnimation'; +import AppRouter from './AppRouter'; +import AppProvider from './AppProvider'; +import ErrorCatch from '../Common/ErrorCatch'; + +export default function AppWrapper({ children }) { + return ( + + + + + }> + {children} + + + + + + ); +} diff --git a/testplan/web_ui/testing/src/state/__tests__/appSlice.test.js b/testplan/web_ui/testing/src/state/__tests__/appSlice.test.js new file mode 100644 index 000000000..2b3430fac --- /dev/null +++ b/testplan/web_ui/testing/src/state/__tests__/appSlice.test.js @@ -0,0 +1,29 @@ +import configureStore from 'redux-mock-store'; +import appSlice from '../appSlice'; +import * as appActions from '../appActions'; +import * as appSelectors from '../appSelectors'; +import appMiddleware from '../appMiddleware'; + +describe('PLACEHOLDER', () => { + + beforeAll(() => { + + }); + + beforeEach(() => { + + }); + + it('PLACEHOLDER', () => { + + }); + + afterEach(() => { + + }); + + afterAll(() => { + + }); + +}); diff --git a/testplan/web_ui/testing/src/state/__tests__/store.test.js b/testplan/web_ui/testing/src/state/__tests__/store.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/testplan/web_ui/testing/src/state/appActions.js b/testplan/web_ui/testing/src/state/appActions.js new file mode 100644 index 000000000..5bcbefe07 --- /dev/null +++ b/testplan/web_ui/testing/src/state/appActions.js @@ -0,0 +1,7 @@ +import appSlice from './appSlice'; + +export const { // eslint-disable-line no-empty-pattern + setIsDevel, + setIsTesting, + setSkipFetch, +} = appSlice.actions; diff --git a/testplan/web_ui/testing/src/state/appMiddleware.js b/testplan/web_ui/testing/src/state/appMiddleware.js new file mode 100644 index 000000000..3df93beb9 --- /dev/null +++ b/testplan/web_ui/testing/src/state/appMiddleware.js @@ -0,0 +1,11 @@ +import URLParamRegistry from '../Common/URLParamRegistry'; +import { appHistory } from './AppRouter'; +import { setIsDevel } from './appActions'; +import { setIsTesting } from './appActions'; +import { setSkipFetch } from './appActions'; + +export default new URLParamRegistry(appHistory) + .registerBidirectionalListener('isDevel', setIsDevel) + .registerBidirectionalListener('isTesting', setIsTesting) + .registerBidirectionalListener('skipFetch', setSkipFetch) + .createMiddleware(); diff --git a/testplan/web_ui/testing/src/state/appSelectors.js b/testplan/web_ui/testing/src/state/appSelectors.js new file mode 100644 index 000000000..f327071e6 --- /dev/null +++ b/testplan/web_ui/testing/src/state/appSelectors.js @@ -0,0 +1,18 @@ + +export const mkGetApiHeaders = () => st => st.app.apiHeaders; +export const getApiHeaders = mkGetApiHeaders(); + +export const mkGetApiBaseURL = () => st => st.app.apiBaseURL; +export const getApiBaseURL = mkGetApiBaseURL(); + +export const mkGetIsTesting = () => st => st.app.isTesting; +export const getIsTesting = mkGetIsTesting(); + +export const mkGetIsDevel = () => st => st.app.isDevel; +export const getIsDevel = mkGetIsDevel(); + +export const mkGetSkipFetch = () => st => st.app.skipFetch; +export const getSkipFetch = mkGetSkipFetch(); + +export const mkGetDocumentationURL = () => st => st.app.apiBaseURL; +export const getDocumentationURL = mkGetDocumentationURL(); diff --git a/testplan/web_ui/testing/src/state/appSlice.js b/testplan/web_ui/testing/src/state/appSlice.js new file mode 100644 index 000000000..fc3259a7f --- /dev/null +++ b/testplan/web_ui/testing/src/state/appSlice.js @@ -0,0 +1,55 @@ +// @ts-nocheck +import { createSlice } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; +const REACT_APP_API_BASE_URL = process.env.REACT_APP_API_BASE_URL; +const API_BASE_URL = + __DEV__ && REACT_APP_API_BASE_URL !== undefined + ? new URL(REACT_APP_API_BASE_URL) + : new URL('/api/v1', window.location.origin); + +// CORS headers: developer.mozilla.org/en-US/docs/Web/HTTP/CORS +const API_CORS_HEADERS = + window.location.origin !== API_BASE_URL.origin + ? { 'Access-Control-Allow-Origin': API_BASE_URL.origin } + : {}; + +/** + * This state slice contains app-wide information that could be used in any + * part of the app + */ +export default createSlice({ + name: 'app', + initialState: { + isTesting: process.env.NODE_ENV === 'test', + isDevel: process.env.NODE_ENV === 'development', + skipFetch: false, + documentationURL: 'http://testplan.readthedocs.io/', + apiBaseURL: API_BASE_URL.toString(), + apiHeaders: { + ...API_CORS_HEADERS, + 'Accept': 'application/json', + 'Accept-Charset': 'utf-8', + }, + }, + reducers: { + setIsDevel: { + reducer(state, { payload }) { state.isDevel = payload; }, + prepare: (isDevel = false) => ({ + payload: __DEV__ ? !!isDevel : false, // always false in production + }), + }, + setIsTesting: { + reducer(state, { payload }) { state.isTesting = payload; }, + prepare: (isTesting = false) => ({ + payload: __DEV__ ? !!isTesting : false, // always false in production + }), + }, + setSkipFetch: { + reducer(state, { payload }) { state.skipFetch = payload; }, + prepare: (skipFetch = false) => ({ + payload: __DEV__ ? !!skipFetch : false, // always false in production + }), + }, + }, +}); diff --git a/testplan/web_ui/testing/src/state/store.js b/testplan/web_ui/testing/src/state/store.js new file mode 100644 index 000000000..5c0ab5b63 --- /dev/null +++ b/testplan/web_ui/testing/src/state/store.js @@ -0,0 +1,22 @@ +import { configureStore } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import { combineReducers } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import { getDefaultMiddleware } from '@reduxjs/toolkit/dist/redux-toolkit.esm'; +import appMiddleware from './appMiddleware'; +import uiMiddleware from '../Report/BatchReport/state/uiMiddleware'; + +const createReducer = () => combineReducers({ + app: require('./appSlice').default.reducer, + report: require('../Report/BatchReport/state/reportSlice').default.reducer, + ui: require('../Report/BatchReport/state/uiSlice').default.reducer, +}); + +const store = configureStore({ + reducer: createReducer(), + middleware: [ appMiddleware, uiMiddleware, ...getDefaultMiddleware() ], +}); + +if(process.env.NODE_ENV !== 'production' && module && module.hot) { + module.hot.accept(() => store.replaceReducer(createReducer())); +} + +export default store;