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) => (
+
+ ))
+ ) : 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 (
-
- ]}
- />
-
- {centerPane}
-
- );
- }
-}
-
-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`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Print page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Help
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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,
+}) => (
+
+
+
+ {' ' + label}
+
+
+));
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,
+}) => (
+
+
+
+ {' ' + label}
+
+
+));
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!
+
+
+ Close
+
+
+));
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
+
+
+
+
+ Close
+
+
+));
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 (
+
+ );
+}, [
+ 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,
+}) => (
+
+));
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
+}) => (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+));
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;