diff --git a/.gitignore b/.gitignore
index 51c269e1b..8b843d479 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,5 @@ Thumbs.db
/doc/en/_build/
/doc/en/html
/pip-wheel-metadata
+node_modules/
+yarn-error.log
diff --git a/.travis.yml b/.travis.yml
index bc9bd2e12..6e872bbeb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,10 @@ dist: bionic
language: python
env:
- - BLACK_VERSION: "19.10b0"
+ global:
+ - REACT_APP_API_BASE_URL: "/fake/api/endpoint"
+ jobs:
+ - BLACK_VERSION: "19.10b0"
python:
- "2.7"
@@ -53,4 +56,3 @@ script:
- ./scripts/utils/crlf_check.sh
- pylint --rcfile pylintrc testplan
- pytest tests --verbose
-
diff --git a/testplan/web_ui/testing/.env b/testplan/web_ui/testing/.env
new file mode 100644
index 000000000..d0032cefe
--- /dev/null
+++ b/testplan/web_ui/testing/.env
@@ -0,0 +1,50 @@
+# When debugging locally, create a file ".env.local" next to this file
+# containing:
+
+REACT_APP_API_BASE_URL=/path/to/api/endpoint
+
+# ... if your API is served from the same origin as this webapp. For example,
+# if this web app is available at "https://www.myapp.mil/" and the API is
+# available at "https://www.myapp.mil/api" then you'd set:
+
+REACT_APP_API_BASE_URL=/api
+
+# If your API is served from a different origin (e.g. if you're debugging
+# locally at http://localhost:3000) than your API (say it's still
+# "https://www.myapp.mil/api") then set:
+
+REACT_APP_API_BASE_URL=https://www.myapp.mil/api
+
+# This will also work if the web app and API are served from the same origin.
+# The app will set basic CORS headers when NODE_ENV == "development" or "test".
+# More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+#
+#=============================================================================#
+# Note that NOT setting REACT_APP_API_BASE_URL - either in the environment or #
+# in another .env.* file - will result in a build error. #
+#=============================================================================#
+#
+# The .env.* files override one another like so (highest > lowest priority):
+# > when NODE_ENV === "development":
+# - (variables from environment)
+# - .env.development.local
+# - .env.development
+# - .env.local
+# - .env
+# > when NODE_ENV === "production":
+# - (variables from environment)
+# - .env.production.local
+# - .env.production
+# - .env.local
+# - .env
+# > when NODE_ENV === "test": (omits .env.local)
+# - (variables from environment)
+# - .env.test.local
+# - .env.test
+# - .env
+#
+# More info:
+# - https://create-react-app.dev/docs/adding-custom-environment-variables/#what-other-env-files-can-be-used
+# - https://create-react-app.dev/docs/advanced-configuration
+#
+REACT_APP_API_BASE_URL=OverrideMeOrThereWillBeABuildError
diff --git a/testplan/web_ui/testing/.eslintrc.json b/testplan/web_ui/testing/.eslintrc.json
index ce2387180..36a8cbdbf 100644
--- a/testplan/web_ui/testing/.eslintrc.json
+++ b/testplan/web_ui/testing/.eslintrc.json
@@ -24,7 +24,7 @@
"react"
],
"rules": {
- "max-len": ["error", { "code": 80 }],
+ "max-len": ["error", { "code": 80, "comments": 120 }],
"linebreak-style": [
"error",
"unix"
diff --git a/testplan/web_ui/testing/package.json b/testplan/web_ui/testing/package.json
index 6cb1c9926..1a60cc224 100644
--- a/testplan/web_ui/testing/package.json
+++ b/testplan/web_ui/testing/package.json
@@ -3,6 +3,7 @@
"version": "0.2.0",
"private": true,
"resolutions": {
+ "@babel/core": "^7.0.0",
"@babel/preset-env": "^7.8.7",
"watchpack": "1.6.1"
},
@@ -10,6 +11,7 @@
"@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",
@@ -17,18 +19,24 @@
"bootstrap": "4.3.1",
"date-fns": "^2.14.0",
"eslint-plugin-react": "^7.14.3",
- "react": "16.6.0",
+ "history": "~4.10.1",
+ "immer": "^6.0.0",
+ "lodash": "~4.17.15",
+ "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": {
"enzyme": "3.7.0",
@@ -41,8 +49,8 @@
"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": "/",
@@ -62,6 +70,5 @@
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
- },
- "proxy": "http://localhost:4000"
+ }
}
diff --git a/testplan/web_ui/testing/public/index.html b/testplan/web_ui/testing/public/index.html
index 7cc6f4511..7a4a98d73 100644
--- a/testplan/web_ui/testing/public/index.html
+++ b/testplan/web_ui/testing/public/index.html
@@ -6,7 +6,15 @@
-
+
+ <% if(process.env.NODE_ENV !== 'production') { %>
+ <% if(process.env.REACT_APP_DEVTOOLS) { %>
+
+ <% } %>
+ <% } %>
Testplan
diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/dictAssertionUtils.js b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/dictAssertionUtils.js
index 2f7aafd52..e0d60ff72 100644
--- a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/dictAssertionUtils.js
+++ b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/DictAssertions/dictAssertionUtils.js
@@ -1,3 +1,4 @@
+import _ from 'lodash';
import {any, sorted, domToString} from './../../../Common/utils';
import {DICT_GRID_STYLE} from './../../../Common/defaults';
@@ -51,8 +52,8 @@ function sortFlattenedJSON(
}
}
- const set = new Set(origFlattenedJSON.map(line => line[0]));
- const allItemsAreSameLevel = set.size === 1;
+ const set = _.uniq(origFlattenedJSON.map(line => line[0]));
+ const allItemsAreSameLevel = set.length === 1;
// if all remaining items of the list are on the same depth level,
// they can be sorted and returned
diff --git a/testplan/web_ui/testing/src/Common/Message.js b/testplan/web_ui/testing/src/Common/Message.js
index 52937c0cb..c560f5b64 100644
--- a/testplan/web_ui/testing/src/Common/Message.js
+++ b/testplan/web_ui/testing/src/Common/Message.js
@@ -13,9 +13,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}
);
}
diff --git a/testplan/web_ui/testing/src/Common/__tests__/testUtils.test.js b/testplan/web_ui/testing/src/Common/__tests__/testUtils.test.js
new file mode 100644
index 000000000..ca6039349
--- /dev/null
+++ b/testplan/web_ui/testing/src/Common/__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 } from '../fakeReport';
+
+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(
+ TESTPLAN_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', () => {
+
+ it('does the jsdoc example', () => {
+ const aliasMap = new Map();
+ const path2PathArrayMap = new Map();
+ const expectedPaths = deriveURLPathsFromReport(
+ TESTPLAN_REPORT, aliasMap, path2PathArrayMap,
+ );
+ expect(expectedPaths).toEqual([
+ '/Sample%20Testplan',
+ '/Sample%20Testplan/Primary',
+ '/Sample%20Testplan/Primary/AlphaSuite',
+ '/Sample%20Testplan/Primary/AlphaSuite/test_equality_passing',
+ '/Sample%20Testplan/Primary/AlphaSuite/test_equality_passing2',
+ '/Sample%20Testplan/Primary/BetaSuite',
+ '/Sample%20Testplan/Primary/BetaSuite/test_equality_passing',
+ '/Sample%20Testplan/Secondary',
+ '/Sample%20Testplan/Secondary/GammaSuite',
+ '/Sample%20Testplan/Secondary/GammaSuite/test_equality_passing',
+ ]);
+ expect(aliasMap).toStrictEqual(new Map([
+ [ 'Sample%20Testplan', '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%20Testplan', [ 'Sample Testplan' ] ],
+ [ '/Sample%20Testplan/Primary', [ 'Sample Testplan', 'Primary' ] ],
+ [
+ '/Sample%20Testplan/Primary/AlphaSuite',
+ [ 'Sample Testplan', 'Primary', 'AlphaSuite' ],
+ ],
+ [
+ '/Sample%20Testplan/Primary/AlphaSuite/test_equality_passing',
+ [
+ 'Sample Testplan',
+ 'Primary',
+ 'AlphaSuite',
+ 'test_equality_passing',
+ ],
+ ],
+ [
+ '/Sample%20Testplan/Primary/AlphaSuite/test_equality_passing2', [
+ 'Sample Testplan',
+ 'Primary',
+ 'AlphaSuite',
+ 'test_equality_passing2',
+ ],
+ ],
+ [
+ '/Sample%20Testplan/Primary/BetaSuite',
+ [ 'Sample Testplan', 'Primary', 'BetaSuite' ],
+ ],
+ [
+ '/Sample%20Testplan/Primary/BetaSuite/test_equality_passing', [
+ 'Sample Testplan',
+ 'Primary',
+ 'BetaSuite',
+ 'test_equality_passing',
+ ],
+ ],
+ [ '/Sample%20Testplan/Secondary', [ 'Sample Testplan', 'Secondary' ] ],
+ [
+ '/Sample%20Testplan/Secondary/GammaSuite',
+ [ 'Sample Testplan', 'Secondary', 'GammaSuite' ],
+ ],
+ [
+ '/Sample%20Testplan/Secondary/GammaSuite/test_equality_passing',
+ [
+ 'Sample Testplan',
+ 'Secondary',
+ 'GammaSuite',
+ 'test_equality_passing',
+ ],
+ ],
+ ]));
+
+ });
+
+});
diff --git a/testplan/web_ui/testing/src/Common/defaults.js b/testplan/web_ui/testing/src/Common/defaults.js
index baaf8a2f2..fd906b1b6 100644
--- a/testplan/web_ui/testing/src/Common/defaults.js
+++ b/testplan/web_ui/testing/src/Common/defaults.js
@@ -12,6 +12,7 @@ const MEDIUM_GREY = '#D0D0D0';
const DARK_GREY = '#ADADAD';
const BLACK = '#404040';
+export const BOTTOMMOST_ENTRY_CATEGORY = 'testcase';
const COLUMN_WIDTH = 22; // unit: em
const MIN_COLUMN_WIDTH = 180; // unit: px
const INTERACTIVE_COL_WIDTH = 28; // wider to fit interactive buttons
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..f6dcf7fc6
--- /dev/null
+++ b/testplan/web_ui/testing/src/Common/filterStates.js
@@ -0,0 +1,3 @@
+export const ALL = 'all';
+export const FAILED = 'fail';
+export const PASSED = 'pass';
diff --git a/testplan/web_ui/testing/src/Common/sampleReports.js b/testplan/web_ui/testing/src/Common/sampleReports.js
index 8adbfeb72..52f705b56 100644
--- a/testplan/web_ui/testing/src/Common/sampleReports.js
+++ b/testplan/web_ui/testing/src/Common/sampleReports.js
@@ -356,7 +356,7 @@ const FakeInteractiveReport = {
entries: [],
logs: [],
name: "test_interactive",
- name_type_index: new Set(),
+ name_type_index: [],
status: 'unknown',
runtime_status: 'ready',
status_override: null,
@@ -369,7 +369,7 @@ const FakeInteractiveReport = {
}],
logs: [],
name: "Interactive Suite",
- name_type_index: new Set(),
+ name_type_index: [],
part: null,
status: 'unknown',
runtime_status: 'ready',
@@ -382,7 +382,7 @@ const FakeInteractiveReport = {
}],
logs: [],
name: "Interactive MTest",
- name_type_index: new Set(),
+ name_type_index: [],
part: null,
status: 'unknown',
runtime_status: 'ready',
@@ -395,7 +395,7 @@ const FakeInteractiveReport = {
}],
meta: {},
name: "Fake Interactive Report",
- name_type_index: new Set(),
+ name_type_index: [],
status: 'unknown',
runtime_status: 'ready',
status_override: null,
diff --git a/testplan/web_ui/testing/src/Common/testUtils.js b/testplan/web_ui/testing/src/Common/testUtils.js
new file mode 100644
index 000000000..ea8405a55
--- /dev/null
+++ b/testplan/web_ui/testing/src/Common/testUtils.js
@@ -0,0 +1,500 @@
+/**
+ * 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';
+export { reverseMap } from './utils';
+
+// `react-scripts test` sets NODE_ENV to "test". This module shouldn't be
+// used at any other time since it would needlessly fatten the build.
+// Move a function to "../utils.js" to use something here in production.
+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 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)
+ .filter(arrayPath => arrayPath.slice(-1)[0] === 'name')
+ .map(arrayPath => {
+ const
+ fullPathKey = arrayPath.slice(0, -1).join('.'),
+ pathBasename = _.get(report, arrayPath),
+ pathBasenameEncoded = encodeURIComponent(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/Common/utils.js b/testplan/web_ui/testing/src/Common/utils.js
index d24423cc9..271279990 100644
--- a/testplan/web_ui/testing/src/Common/utils.js
+++ b/testplan/web_ui/testing/src/Common/utils.js
@@ -2,6 +2,7 @@
* Common utility functions.
*/
import {NAV_ENTRY_DISPLAY_DATA} from "./defaults";
+import _ from 'lodash';
/**
* Get the data to be used when displaying the nav entry.
@@ -97,3 +98,69 @@ export {
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 isNonemptyArray = x => Array.isArray(x) && x.length;
+
+export const unindent = (strArr, ...tagsArr) => strArr.slice(1).reduce(
+ (acc, str, i) => {
+ return `${acc}${`${tagsArr[i]}${str}`.replace(/^\s+/mg, '')}`;
+ },
+ strArr[0]
+).trimLeft();
+
+export const flattened = (strArr, ...tagsArr) => {
+ return _.spread(unindent)([ strArr, ...tagsArr ])
+ .replace(/[ \t]*\n/g, ' ')
+ .trimRight();
+};
+
+/**
+ * The difference between this and lodash.toPlainObject is that this also
+ * plain-objectifies the object's prototype.too. The _...*In*_ suffix follows
+ * the lodash naming scheme where the non-_...*In*_ function acts only on own
+ * properties and the _...*In*_ acts on own and inherited properties.
+ *
+ * Only values meeting the redux definition of "plain" types will be returned,
+ * e.g. the result shallowly omits functions and `symbol`'s.
+ *
+ * @example
+
+ > const err = new Error('oops');
+ > const errObjRepr = { name: 'Error', message: 'oops', stack: 'Uncaught Error: oops...' };
+ > require('lodash').isEqual(toPlainObjectIn(err), errObjRepr); // i.e. deep equals
+ true
+
+ * @param {object} obj
+ * @param {boolean} [enumerableOnly=false] - whether or not to skip
+ * non-enumerable properties like Lodash does.
+ * @returns {Object.}
+ */
+export const toPlainObjectIn = (obj, enumerableOnly = false) => {
+ const objectify = o => {
+ return Object.fromEntries(Object.entries(o)
+ .filter(([_, v]) => _.isPlainObject(v.value))
+ .filter(([_, v]) => !enumerableOnly || v.enumerable)
+ .map(([k, v]) => [k, v.value])
+ );
+ };
+ const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
+ const ownProps = objectify(ownDescriptors);
+ const proto = Object.getPrototypeOf(obj);
+ const protoDescriptors = Object.getOwnPropertyDescriptors(proto);
+ const inheritedProps = objectify(protoDescriptors);
+ return { ...inheritedProps, ...ownProps };
+};
+
+export const joinURLComponent = (base, component) => {
+ return `${base.replace(/\/+$/, '').trim()}/${component}`;
+};
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__/__snapshots__/InteractiveNav.test.js.snap b/testplan/web_ui/testing/src/Nav/__tests__/__snapshots__/InteractiveNav.test.js.snap
index e02afbeae..fb0e87353 100644
--- a/testplan/web_ui/testing/src/Nav/__tests__/__snapshots__/InteractiveNav.test.js.snap
+++ b/testplan/web_ui/testing/src/Nav/__tests__/__snapshots__/InteractiveNav.test.js.snap
@@ -36,7 +36,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
"entries": Array [],
"logs": Array [],
"name": "test_interactive",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"runtime_status": "ready",
"status": "unknown",
"status_override": null,
@@ -50,7 +50,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
],
"logs": Array [],
"name": "Interactive Suite",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"part": null,
"runtime_status": "ready",
"status": "unknown",
@@ -64,7 +64,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
],
"logs": Array [],
"name": "Interactive MTest",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"part": null,
"runtime_status": "ready",
"status": "unknown",
@@ -78,7 +78,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
],
"meta": Object {},
"name": "Fake Interactive Report",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"runtime_status": "ready",
"status": "unknown",
"status_override": null,
@@ -123,7 +123,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
"entries": Array [],
"logs": Array [],
"name": "test_interactive",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"runtime_status": "ready",
"status": "unknown",
"status_override": null,
@@ -137,7 +137,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
],
"logs": Array [],
"name": "Interactive Suite",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"part": null,
"runtime_status": "ready",
"status": "unknown",
@@ -151,7 +151,7 @@ exports[`InteractiveNav shallow renders and matches snapshot 1`] = `
],
"logs": Array [],
"name": "Interactive MTest",
- "name_type_index": Set {},
+ "name_type_index": Array [],
"part": null,
"runtime_status": "ready",
"status": "unknown",
diff --git a/testplan/web_ui/testing/src/Nav/navUtils.js b/testplan/web_ui/testing/src/Nav/navUtils.js
index d69083283..2cde66b6a 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',
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/AutoSelectRedirect.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/AutoSelectRedirect.jsx
new file mode 100644
index 000000000..0c4757267
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/AutoSelectRedirect.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Redirect } from 'react-router';
+import { connect } from 'react-redux';
+import _ from 'lodash';
+import { setSelectedEntry } from '../state/uiActions';
+import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults';
+import { joinURLComponent } from '../../../Common/utils';
+
+const connector = connect(
+ null,
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetSelectedEntry: entry => dispatch(setSelectedEntry(entry)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps, ownProps) {
+ const { boundSetSelectedEntry } = dispatchProps;
+ const { location, entry } = ownProps;
+ return {
+ getRedirectToEntry: () => {
+ const to = {
+ ...location,
+ pathname: joinURLComponent(location.pathname, entry.name || ''),
+ };
+ let currEntry = entry, nextEntry, nextAlias;
+ while(
+ _.isObjectLike(currEntry)
+ && currEntry.category !== BOTTOMMOST_ENTRY_CATEGORY
+ && Array.isArray(currEntry.entries)
+ && currEntry.entries.length === 1
+ && _.isObjectLike(nextEntry = currEntry.entries[0])
+ && _.isString(nextAlias = nextEntry.name)
+ ) {
+ currEntry = nextEntry;
+ const nextURLBasename = encodeURIComponent(nextAlias);
+ to.pathname = joinURLComponent(to.pathname, nextURLBasename);
+ }
+ boundSetSelectedEntry(currEntry);
+ return to;
+ },
+ };
+ },
+);
+
+const AutoSelectRedirect = ({ getRedirectToEntry }) => (
+
+);
+
+export default connector(AutoSelectRedirect);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/CenterPane.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/CenterPane.jsx
new file mode 100644
index 000000000..2a456409e
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/CenterPane.jsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from '@reduxjs/toolkit';
+import {
+ mkGetUIFilter,
+ mkGetUISelectedEntry,
+ mkGetUISidebarWidthFirstAvailable,
+} from '../state/uiSelectors';
+import {
+ mkGetReportDocument,
+ mkGetReportIsFetching,
+ mkGetReportLastFetchError,
+} from '../state/reportSelectors';
+import AssertionPane from '../../../AssertionPane/AssertionPane';
+import Message from '../../../Common/Message';
+import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults';
+import { isNonemptyArray } from '../../../Common/utils';
+import _ from 'lodash';
+
+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';
+// using a static placeholder - rather than creating a new one every time in
+// connect or the component - will help prevent unnecessary rerenders since
+// we'll maintain the same referential identity
+const SELECTED_ENTRY_PLACEHOLDER = {
+ uid: '',
+ category: '',
+ logs: [],
+ entries: [],
+ description: [],
+};
+
+const placeholderConnector = connect(
+ () => {
+ const getDocument = mkGetReportDocument();
+ const getIsFetching = mkGetReportIsFetching();
+ const getError = mkGetReportLastFetchError();
+ return function mapStateToProps(state) {
+ const isFetching = getIsFetching(state);
+ const error = getError(state);
+ const document = getDocument(state);
+ let message = STARTING_MSG;
+ if(!isFetching && error) {
+ message = _.isObject(error)
+ ? `${ERRORED_PREFIX_MSG}: ${error.message}`
+ : `${ERRORED_PREFIX_MSG}.`;
+ } else if(isFetching) {
+ message = FETCHING_MSG;
+ } else if(!isFetching && !error && _.isObject(document)) {
+ message = FINISHED_MSG;
+ }
+ return { message };
+ };
+ }
+);
+
+const Placeholder = ({ message, left }) => (
+
+);
+
+const ConnectedPlaceholder = placeholderConnector(Placeholder);
+
+const centerPaneConnector = connect(
+ () => {
+ const getFilter = mkGetUIFilter();
+ const getSelectedEntrySafe = createSelector(
+ mkGetUISelectedEntry(),
+ entry => (entry || {})
+ );
+ const getReportUID = createSelector(
+ mkGetReportDocument(),
+ document => (document || {}).uid,
+ );
+ const getSidebarWidth = mkGetUISidebarWidthFirstAvailable();
+ return function mapStateToProps(state) {
+ const selectedEntry = getSelectedEntrySafe(state);
+ return {
+ filter: getFilter(state),
+ selectedEntry: selectedEntry.category === BOTTOMMOST_ENTRY_CATEGORY
+ ? selectedEntry
+ : SELECTED_ENTRY_PLACEHOLDER,
+ reportUID: getReportUID(state),
+ left: getSidebarWidth(state),
+ };
+ };
+ },
+);
+
+const CenterPane = ({ selectedEntry, reportUID, filter, left }) => {
+ const { uid, category, logs, entries, description } = selectedEntry;
+ const descriptionEntries = React.useMemo(() => (
+ isNonemptyArray(description) ? description : [ description ]
+ ).filter(Boolean), [ description ]);
+ if(category === BOTTOMMOST_ENTRY_CATEGORY) {
+ return (
+
+ );
+ }
+ return ;
+};
+
+export default centerPaneConnector(CenterPane);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DisplayEmptyCheckBox.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DisplayEmptyCheckBox.jsx
new file mode 100644
index 000000000..373a1823d
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DisplayEmptyCheckBox.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Label, Input, DropdownItem } from 'reactstrap';
+import { css } from 'aphrodite';
+import { connect } from 'react-redux';
+import { mkGetUIIsDisplayEmpty } from '../state/uiSelectors';
+import { setDisplayEmpty } from '../state/uiActions';
+import navStyles from '../../../Toolbar/navStyles';
+
+const DROPDOWN_ITEM_CLASSES = css(navStyles.dropdownItem);
+const FILTER_LABEL_CLASSES = css(navStyles.filterLabel);
+
+const connector = connect(
+ () => {
+ const getIsDisplayEmpty = mkGetUIIsDisplayEmpty();
+ return function mapStateToProps(state) {
+ return {
+ isDisplayEmpty: getIsDisplayEmpty(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetDisplayEmpty: isDisplay => dispatch(setDisplayEmpty(isDisplay)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps, ownProps) {
+ const { isDisplayEmpty } = stateProps;
+ const { boundSetDisplayEmpty } = dispatchProps;
+ const { label } = ownProps;
+ return {
+ label: label || '',
+ isDisplayEmpty,
+ onChange: () => boundSetDisplayEmpty(!isDisplayEmpty),
+ };
+ }
+);
+
+const DisplayEmptyCheckBox = ({ label, isDisplayEmpty, onChange }) => (
+
+
+
+ {' ' + label}
+
+
+);
+
+export default connector(DisplayEmptyCheckBox);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DocumentationButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DocumentationButton.jsx
new file mode 100644
index 000000000..f60787728
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/DocumentationButton.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { NavItem } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faBook } from '@fortawesome/free-solid-svg-icons';
+import { TOOLBAR_BUTTON_CLASSES, BUTTONS_BAR_CLASSES } from '../styles';
+
+library.add(faBook);
+
+export default function DocumentationButton() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/EmptyListGroupItem.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/EmptyListGroupItem.jsx
new file mode 100644
index 000000000..12cc025e4
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/EmptyListGroupItem.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { ListGroupItem } from 'reactstrap';
+import { css } from 'aphrodite';
+import { navUtilsStyles } from '../styles';
+
+const LGI_CLASSES = css(navUtilsStyles.navButton);
+
+export default function EmptyListGroupItem() {
+ return (
+
+ No entries to display...
+
+ );
+}
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterButton.jsx
new file mode 100644
index 000000000..c6923e77d
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterButton.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import {
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+ UncontrolledDropdown,
+} from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faFilter } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import {
+ TOOLBAR_BUTTON_CLASSES,
+ BUTTONS_BAR_CLASSES,
+ FILTER_DROPDOWN_CLASSES,
+} from '../styles';
+import FilterRadioButton from './FilterRadioButton';
+import DisplayEmptyCheckBox from './DisplayEmptyCheckBox';
+import * as filterStates from '../../../Common/filterStates';
+
+library.add(faFilter);
+
+export default function FilterButton({ toolbarStyle }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterRadioButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterRadioButton.jsx
new file mode 100644
index 000000000..00a42f913
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/FilterRadioButton.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { DropdownItem, Input, Label } from 'reactstrap';
+import { connect } from 'react-redux';
+import { mkGetUIFilter } from '../state/uiSelectors';
+import { setFilter } from '../state/uiActions';
+import { DROPDOWN_ITEM_CLASSES, FILTER_LABEL_CLASSES } from '../styles';
+
+const connector = connect(
+ () => {
+ const getFilter = mkGetUIFilter();
+ return function mapStateToProps(state) {
+ return {
+ filter: getFilter(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ onChange: evt => {
+ dispatch(setFilter(evt.currentTarget.value));
+ },
+ };
+ },
+ function mergeProps(stateProps, dispatchProps, ownProps) {
+ const { filter } = stateProps;
+ const { onChange } = dispatchProps;
+ const { value, label } = ownProps;
+ return {
+ value,
+ label,
+ isChecked: filter === value,
+ onChange,
+ };
+ },
+);
+
+const FilterRadioButton = ({ isChecked, onChange, value, label }) => (
+
+
+
+ {' ' + label}
+
+
+);
+
+export default connector(FilterRadioButton);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpButton.jsx
new file mode 100644
index 000000000..4380c588a
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpButton.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { NavItem } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+import { connect } from 'react-redux';
+import { mkGetUIIsShowHelpModal } from '../state/uiSelectors';
+import { setShowHelpModal } from '../state/uiActions';
+import { TOOLBAR_BUTTON_CLASSES, BUTTONS_BAR_CLASSES } from '../styles';
+
+library.add(faQuestionCircle);
+
+const connector = connect(
+ () => {
+ const getIsShowHelpModal = mkGetUIIsShowHelpModal();
+ return function mapStateToProps(state) {
+ return {
+ isShowHelpModal: getIsShowHelpModal(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetShowHelpModal: isShow => dispatch(setShowHelpModal(isShow)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps) {
+ const { isShowHelpModal } = stateProps;
+ const { boundSetShowHelpModal } = dispatchProps;
+ return {
+ onClick: evt => {
+ evt.stopPropagation();
+ boundSetShowHelpModal(!isShowHelpModal);
+ },
+ };
+ },
+);
+
+/**
+ * Return the button which toggles the help modal.
+ * @returns {React.FunctionComponentElement}
+ */
+const HelpButton = ({ onClick }) => (
+
+
+
+);
+
+export default connector(HelpButton);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpModal.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpModal.jsx
new file mode 100644
index 000000000..cb6375d85
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/HelpModal.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { ModalHeader, ModalFooter, ModalBody, Modal, Button } from 'reactstrap';
+import { connect } from 'react-redux';
+import { mkGetUIIsShowHelpModal } from '../state/uiSelectors';
+import { setShowHelpModal } from '../state/uiActions';
+
+const connector = connect(
+ () => {
+ const getIsShowHelpModal = mkGetUIIsShowHelpModal();
+ return function mapStateToProps(state) {
+ return {
+ isShowHelpModal: getIsShowHelpModal(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetShowHelpModal: isShow => dispatch(setShowHelpModal(isShow)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps) {
+ const { isShowHelpModal } = stateProps;
+ const { boundSetShowHelpModal } = dispatchProps;
+ return {
+ isShowHelpModal,
+ toggleModal: () => boundSetShowHelpModal(!isShowHelpModal),
+ };
+ },
+);
+
+const HelpModal = ({ isShowHelpModal, toggleModal }) => (
+
+ Help
+
+ This is filter box help!
+
+
+ Close
+
+
+);
+
+export default connector(HelpModal);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoButton.jsx
new file mode 100644
index 000000000..571fc5277
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoButton.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { NavItem } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faInfo } from '@fortawesome/free-solid-svg-icons';
+import { connect } from 'react-redux';
+import { mkGetUIIsShowInfoModal } from '../state/uiSelectors';
+import { setShowInfoModal } from '../state/uiActions';
+import { TOOLBAR_BUTTON_CLASSES } from '../styles';
+import { BUTTONS_BAR_CLASSES } from '../styles';
+
+library.add(faInfo);
+
+const connector = connect(
+ () => {
+ const getIsShowInfoModal = mkGetUIIsShowInfoModal();
+ return function mapStateToProps(state) {
+ return {
+ isShowInfoModal: getIsShowInfoModal(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetShowInfoModal: isShow => dispatch(setShowInfoModal(isShow)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps) {
+ const { isShowInfoModal } = stateProps;
+ const { boundSetShowInfoModal } = dispatchProps;
+ return {
+ toggleInfo: evt => {
+ evt.stopPropagation();
+ boundSetShowInfoModal(!isShowInfoModal);
+ },
+ };
+ },
+);
+
+const InfoButton = ({ toggleInfo }) => (
+
+
+
+);
+
+export default connector(InfoButton);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoModal.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoModal.jsx
new file mode 100644
index 000000000..a996ecd70
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoModal.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { ModalHeader, ModalFooter, ModalBody, Modal, Button } from 'reactstrap';
+import { connect } from 'react-redux';
+import { mkGetUIIsShowInfoModal } from '../state/uiSelectors';
+import { setShowInfoModal } from '../state/uiActions';
+import InfoTable from './InfoTable';
+
+const connector = connect(
+ () => {
+ const getIsShowInfoModal = mkGetUIIsShowInfoModal();
+ return function mapStateToProps(state) {
+ return {
+ isShowInfoModal: getIsShowInfoModal(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetShowInfoModal: isShow => dispatch(setShowInfoModal(isShow)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps) {
+ const { isShowInfoModal } = stateProps;
+ const { boundSetShowInfoModal } = dispatchProps;
+ return {
+ isShowInfoModal,
+ toggleInfo: () => boundSetShowInfoModal(!isShowInfoModal),
+ };
+ },
+);
+
+const InfoModal = ({ isShowInfoModal, toggleInfo }) => (
+
+ Information
+
+
+
+
+ Close
+
+
+);
+
+export default connector(InfoModal);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoTable.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoTable.jsx
new file mode 100644
index 000000000..7c57c0f1d
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/InfoTable.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { css } from 'aphrodite';
+import { Table } from 'reactstrap';
+import { connect } from 'react-redux';
+import { mkGetReportDocument } from '../state/reportSelectors';
+import navStyles from '../../../Toolbar/navStyles';
+
+const INFO_TABLE_CLASSES = css(navStyles.infoTable);
+const INFO_TABLE_KEY_CLASSES = css(navStyles.infoTableKey);
+const INFO_TABLE_VAL_CLASSES = css(navStyles.infoTableValue);
+
+const connector = connect(
+ () => {
+ const getReportDocument = mkGetReportDocument();
+ return function mapStateToProps(state) {
+ return {
+ reportDocument: getReportDocument(state),
+ };
+ };
+ },
+);
+
+const InfoTable = ({ reportDocument }) => 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 (
+
+ );
+}, [ reportDocument ]);
+
+export default connector(InfoTable);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumb.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumb.jsx
new file mode 100644
index 000000000..345cbf78c
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumb.jsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { NavLink, withRouter } from 'react-router-dom';
+import _ from 'lodash';
+import { css } from 'aphrodite';
+import { connect } from 'react-redux';
+import { setSelectedEntry } from '../state/uiActions';
+import { mkGetUISelectedEntry } from '../state/uiSelectors';
+import {
+ CommonStyles,
+ navBreadcrumbStyles,
+ UNDECORATED_LINK_STYLE,
+ ACTIVE_LINK_CLASSES,
+} from '../styles';
+import NavEntry from '../../../Nav/NavEntry';
+import { joinURLComponent } from '../../../Common/utils';
+
+const BREADCRUMB_LINK_CLASSES = css(
+ navBreadcrumbStyles.breadcrumbEntry,
+ CommonStyles.unselectable,
+);
+
+const connector = connect(
+ () => {
+ const getSelectedEntry = mkGetUISelectedEntry();
+ return function mapStateToProps(state) {
+ return {
+ selectedEntry: getSelectedEntry(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetSelectedEntry: entry => dispatch(setSelectedEntry(entry)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps, ownProps) {
+ const { selectedEntry } = stateProps;
+ const { boundSetSelectedEntry } = dispatchProps;
+ const {
+ location: currentLocation,
+ match: { url: matchedUrl = '' },
+ entry,
+ } = ownProps;
+ const {
+ name: entryName = '',
+ uid: entryUid = '',
+ category: entryCategory = '',
+ status: entryStatus = '',
+ counter: {
+ passed: nPass = 0,
+ failed: nFail = 0,
+ error: nError = 0,
+ },
+ } = entry;
+ return {
+ entryName,
+ entryUid,
+ entryCategory,
+ entryStatus,
+ nPass,
+ nFailOrError: nFail + nError,
+ to: { ...currentLocation, pathname: joinURLComponent(matchedUrl, '') },
+ onClick: () => {
+ if(selectedEntry !== entry && _.isObject(entry)) {
+ boundSetSelectedEntry(entry);
+ }
+ },
+ };
+ }
+);
+
+const NavBreadcrumb = ({
+ onClick, entryName, entryUid, entryCategory, entryStatus, nPass,
+ nFailOrError, to,
+}) => (
+
+
+
+);
+
+export default withRouter(connector(NavBreadcrumb));
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumbWithNextRoute.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumbWithNextRoute.jsx
new file mode 100644
index 000000000..44582f9b7
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavBreadcrumbWithNextRoute.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Route } from 'react-router';
+import _ from 'lodash';
+import NavBreadcrumb from './NavBreadcrumb';
+import { joinURLComponent } from '../../../Common/utils';
+
+const NavBreadcrumbWithNextRoute = ({ entries, match }) => {
+ const routePath = _.isObject(match) ? match.url : null;
+ const tgtEntry = React.useMemo(() => {
+ if(_.isArray(entries) && _.isObject(match)) {
+ const decodedName = decodeURIComponent(match.params.id);
+ return entries.find(e => decodedName === e.name);
+ } else if(_.isObject(entries)) {
+ return entries;
+ }
+ return null;
+ }, [ entries, match ]);
+ const tgtEntryIsObj = _.isObject(tgtEntry);
+ const tgtEntryEntries = tgtEntryIsObj ? tgtEntry.entries : null;
+ return !(tgtEntryIsObj && _.isString(routePath)) ? null : (
+ <>
+
+ (
+
+ )}/>
+ >
+ );
+};
+
+export default NavBreadcrumbWithNextRoute;
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavPanes.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavPanes.jsx
new file mode 100644
index 000000000..46f56c9f9
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavPanes.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { css } from 'aphrodite';
+import { Route } from 'react-router';
+import { navBreadcrumbStyles } from '../styles';
+import {
+ mkGetReportIsFetching,
+ mkGetReportLastFetchError,
+ mkGetReportDocument,
+} from '../state/reportSelectors';
+import EmptyListGroupItem from './EmptyListGroupItem';
+import NavBreadcrumbWithNextRoute from './NavBreadcrumbWithNextRoute';
+import NavSidebarWithNextRoute from './NavSidebarWithNextRoute';
+import AutoSelectRedirect from './AutoSelectRedirect';
+
+const NAV_BREADCRUMB_CONTAINER_CLASSES = css(
+ navBreadcrumbStyles.navBreadcrumbs,
+ navBreadcrumbStyles.breadcrumbContainer
+);
+
+const connector = connect(
+ () => {
+ const getReportIsFetching = mkGetReportIsFetching();
+ const getReportLastFetchError = mkGetReportLastFetchError();
+ const getReportDocument = mkGetReportDocument();
+ return function mapStateToProps(state) {
+ return {
+ document: getReportDocument(state),
+ fetchError: getReportLastFetchError(state),
+ isFetching: getReportIsFetching(state),
+ };
+ };
+ },
+);
+
+const NavPanes = ({ document, fetchError, isFetching }) => {
+ const entries = React.useMemo(() => [ document ], [ document ]);
+ return (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.
+ */
+ }
+ (
+
+ )}/>
+ (
+
+ )}/>
+ >
+ );
+};
+
+export default connector(NavPanes);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebar.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebar.jsx
new file mode 100644
index 000000000..888075aa7
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebar.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { ListGroup } from 'reactstrap';
+import { css } from 'aphrodite';
+import { connect } from 'react-redux';
+import _ from 'lodash';
+import {
+ mkGetUIFilter,
+ mkGetUISidebarWidthFirstAvailable,
+} from '../state/uiSelectors';
+import { setSidebarWidth } from '../state/uiActions';
+import NavSidebarEntry from './NavSidebarEntry';
+import Column from '../../../Nav/Column';
+import * as filterStates from '../../../Common/filterStates';
+import { isNonemptyArray } from '../../../Common/utils';
+import { navListStyles } from '../styles';
+import EmptyListGroupItem from './EmptyListGroupItem';
+
+const BUTTON_LIST_CLASSES = css(navListStyles.buttonList);
+
+const connector = connect(
+ () => {
+ const getFilter = mkGetUIFilter();
+ const getFirstAvailableWidth = mkGetUISidebarWidthFirstAvailable();
+ return function mapStateToProps(state) {
+ return {
+ filter: getFilter(state),
+ width: getFirstAvailableWidth(state), // string of either em or px
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ const onDragUnrestricted = px => dispatch(setSidebarWidth(px));
+ return {
+ onDrag: _.debounce(onDragUnrestricted, 12, { maxWait: 17 }),
+ };
+ },
+);
+
+const NavSidebar = ({ entries, filter, location, match, width, onDrag }) => {
+
+ const entryFilter = React.useCallback(currEntry => {
+ if(!_.isObject(currEntry)) return false;
+ const { passed = 0, failed = 0, errored = 0 } = currEntry.counter || {};
+ return !(
+ (filter === filterStates.PASSED && passed === 0) ||
+ (filter === filterStates.FAILED && (failed + errored) === 0)
+ );
+ }, [ filter ]);
+
+ const links = React.useMemo(() => {
+ const _links = !isNonemptyArray(entries) ? [] :
+ entries.filter(entryFilter).map((entry, idx) => (
+
+ ));
+ return _links.length ? _links : ( );
+ }, [ entries, location, match, entryFilter ]);
+
+ return (
+
+
+ {links}
+
+
+ );
+};
+
+export default connector(NavSidebar);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarEntry.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarEntry.jsx
new file mode 100644
index 000000000..c68ce36c9
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarEntry.jsx
@@ -0,0 +1,139 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { css } from 'aphrodite';
+import { ListGroupItem } from 'reactstrap';
+import _ from 'lodash';
+import { mkGetUISelectedEntry, mkGetUIIsShowTags } from '../state/uiSelectors';
+import { setSelectedEntry } from '../state/uiActions';
+import {
+ navUtilsStyles,
+ UNDECORATED_LINK_STYLE,
+ ACTIVE_LINK_CLASSES,
+} from '../styles';
+import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults';
+import { joinURLComponent } from '../../../Common/utils';
+import TagList from '../../../Nav/TagList';
+import NavEntry from '../../../Nav/NavEntry';
+
+const SIDEBAR_LINK_CLASSES = css(
+ navUtilsStyles.navButton,
+ navUtilsStyles.navButtonInteract,
+);
+
+const connector = connect(
+ () => {
+ const getIsShowTags = mkGetUIIsShowTags();
+ const getSelectedEntry = mkGetUISelectedEntry();
+ return function mapStateToProps(state) {
+ return {
+ isShowTags: getIsShowTags(state),
+ selectedEntry: getSelectedEntry(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetSelectedEntry: entry => dispatch(setSelectedEntry(entry)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps, ownProps) {
+ const { isShowTags, selectedEntry } = stateProps;
+ const { boundSetSelectedEntry } = dispatchProps;
+ const {
+ idx,
+ entry,
+ location: currentLocation = {},
+ match: {
+ url: matchedUrl = '',
+ } = {},
+ entry: {
+ name: entryName = '',
+ tags: entryTags = {},
+ uid: entryUid = '',
+ category: entryCategory = '',
+ status: entryStatus = '',
+ counter: {
+ passed: nPass = 0,
+ failed: nFail = 0,
+ error: nError = 0,
+ } = {},
+ },
+ } = ownProps;
+ return {
+ entryName,
+ entryTags,
+ entryUid,
+ entryCategory,
+ entryStatus,
+ nPass,
+ isShowTags,
+ currentLocation,
+ nextPathname: joinURLComponent(matchedUrl, encodeURIComponent(entryName)),
+ nFailOrError: nFail + nError,
+ tabIndex: `${idx + 1}`,
+ isActive: () => (
+ _.isObject(selectedEntry) && selectedEntry.uid === entryUid
+ ),
+ onClick: () => {
+ if(selectedEntry !== entry && _.isObject(entry)) {
+ boundSetSelectedEntry(entry);
+ }
+ },
+ };
+ }
+);
+
+const NavSidebarEntry = ({
+ entryName, entryTags, entryUid, entryCategory, entryStatus, nPass, onClick,
+ isShowTags, nFailOrError, tabIndex, nextPathname, currentLocation, isActive
+}) => {
+
+ const nextLocation = React.useMemo(() => {
+ if(entryCategory === BOTTOMMOST_ENTRY_CATEGORY) {
+ return currentLocation;
+ } else {
+ return { ...currentLocation, pathname: nextPathname };
+ }
+ }, [ nextPathname, currentLocation, entryCategory ]);
+
+ const MaybeTagList = React.useCallback(props => {
+ if(!isShowTags || !entryTags) {
+ return null;
+ } else {
+ return (
+
+ );
+ }
+ }, [ entryTags, entryName, isShowTags ]);
+
+ const NavLinkAsTag = React.useCallback(props => {
+ return ( );
+ }, []);
+
+ return (
+
+
+
+
+ );
+};
+
+export default connector(NavSidebarEntry);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarWithNextRoute.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarWithNextRoute.jsx
new file mode 100644
index 000000000..eb7456a4b
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/NavSidebarWithNextRoute.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Route } from 'react-router';
+import _ from 'lodash';
+import { BOTTOMMOST_ENTRY_CATEGORY } from '../../../Common/defaults';
+import NavSidebar from './NavSidebar';
+
+const NavSidebarWithNextRoute = ({ entries, passedLast, match }) => {
+
+ const routePath = _.isObject(match) ? match.url : null;
+ const abortRender = !_.isObject(match) || passedLast;
+ const isLast = (
+ _.isObject(entries) && entries.category === BOTTOMMOST_ENTRY_CATEGORY
+ );
+
+ const tgtEntries = React.useMemo(() => {
+ let tgt = null;
+ if(!abortRender && _.isObject(entries)) {
+ tgt = entries;
+ if(_.isArray(entries) && _.isObject(match)) {
+ const decodedName = decodeURIComponent(match.params.id);
+ tgt = entries.find(e => decodedName === e.name);
+ }
+ }
+ return tgt !== null ? tgt.entries : null;
+ }, [ abortRender, entries, match ]);
+
+ return abortRender ? null : (
+ <>
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ >
+ );
+};
+
+export default NavSidebarWithNextRoute;
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/PrintButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/PrintButton.jsx
new file mode 100644
index 000000000..e1a9191f7
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/PrintButton.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { NavItem } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPrint } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { TOOLBAR_BUTTON_CLASSES, BUTTONS_BAR_CLASSES } from '../styles';
+
+library.add(faPrint);
+
+export default function PrintButton() {
+ return(
+
+
+
+ );
+};
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/TagsButton.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/TagsButton.jsx
new file mode 100644
index 000000000..b4864eab7
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/TagsButton.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { NavItem } from 'reactstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faTags } from '@fortawesome/free-solid-svg-icons';
+import { connect } from 'react-redux';
+import { TOOLBAR_BUTTON_CLASSES, BUTTONS_BAR_CLASSES } from '../styles';
+import { mkGetUIIsShowTags } from '../state/uiSelectors';
+import { setShowTags } from '../state/uiActions';
+
+library.add(faTags);
+
+const connector = connect(
+ () => {
+ const getIsShowTags = mkGetUIIsShowTags();
+ return function mapStateToProps(state) {
+ return {
+ isShowTags: getIsShowTags(state),
+ };
+ };
+ },
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundSetShowTags: isShow => dispatch(setShowTags(isShow)),
+ };
+ },
+ function mergeProps(stateProps, dispatchProps) {
+ const { isShowTags } = stateProps;
+ const { boundSetShowTags } = dispatchProps;
+ return {
+ onClick: evt => {
+ evt.stopPropagation();
+ boundSetShowTags(!isShowTags);
+ },
+ };
+ },
+);
+
+const TagsButton = ({ onClick }) => (
+
+
+
+);
+
+export default connector(TagsButton);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/components/Toolbar.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/Toolbar.jsx
new file mode 100644
index 000000000..1040f976a
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/Toolbar.jsx
@@ -0,0 +1,14 @@
+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/BatchReportBeta/components/TopNavbar.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/TopNavbar.jsx
new file mode 100644
index 000000000..d3492493d
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/components/TopNavbar.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Navbar, Nav, Collapse } from 'reactstrap';
+import { connect } from 'react-redux';
+import { mkGetUIToolbarStyle } from '../state/uiSelectors';
+import { TOOLBAR_CLASSES, FILTER_BOX_CLASSES } from '../styles';
+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 function mapStateToProps(state) {
+ return {
+ toolbarStyle: getToolbarStyle(state),
+ };
+ };
+ }
+);
+
+const TopNavbar = ({ toolbarStyle, children = null }) => (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+);
+
+export default connector(TopNavbar);
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/index.jsx b/testplan/web_ui/testing/src/Report/BatchReportBeta/index.jsx
new file mode 100644
index 000000000..571c4a30e
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/index.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { connect, Provider } from 'react-redux';
+import { Router } from 'react-router';
+import CenterPane from './components/CenterPane';
+import Toolbar from './components/Toolbar';
+import uiHistory from './state/uiHistory';
+import NavPanes from './components/NavPanes';
+import { BATCH_REPORT_CLASSES } from './styles';
+import { fetchReport } from './state/reportActions';
+import store from './state/store';
+
+const __DEV__ = process.env.NODE_ENV !== 'production';
+
+const connector = connect(
+ null,
+ function mapDispatchToProps(dispatch) {
+ return {
+ boundFetchReport: arg => dispatch(fetchReport(arg)),
+ };
+ },
+);
+
+const BatchReportInner = ({ children = null, ...props }) => {
+ const { boundFetchReport, uid, axios, testReport } = props;
+ React.useEffect(() => {
+ const arg = { axios };
+ if(testReport && __DEV__) arg.testReport = testReport;
+ else arg.uid = uid;
+ // this `.abort()` function will be called when this component unmounts,
+ // thus cancelling any outstanding requests
+ return boundFetchReport(arg).abort;
+ }, [ testReport, uid, boundFetchReport, axios ]);
+ return (
+
+
+
+
+ {children}
+
+ );
+};
+
+const ConnectedBatchReportInner = connector(BatchReportInner);
+
+export default function BatchReport({ match, axios, testReport, ...props }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/__tests__/fetchReport.test.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/__tests__/fetchReport.test.js
new file mode 100644
index 000000000..00952f105
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/__tests__/fetchReport.test.js
@@ -0,0 +1,37 @@
+/** @jest-environment jsdom */
+
+const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
+
+describe('REACT_APP_API_BASE_URL', () => {
+
+ beforeAll(() => {
+ window._origOrigin = window.origin;
+ window.origin = 'http://fake-origin:8901';
+ });
+
+ afterAll(() => {
+ window.origin = window._origOrigin;
+ delete window._origOrigin;
+ });
+
+ it('Is defined', () => {
+ expect(typeof API_BASE_URL).toEqual('string');
+ });
+
+ it('Is the correct format', () => {
+ let numErrs = 0;
+ try {
+ // first test if the variable is a full URI e.g. http://1.2.3.4:8080/api
+ new URL(API_BASE_URL);
+ } catch(err1) {
+ numErrs++;
+ // if it's not a full URI it could still be a path intended to be
+ // relative to the current origin e.g. just /api
+ try {
+ new URL(API_BASE_URL, window.location.origin);
+ } catch(err2) { numErrs++; }
+ }
+ expect(numErrs).toBeLessThan(2);
+ });
+
+});
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/fetchReport.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/fetchReport.js
new file mode 100644
index 000000000..0adc1e44f
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/fetchReport.js
@@ -0,0 +1,160 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import Axios from 'axios';
+import { headers as defaultHeaders } from 'axios/lib/defaults';
+import _ from 'lodash';
+import {
+ getReportUid,
+ getReportIsFetching,
+ getReportDocument,
+ getReportLastFetchError,
+ getReportIsFetchCancelled,
+} from '../reportSelectors';
+import { PropagateIndices } from '../../../reportUtils';
+import { toPlainObjectIn, flattened } from '../../../../Common/utils';
+
+const __DEV__ = process.env.NODE_ENV !== 'production';
+const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
+const UNSET_API_BASE_URL_VAL = 'OverrideMeOrThereWillBeABuildError';
+/** @type {URL} */
+const API_BASE_URL_OBJ = (() => {
+ // these conditionals + try-catches are top-level so errors will be caught
+ // during the build.
+ // eslint-disable-next-line max-len
+ if(!_.isString(API_BASE_URL) || API_BASE_URL === UNSET_API_BASE_URL_VAL) {
+ throw new Error(
+ "The environment variable REACT_APP_API_BASE_URL must be set to your " +
+ "API's base URL. See this project's .env file for more information."
+ );
+ }
+ let apiBaseUrlObj;
+ try {
+ // this will not error when API_BASE_URL is a full URI
+ apiBaseUrlObj = new URL(API_BASE_URL);
+ } catch(err1) {
+ try {
+ // this will not error when API_BASE_URL only a path
+ apiBaseUrlObj = new URL(API_BASE_URL, window.location.origin);
+ } catch(err2) {
+ throw new Error(
+ `The environment variable REACT_APP_API_BASE_URL is not set to a ` +
+ `valid URL or a URL path - received ` +
+ `REACT_APP_API_BASE_URL="${API_BASE_URL}".`
+ );
+ }
+ }
+ return apiBaseUrlObj;
+})();
+
+let TEST_REPORTS = {}; // eslint-disable-line no-unused-vars
+if(__DEV__) {
+ TEST_REPORTS = {
+ fakeReport: require('../../../../Common/fakeReport'),
+ sampleReports: require('../../../../Common/sampleReports'),
+ };
+}
+
+const axiosDefaultConfig = (() => {
+ const apiBaseURLOrigin = API_BASE_URL_OBJ.origin;
+ const apiBaseURL = API_BASE_URL_OBJ.href;
+ const headers = { ...defaultHeaders.common, Accept: 'application/json' };
+ if(__DEV__ && window.location.origin !== apiBaseURLOrigin) {
+ headers['Access-Control-Allow-Origin'] = apiBaseURLOrigin;
+ }
+ return { baseURL: apiBaseURL, headers, timeout: 60_000 };
+})();
+
+const fetchFakeReport = async ({ testReport }, { dispatch, requestId }) => {
+ const { setReportUID } = await import('./');
+ dispatch(setReportUID(testReport));
+ const data = await _.get(TEST_REPORTS, testReport, requestId);
+ if(data === requestId) throw new Error(flattened`
+ Invalid object path "${testReport}", valid paths are
+ `);
+ return { data, headers: { 'content-length': JSON.stringify(data).length } };
+};
+
+const fetchRemoteReport = async ({ axios: mockAxios, uid }, thunkAPI) => {
+ const { dispatch, signal } = thunkAPI;
+ const { setReportUID } = await import('./');
+ // setup fetch
+ dispatch(setReportUID(uid));
+ let axiosInstance, cancelFunc, cancelToken, abortListenerID = -1;
+ if(_.isObject(mockAxios)) {
+ axiosInstance = mockAxios;
+ if(_.isObject(axiosInstance.defaults.cancelToken)) {
+ cancelToken = axiosInstance.defaults.cancelToken.token;
+ }
+ } else {
+ const cancelSource = Axios.CancelToken.source();
+ cancelFunc = cancelSource.cancel;
+ cancelToken = cancelSource.token;
+ axiosInstance = Axios.create({ ...axiosDefaultConfig, cancelToken });
+ }
+ if(_.isFunction(cancelFunc)) signal.addEventListener('abort', () => {
+ cancelFunc('The fetch was cancelled.');
+ });
+ if(_.isObject(cancelToken)) abortListenerID = setInterval(() => {
+ cancelToken.throwIfRequested();
+ }, 10);
+ // execute fetch, see https://github.com/axios/axios#response-schema
+ return await axiosInstance.get(`/${uid}`).finally(() => {
+ if(abortListenerID > 0) clearInterval(abortListenerID);
+ });
+};
+
+/** This function keeps duplicate fetches from occurring */
+const checkShouldAcceptFetchRequest = ({ uid }, { getState }) => {
+ if(uid !== getReportUid(getState())) return true;
+ if(getReportIsFetching(getState())) return false;
+ if(_.isObject(getReportDocument(getState()))) return (
+ !_.isNil(getReportLastFetchError(getState())) ||
+ getReportIsFetchCancelled(getState())
+ );
+};
+
+/**
+ * During testing / development it's possible to pass in the path to one
+ * of the promises in the following object and return it instead of actually
+ * doing a fetch
+ * @example
+ fetchReport({ testReport: 'fakeReport.TESTPLAN_REPORT' })
+
+ * @param {object} arg
+ * @param {string} arg.uid
+ * @param {object=} arg.axios
+ * @param {object=} arg.testReport
+ * @returns {{ abort(): void }}
+ */
+const fetchReport = createAsyncThunk(
+ 'report/fetchReport',
+ async (arg, thunkAPI) => {
+ const { dispatch, rejectWithValue, requestId } = thunkAPI;
+ try {
+ if(!_.isPlainObject(arg))
+ throw new class extends Error { name = 'FetchReportArgError'; }(
+ __DEV__ ? '`fetchReport` takes a single plain object argument'
+ : 'Contact support'
+ );
+ const { setLastResponseContentLength } = await import('./');
+ const { headers, data } = await (
+ __DEV__ && _.isString(arg.testReport)
+ ? fetchFakeReport(arg, { dispatch, requestId })
+ : fetchRemoteReport(arg, thunkAPI)
+ );
+ dispatch(setLastResponseContentLength(headers['content-length']));
+ return PropagateIndices(data);
+ } catch(err) {
+ return rejectWithValue(
+ Axios.isCancel(err)
+ ? _.toPlainObject(err)
+ : toPlainObjectIn(err)
+ );
+ }
+ },
+ {
+ condition: checkShouldAcceptFetchRequest,
+ dispatchConditionRejection: false,
+ }
+);
+
+export default fetchReport;
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/index.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/index.js
new file mode 100644
index 000000000..febe008b1
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportActions/index.js
@@ -0,0 +1,8 @@
+import reportSlice from '../reportSlice';
+
+export const {
+ setLastResponseContentLength,
+ setReportUID,
+} = reportSlice.actions;
+
+export { default as fetchReport } from './fetchReport';
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSelectors.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSelectors.js
new file mode 100644
index 000000000..7ddef248c
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSelectors.js
@@ -0,0 +1,17 @@
+/* eslint-disable max-len */
+import { createSelector } from '@reduxjs/toolkit';
+import _ from 'lodash';
+
+export const createReportSelector = (...funcs) => _.spread(createSelector)([ state => state.report, ...funcs ]);
+export const mkGetLastResponseContentLength = () => createReportSelector(({ lastResponseContentLength }) => lastResponseContentLength);
+export const mkGetReportUid = () => createReportSelector(({ uid }) => uid);
+export const mkGetReportDocument = () => createReportSelector(({ document }) => document);
+export const mkGetReportIsFetching = () => createReportSelector(({ isFetching }) => isFetching);
+export const mkGetReportIsFetchCancelled = () => createReportSelector(({ isFetchCancelled }) => isFetchCancelled);
+export const mkGetReportLastFetchError = () => createReportSelector(({ fetchError }) => fetchError);
+export const getLastResponseContentLength = mkGetLastResponseContentLength();
+export const getReportUid = mkGetReportUid();
+export const getReportDocument = mkGetReportDocument();
+export const getReportIsFetching = mkGetReportIsFetching();
+export const getReportIsFetchCancelled = mkGetReportIsFetchCancelled();
+export const getReportLastFetchError = mkGetReportLastFetchError();
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSlice.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSlice.js
new file mode 100644
index 000000000..f12694178
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/reportSlice.js
@@ -0,0 +1,57 @@
+import _ from 'lodash';
+import { createSlice } from '@reduxjs/toolkit';
+import fetchReport from './reportActions/fetchReport';
+import Axios from 'axios';
+
+export default createSlice({
+ name: 'report',
+ initialState: {
+ uid: null,
+ document: null,
+ isFetching: false,
+ isFetchCancelled: false,
+ fetchError: null,
+ lastResponseContentLength: null,
+ },
+ reducers: {
+ setReportUID: {
+ reducer(state, { payload }) { state.uid = payload; },
+ prepare: (uid = null) => ({ payload: uid }),
+ },
+ setLastResponseContentLength: {
+ reducer(state, { payload }) {
+ state.lastResponseContentLength = payload;
+ },
+ prepare: (contentLength = 0) => ({
+ payload: _.isString(contentLength)
+ ? parseInt(contentLength)
+ : contentLength,
+ }),
+ },
+ },
+ extraReducers: {
+ [fetchReport.pending.type](state) {
+ state.isFetching = true;
+ state.isFetchCancelled = false;
+ state.fetchError = null;
+ },
+ [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;
+ state.isFetchCancelled = Axios.isCancel(rejectValue);
+ state.fetchError = rejectValue;
+ } else {
+ state.isFetchCancelled = false;
+ state.fetchError = action.error;
+ }
+ },
+ },
+});
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/store.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/store.js
new file mode 100644
index 000000000..6785b757e
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/store.js
@@ -0,0 +1,124 @@
+import {
+ configureStore,
+ combineReducers,
+ getDefaultMiddleware,
+} from '@reduxjs/toolkit';
+import { setUseProxies } from 'immer';
+import { fetchReport } from './reportActions';
+import { getLastResponseContentLength } from './reportSelectors';
+import { setSelectedEntry } from './uiActions';
+import { flattened } from '../../../Common/utils';
+
+const __DEV__ = process.env.NODE_ENV !== 'production';
+
+// At @reduxjs/toolkit/dist/redux-toolkit.esm.js:1422 (@reduxjs/toolkit@1.3.4)
+// ES5 compatibility is enabled, which switches off Proxy support, which is bad:
+// https://immerjs.github.io/immer/docs/performance#:~:text=The%20ES5%20fallback%20implementation
+setUseProxies(true);
+
+const createReducer = () => combineReducers({
+ // eslint-disable-next-line max-len
+ report: require('./reportSlice').default.reducer,
+ ui: require('./uiSlice').default.reducer,
+});
+
+let devTools = false;
+const middlewareOptions = {
+ serializableCheck: false,
+ immutableCheck: false,
+};
+
+if(__DEV__) {
+ const FIX_DEVTOOL_SPEED = process.env.REACT_APP_FIX_DEVTOOL_SPEED === 'true';
+ // eslint-disable-next-line max-len
+ const DISABLE_REDUX_DEVTOOLS = process.env.REACT_APP_DISABLE_REDUX_DEVTOOLS === 'true';
+ let shaveObjectEntriesFromObject = null;
+ if(__DEV__ && FIX_DEVTOOL_SPEED && !DISABLE_REDUX_DEVTOOLS) {
+ console.warn(flattened`
+ You've set the environment variable REACT_APP_FIX_DEVTOOL_SPEED=true
+ which will disable several features in Redux Devtools.
+ `);
+ const _ = require('lodash');
+ shaveObjectEntriesFromObject = (entry, placeholder = '<>') => (
+ !_.isObjectLike(entry) ? entry : Object.fromEntries(
+ Object.entries(entry).map(([ prop, val ]) => [
+ prop, _.isObjectLike(val)
+ ? _.isFunction(placeholder) ? placeholder(prop, val) : placeholder
+ : val
+ ])
+ ));
+ }
+
+ devTools = !DISABLE_REDUX_DEVTOOLS && (!FIX_DEVTOOL_SPEED ? true : {
+ name: 'testplan',
+ maxAge: 10,
+ trace: true,
+ traceLimit: 3,
+ shouldCatchErrors: true,
+ shouldRecordChanges: true,
+ shouldHotReload: true,
+ actionSanitizer: action => {
+ switch(action.type) {
+ case fetchReport.fulfilled.type:
+ return { ...action, payload: '<>' };
+ case setSelectedEntry.type:
+ return { ...action, payload: shaveObjectEntriesFromObject
+ ? shaveObjectEntriesFromObject(action.payload)
+ : action.payload
+ };
+ default:
+ return action;
+ }
+ },
+ stateSanitizer: state => {
+ // devtools crashes and the UI freezes at 45_864_416 so this number can
+ // likely be tuned up or down (right now it's just a guess)
+ if(35_000_000 < getLastResponseContentLength(state)) {
+ // these slow down the extension - and hence the UI - a lot
+ window.__CURRENT_REPORT = state.report.document;
+ window.__SELECTED_ENTRY = state.ui.selectedEntry;
+ return {
+ ...state,
+ report: {
+ ...state.report,
+ document: '<>',
+ },
+ ui: {
+ ...state.ui,
+ selectedEntry: shaveObjectEntriesFromObject
+ ? shaveObjectEntriesFromObject(
+ state.ui.selectedEntry,
+ key => `<>`
+ )
+ : state.ui.selectedEntry,
+ },
+ };
+ }
+ return state;
+ },
+ });
+
+ middlewareOptions.serializableCheck = !FIX_DEVTOOL_SPEED ? true : ({
+ ignoredPaths: [ 'report.document', 'ui.selectedEntry' ],
+ ignoredActions: [
+ fetchReport.fulfilled.type,
+ setSelectedEntry.type,
+ ],
+ });
+
+ middlewareOptions.immutableCheck = !FIX_DEVTOOL_SPEED ? true : ({
+ ignoredPaths: [ 'report.document', 'ui.selectedEntry' ],
+ });
+}
+
+const store = configureStore({
+ reducer: createReducer(),
+ middleware: getDefaultMiddleware(middlewareOptions),
+ devTools,
+});
+
+if(__DEV__ && module && module.hot) {
+ module.hot.accept(() => store.replaceReducer(createReducer()));
+}
+
+export default store;
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiActions.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiActions.js
new file mode 100644
index 000000000..848d7a0b5
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiActions.js
@@ -0,0 +1,11 @@
+import uiSlice from './uiSlice';
+
+export const {
+ setShowTags,
+ setShowInfoModal,
+ setFilter,
+ setDisplayEmpty,
+ setShowHelpModal,
+ setSelectedEntry,
+ setSidebarWidth,
+} = uiSlice.actions;
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiHistory.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiHistory.js
new file mode 100644
index 000000000..d012603a7
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiHistory.js
@@ -0,0 +1,3 @@
+import { createHashHistory } from 'history';
+
+export default createHashHistory({ basename: '/' });
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSelectors.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSelectors.js
new file mode 100644
index 000000000..e6003f1ea
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSelectors.js
@@ -0,0 +1,46 @@
+/* eslint-disable max-len */
+import { createSelector } from '@reduxjs/toolkit';
+import { css } from 'aphrodite';
+import _ from 'lodash';
+import { mkGetReportDocument } from './reportSelectors';
+import navStyles from '../../../Toolbar/navStyles';
+import { STATUS_CATEGORY } from '../../../Common/defaults';
+import { COLUMN_WIDTH } from '../../../Common/defaults';
+
+const STATUS_CATEGORY_STYLE_MAP = {
+ passed: navStyles.toolbarPassed,
+ failed: navStyles.toolbarFailed,
+ error: navStyles.toolbarFailed,
+ unstable: navStyles.toolbarUnstable,
+};
+
+export const createUISelector = (...funcs) => _.spread(createSelector)([ state => state.ui, ...funcs ]);
+export const mkGetUIIsShowHelpModal = () => createUISelector(({ isShowHelpModal }) => isShowHelpModal);
+export const mkGetUIIsDisplayEmpty = () => createUISelector(({ isDisplayEmpty }) => isDisplayEmpty);
+export const mkGetUIFilter = () => createUISelector(({ filter }) => filter);
+export const mkGetUIIsShowTags = () => createUISelector(({ isShowTags }) => isShowTags);
+export const mkGetUIIsShowInfoModal = () => createUISelector(({ isShowInfoModal }) => isShowInfoModal);
+export const mkGetUISelectedEntry = () => createUISelector(({ selectedEntry }) => selectedEntry);
+
+export const mkGetUIToolbarStyle = () => createSelector(
+ mkGetReportDocument(),
+ document => {
+ let statusStyle = navStyles.toolbarUnknown;
+ if(_.isObject(document) && document.status) {
+ const statcat = STATUS_CATEGORY[document.status];
+ if(statcat in STATUS_CATEGORY_STYLE_MAP) {
+ statusStyle = STATUS_CATEGORY_STYLE_MAP[statcat];
+ }
+ }
+ return css(navStyles.toolbar, statusStyle);
+ },
+);
+
+
+export const mkGetUISidebarWidthFirstAvailable = () => createUISelector(
+ uiState => (
+ uiState.sidebarWidthPx || uiState.sidebarWidthEm || `${COLUMN_WIDTH}em`
+ ),
+);
+
+export const getUISelectedEntry = mkGetUISelectedEntry();
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSlice.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSlice.js
new file mode 100644
index 000000000..da4aedf3b
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/state/uiSlice.js
@@ -0,0 +1,58 @@
+import { createSlice } from '@reduxjs/toolkit/dist';
+import * as filterStates from '../../../Common/filterStates';
+import { fetchReport } from './reportActions';
+import { COLUMN_WIDTH } from '../../../Common/defaults';
+
+const FILTER_STATES_ARR = Object.values(filterStates);
+
+/** This state slice contains information specific to how the UI should look */
+export default createSlice({
+ name: 'ui',
+ initialState: {
+ isShowHelpModal: false,
+ isDisplayEmpty: true,
+ filter: filterStates.ALL,
+ isShowTags: false,
+ isShowInfoModal: false,
+ selectedEntry: null,
+ sidebarWidthPx: null,
+ sidebarWidthEm: `${COLUMN_WIDTH}em`,
+ },
+ reducers: {
+ setSelectedEntry: {
+ reducer(state, action) { state.selectedEntry = action.payload; },
+ prepare: (entry = null) => ({ payload: entry }),
+ },
+ setSidebarWidth: {
+ reducer(state, { payload }) { state.sidebarWidthPx = payload; },
+ prepare: (widthPxStr) => ({ payload: widthPxStr }),
+ },
+ setShowTags: {
+ reducer(state, { payload }) { state.isShowTags = payload; },
+ prepare: (showTags = false) => ({ payload: !!showTags }),
+ },
+ setShowInfoModal: {
+ reducer(state, { payload }) { state.isShowInfoModal = payload; },
+ prepare: (showInfoModal = false) => ({ payload: !!showInfoModal }),
+ },
+ setFilter: {
+ reducer(state, { payload }) { state.filter = payload; },
+ prepare: (filter = filterStates.ALL) => ({
+ payload: FILTER_STATES_ARR.includes(filter) ? filter : filterStates.ALL
+ }),
+ },
+ setDisplayEmpty: {
+ reducer(state, { payload }) { state.isDisplayEmpty = payload; },
+ prepare: (displayEmpty = true) => ({ payload: !!displayEmpty }),
+ },
+ setShowHelpModal: {
+ reducer(state, { payload }) { state.isShowHelpModal = payload; },
+ prepare: (showHelpModal = false) => ({ payload: !!showHelpModal }),
+ },
+ },
+ extraReducers: {
+ [fetchReport.fulfilled.type](state, action) {
+ state.selectedEntry = action.payload;
+ },
+ },
+});
diff --git a/testplan/web_ui/testing/src/Report/BatchReportBeta/styles.js b/testplan/web_ui/testing/src/Report/BatchReportBeta/styles.js
new file mode 100644
index 000000000..de16d4cab
--- /dev/null
+++ b/testplan/web_ui/testing/src/Report/BatchReportBeta/styles.js
@@ -0,0 +1,36 @@
+import { StyleSheet, css } from 'aphrodite';
+import navStyles from '../../Toolbar/navStyles';
+import { styles as navUtilsStyles } from '../../Nav/navUtils';
+
+export { default as CommonStyles } from '../../Common/Styles';
+export { styles as navBreadcrumbStyles } from '../../Nav/NavBreadcrumbs';
+export { navUtilsStyles };
+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'
+ }
+});
+
+export const BATCH_REPORT_CLASSES = css(batchReportStyles.batchReport);
+export const TOOLBAR_CLASSES = css(navStyles.toolbar);
+export const TOOLBAR_BUTTON_CLASSES = css(navStyles.toolbarButton);
+export const BUTTONS_BAR_CLASSES = css(navStyles.buttonsBar);
+export const FILTER_BOX_CLASSES = css(navStyles.filterBox);
+export const FILTER_DROPDOWN_CLASSES = css(navStyles.filterDropdown);
+export const FILTER_LABEL_CLASSES = css(navStyles.filterLabel);
+export const DROPDOWN_ITEM_CLASSES = css(navStyles.dropdownItem);
+export const ACTIVE_LINK_CLASSES = css(navUtilsStyles.navButtonInteractFocus);
+export const UNDECORATED_LINK_STYLE = {
+ textDecoration: 'none',
+ color: 'currentColor',
+};
diff --git a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap
index 9e3087ca4..2436aab08 100644
--- a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap
+++ b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/BatchReport.test.js.snap
@@ -47,12 +47,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -100,12 +100,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing2",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing2|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -130,13 +130,13 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -185,12 +185,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -217,12 +217,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "BetaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -249,14 +249,14 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
"BetaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -310,12 +310,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -332,12 +332,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "GammaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -354,12 +354,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Secondary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Secondary|multitest",
"Sample Testplan|testplan",
"GammaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -376,7 +376,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
@@ -385,7 +385,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
"BetaSuite|testsuite",
"Secondary|multitest",
"GammaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -451,12 +451,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -504,12 +504,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing2",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing2|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -534,13 +534,13 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -589,12 +589,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -621,12 +621,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "BetaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -653,14 +653,14 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
"BetaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -714,12 +714,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -736,12 +736,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "GammaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -758,12 +758,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Secondary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Secondary|multitest",
"Sample Testplan|testplan",
"GammaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -780,7 +780,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
@@ -789,7 +789,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
"BetaSuite|testsuite",
"Secondary|multitest",
"GammaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -843,12 +843,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -896,12 +896,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing2",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing2|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -926,13 +926,13 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -981,12 +981,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1013,12 +1013,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "BetaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"BetaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1045,14 +1045,14 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
"test_equality_passing2|testcase",
"BetaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1106,12 +1106,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -1128,12 +1128,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "GammaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"GammaSuite|testsuite",
"Secondary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -1150,12 +1150,12 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Secondary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Secondary|multitest",
"Sample Testplan|testplan",
"GammaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {},
@@ -1172,7 +1172,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
@@ -1181,7 +1181,7 @@ exports[`BatchReport loads a more complex report and autoselects entries 1`] = `
"BetaSuite|testsuite",
"Secondary|multitest",
"GammaSuite|testsuite",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -1258,12 +1258,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1294,12 +1294,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1327,12 +1327,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1360,12 +1360,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -1430,12 +1430,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1466,12 +1466,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1499,12 +1499,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1532,12 +1532,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -1590,12 +1590,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1626,12 +1626,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1659,12 +1659,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1692,12 +1692,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"meta": Object {},
"name": "Sample Testplan",
- "name_type_index": Set {
+ "name_type_index": Array [
"Sample Testplan|testplan",
"Primary|multitest",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags_index": Object {
@@ -1744,12 +1744,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1780,12 +1780,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1813,12 +1813,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "Primary",
- "name_type_index": Set {
+ "name_type_index": Array [
"Primary|multitest",
"Sample Testplan|testplan",
"AlphaSuite|testsuite",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
@@ -1867,12 +1867,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "test_equality_passing",
- "name_type_index": Set {
+ "name_type_index": Array [
"test_equality_passing|testcase",
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
- },
+ ],
"status": "passed",
"status_override": null,
"tags": Object {
@@ -1903,12 +1903,12 @@ exports[`BatchReport loads a simple report and autoselects entries 1`] = `
],
"logs": Array [],
"name": "AlphaSuite",
- "name_type_index": Set {
+ "name_type_index": Array [
"AlphaSuite|testsuite",
"Primary|multitest",
"Sample Testplan|testplan",
"test_equality_passing|testcase",
- },
+ ],
"status": "failed",
"status_override": null,
"tags": Object {
diff --git a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap
index 33e9f6e09..6c967dbeb 100644
--- a/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap
+++ b/testplan/web_ui/testing/src/Report/__tests__/__snapshots__/InteractiveReport.test.js.snap
@@ -646,12 +646,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"hash": 44444,
"logs": Array [],
"name": "testcaseName",
- "name_type_index": Set {
+ "name_type_index": Array [
"testcaseName|testcase",
"SuiteName|testsuite",
"MultitestName|multitest",
"TestplanName|testplan",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
"MultitestUID",
@@ -674,12 +674,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"fix_spec_path": null,
"hash": 33333,
"name": "SuiteName",
- "name_type_index": Set {
+ "name_type_index": Array [
"SuiteName|testsuite",
"MultitestName|multitest",
"TestplanName|testplan",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
"MultitestUID",
@@ -700,12 +700,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"fix_spec_path": null,
"hash": 22222,
"name": "MultitestName",
- "name_type_index": Set {
+ "name_type_index": Array [
"MultitestName|multitest",
"TestplanName|testplan",
"SuiteName|testsuite",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
],
@@ -725,12 +725,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"hash": 11111,
"meta": Object {},
"name": "TestplanName",
- "name_type_index": Set {
+ "name_type_index": Array [
"TestplanName|testplan",
"MultitestName|multitest",
"SuiteName|testsuite",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [],
"runtime_status": "running",
"status": "unknown",
@@ -761,12 +761,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"hash": 44444,
"logs": Array [],
"name": "testcaseName",
- "name_type_index": Set {
+ "name_type_index": Array [
"testcaseName|testcase",
"SuiteName|testsuite",
"MultitestName|multitest",
"TestplanName|testplan",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
"MultitestUID",
@@ -789,12 +789,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"fix_spec_path": null,
"hash": 33333,
"name": "SuiteName",
- "name_type_index": Set {
+ "name_type_index": Array [
"SuiteName|testsuite",
"MultitestName|multitest",
"TestplanName|testplan",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
"MultitestUID",
@@ -815,12 +815,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"fix_spec_path": null,
"hash": 22222,
"name": "MultitestName",
- "name_type_index": Set {
+ "name_type_index": Array [
"MultitestName|multitest",
"TestplanName|testplan",
"SuiteName|testsuite",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [
"TestplanUID",
],
@@ -840,12 +840,12 @@ exports[`InteractiveReport Updates testcase state 1`] = `
"hash": 11111,
"meta": Object {},
"name": "TestplanName",
- "name_type_index": Set {
+ "name_type_index": Array [
"TestplanName|testplan",
"MultitestName|multitest",
"SuiteName|testsuite",
"testcaseName|testcase",
- },
+ ],
"parent_uids": Array [],
"runtime_status": "running",
"status": "unknown",
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..ced909733 100644
--- a/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js
+++ b/testplan/web_ui/testing/src/Report/__tests__/reportUtils.test.js
@@ -70,7 +70,7 @@ describe('Report/reportUtils', () => {
[
[
'testplan',
- new Set([
+ [
'Sample Testplan|testplan',
'Primary|multitest',
'AlphaSuite|testsuite',
@@ -79,37 +79,37 @@ describe('Report/reportUtils', () => {
'BetaSuite|testsuite',
'Secondary|multitest',
'GammaSuite|testsuite',
- ]),
+ ],
],
[
'multitest',
- new Set([
+ [
'Primary|multitest',
'Sample Testplan|testplan',
'AlphaSuite|testsuite',
'test_equality_passing|testcase',
'test_equality_passing2|testcase',
'BetaSuite|testsuite',
- ]),
+ ],
],
[
'testsuite',
- new Set([
+ [
'AlphaSuite|testsuite',
'Primary|multitest',
'Sample Testplan|testplan',
'test_equality_passing|testcase',
'test_equality_passing2|testcase',
- ]),
+ ],
],
[
'testcase',
- new Set([
+ [
'test_equality_passing|testcase',
'AlphaSuite|testsuite',
'Primary|multitest',
'Sample Testplan|testplan',
- ]),
+ ],
],
].forEach(([entryType, nameTypeIndex]) => {
it(`${entryType} name_type_index - stores ancestors & ` +
diff --git a/testplan/web_ui/testing/src/Report/reportUtils.js b/testplan/web_ui/testing/src/Report/reportUtils.js
index 3299a49cf..f59e08e11 100644
--- a/testplan/web_ui/testing/src/Report/reportUtils.js
+++ b/testplan/web_ui/testing/src/Report/reportUtils.js
@@ -3,7 +3,7 @@
*/
import React from "react";
import format from 'date-fns/format';
-
+import _ from 'lodash';
import AssertionPane from '../AssertionPane/AssertionPane';
import Message from '../Common/Message';
@@ -30,7 +30,7 @@ function _mergeTags(tagsA, tagsB) {
const tags = tagsB[tagName];
if (tagsA.hasOwnProperty(tagName)) {
let tagsArray = tags.concat(tagsA[tagName]);
- let tagsSet = new Set(tagsArray);
+ let tagsSet = _.uniq(tagsArray);
mergedTags[tagName] = [...tagsSet];
} else {
mergedTags[tagName] = tags;
@@ -59,12 +59,12 @@ const propagateIndicesRecur = (entries, parentIndices) => {
if (parentIndices === undefined) {
parentIndices = {
tags_index: {},
- name_type_index: new Set(),
+ name_type_index: [],
};
}
let indices = {
tags_index: {},
- name_type_index: new Set(),
+ name_type_index: [],
counter: {
passed: 0,
failed: 0,
@@ -76,7 +76,7 @@ const propagateIndicesRecur = (entries, parentIndices) => {
// Initialize indices.
let tagsIndex = {};
const entryNameType = entry.name + '|' + entryType;
- let nameTypeIndex = new Set([
+ let nameTypeIndex = _.uniq([
entryNameType,
...parentIndices.name_type_index
]);
@@ -94,7 +94,7 @@ const propagateIndicesRecur = (entries, parentIndices) => {
{ tags_index: tags, name_type_index: nameTypeIndex }
);
tagsIndex = _mergeTags(tagsIndex, descendantsIndices.tags_index);
- nameTypeIndex = new Set([
+ nameTypeIndex = _.uniq([
...nameTypeIndex,
...descendantsIndices.name_type_index
]);
@@ -107,7 +107,7 @@ const propagateIndicesRecur = (entries, parentIndices) => {
// Update Array of entries indices.
indices.tags_index = _mergeTags(indices.tags_index, tagsIndex);
- indices.name_type_index = new Set([
+ indices.name_type_index = _.uniq([
...indices.name_type_index,
...nameTypeIndex
]);
diff --git a/testplan/web_ui/testing/src/index.js b/testplan/web_ui/testing/src/index.js
index 1fc7c4ae3..6f95c867c 100644
--- a/testplan/web_ui/testing/src/index.js
+++ b/testplan/web_ui/testing/src/index.js
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import BatchReport from './Report/BatchReport';
+import BatchReportBeta from './Report/BatchReportBeta';
import InteractiveReport from './Report/InteractiveReport';
import EmptyReport from './Report/EmptyReport';
import {POLL_MS} from './Common/defaults.js';
@@ -17,6 +18,7 @@ import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const AppRouter = () => (
+
diff --git a/testplan/web_ui/testing/src/setupTests.js b/testplan/web_ui/testing/src/setupTests.js
index 7a1fee7e8..23d923788 100644
--- a/testplan/web_ui/testing/src/setupTests.js
+++ b/testplan/web_ui/testing/src/setupTests.js
@@ -1,4 +1,5 @@
+import 'react-app-polyfill/stable';
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() });