diff --git a/babel.config.js b/babel.config.js index 71eee90f2..533c5445f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,8 @@ module.exports = { presets: [ 'react-app' + ], + plugins: [ + '@babel/plugin-proposal-class-properties' ] } diff --git a/ccdb5_ui/config/settings.py b/ccdb5_ui/config/settings.py index a4fb18b24..453287d58 100644 --- a/ccdb5_ui/config/settings.py +++ b/ccdb5_ui/config/settings.py @@ -37,7 +37,8 @@ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.staticfiles', - 'ccdb5_ui' + 'ccdb5_ui', + 'flags', ) MIDDLEWARE_CLASSES = ( diff --git a/ccdb5_ui/config/urls.py b/ccdb5_ui/config/urls.py index a6e0861e2..bf1a01f3f 100644 --- a/ccdb5_ui/config/urls.py +++ b/ccdb5_ui/config/urls.py @@ -3,7 +3,7 @@ try: from django.urls import re_path -except ImportError: +except ImportError: # pragma: no cover from django.conf.urls import url as re_path diff --git a/ccdb5_ui/tests/test_views.py b/ccdb5_ui/tests/test_views.py index 2256d1bff..b57361370 100644 --- a/ccdb5_ui/tests/test_views.py +++ b/ccdb5_ui/tests/test_views.py @@ -1,6 +1,7 @@ -from django.test import RequestFactory, SimpleTestCase +from unittest.mock import patch from ccdb5_ui.views import CCDB5MainView +from django.test import RequestFactory, SimpleTestCase class CCDB5MainViewTest(SimpleTestCase): @@ -8,17 +9,29 @@ def setUp(self): self.factory = RequestFactory() self.view = CCDB5MainView.as_view() - def test_get_supported_user_agent(self): + @patch('ccdb5_ui.views.flag_enabled') + def test_get_supported_user_agent(self, mock_flag_enabled): + mock_flag_enabled.return_value = True + request = self.factory.get('/', HTTP_USER_AGENT="Foo") response = self.view(request) self.assertContains(response, "Search the Consumer Complaint Database") + mock_flag_enabled.assert_called_once_with('CCDB5_TRENDS') + + @patch('ccdb5_ui.views.flag_enabled') + def test_get_unsupported_user_agent(self, mock_flag_enabled): + mock_flag_enabled.return_value = True - def test_get_unsupported_user_agent(self): request = self.factory.get('/', HTTP_USER_AGENT="MSIE 8.0;") response = self.view(request) self.assertContains(response, "A more up-to-date browser is required") + mock_flag_enabled.assert_called_once_with('CCDB5_TRENDS') + + @patch('ccdb5_ui.views.flag_enabled') + def test_get_no_user_agent(self, mock_flag_enabled): + mock_flag_enabled.return_value = True - def test_get_no_user_agent(self): request = self.factory.get('/') response = self.view(request) self.assertContains(response, "Search the Consumer Complaint Database") + mock_flag_enabled.assert_called_once_with('CCDB5_TRENDS') diff --git a/ccdb5_ui/views.py b/ccdb5_ui/views.py index a49b61940..b49890c19 100644 --- a/ccdb5_ui/views.py +++ b/ccdb5_ui/views.py @@ -1,9 +1,10 @@ from django.views.generic.base import TemplateView from django.conf import settings +from flags.state import flag_enabled try: STANDALONE = settings.STANDALONE -except: # pragma: no cover +except Exception: # pragma: no cover STANDALONE = False @@ -19,6 +20,7 @@ 'MSIE 7.0;', ] + class CCDB5MainView(TemplateView): template_name = 'ccdb5_ui/ccdb-main.html' base_template = BASE_TEMPLATE @@ -41,3 +43,12 @@ def get_context_data(self, **kwargs): context['ccdb5_base_template'] = self.base_template context['unsupported_browser'] = unsupported return context + + def render_to_response(self, context, **response_kwargs): + response = super(CCDB5MainView, self).render_to_response( + context, **response_kwargs) + + show_trends = flag_enabled('CCDB5_TRENDS') + + response.set_cookie('showTrends', 'show' if show_trends else 'hide') + return response diff --git a/package-lock.json b/package-lock.json index 9af4193d4..cdd5be6f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,26 +29,192 @@ } }, "@babel/core": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.5.tgz", - "integrity": "sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helpers": "^7.5.5", - "@babel/parser": "^7.5.5", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5", - "convert-source-map": "^1.1.0", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.2.tgz", + "integrity": "sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.2", + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helpers": "^7.10.1", + "@babel/parser": "^7.10.2", + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.2", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "json5": "^2.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "requires": { + "@babel/highlight": "^7.10.1" + } + }, + "@babel/generator": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", + "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "requires": { + "@babel/types": "^7.10.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", + "integrity": "sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", + "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "requires": { + "@babel/helper-module-imports": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", + "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", + "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", + "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "requires": { + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", + "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helpers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", + "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", + "requires": { + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==" + }, + "@babel/template": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", + "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/traverse": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz", + "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", + "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -281,6 +447,11 @@ "@babel/types": "^7.8.3" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", + "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==" + }, "@babel/helper-wrap-function": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", @@ -328,12 +499,155 @@ } }, "@babel/plugin-proposal-class-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", - "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz", + "integrity": "sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-class-features-plugin": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "requires": { + "@babel/highlight": "^7.10.1" + } + }, + "@babel/generator": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", + "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "requires": { + "@babel/types": "^7.10.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz", + "integrity": "sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ==", + "requires": { + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1" + } + }, + "@babel/helper-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", + "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==" + }, + "@babel/helper-replace-supers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", + "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", + "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==" + }, + "@babel/template": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", + "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/traverse": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz", + "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", + "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-proposal-decorators": { @@ -409,6 +723,158 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, + "@babel/plugin-proposal-private-methods": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz", + "integrity": "sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "requires": { + "@babel/highlight": "^7.10.1" + } + }, + "@babel/generator": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", + "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "requires": { + "@babel/types": "^7.10.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz", + "integrity": "sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ==", + "requires": { + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1" + } + }, + "@babel/helper-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", + "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==" + }, + "@babel/helper-replace-supers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", + "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", + "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==" + }, + "@babel/template": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", + "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/traverse": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz", + "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", + "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.8.8", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz", @@ -434,6 +900,21 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-class-properties": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz", + "integrity": "sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==" + } + } + }, "@babel/plugin-syntax-decorators": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz", @@ -879,6 +1360,21 @@ "@babel/plugin-syntax-typescript": "^7.8.3" } }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz", + "integrity": "sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==" + } + } + }, "@babel/plugin-transform-unicode-regex": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", @@ -889,69 +1385,793 @@ } }, "@babel/preset-env": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.7.tgz", - "integrity": "sha512-BYftCVOdAYJk5ASsznKAUl53EMhfBbr8CJ1X+AJLfGPscQkwJFiaV/Wn9DPH/7fzm2v6iRYJKYHSqyynTGw0nw==", - "requires": { - "@babel/compat-data": "^7.8.6", - "@babel/helper-compilation-targets": "^7.8.7", - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-proposal-async-generator-functions": "^7.8.3", - "@babel/plugin-proposal-dynamic-import": "^7.8.3", - "@babel/plugin-proposal-json-strings": "^7.8.3", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.8.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.8.3", - "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.2.tgz", + "integrity": "sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA==", + "requires": { + "@babel/compat-data": "^7.10.1", + "@babel/helper-compilation-targets": "^7.10.2", + "@babel/helper-module-imports": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-proposal-async-generator-functions": "^7.10.1", + "@babel/plugin-proposal-class-properties": "^7.10.1", + "@babel/plugin-proposal-dynamic-import": "^7.10.1", + "@babel/plugin-proposal-json-strings": "^7.10.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", + "@babel/plugin-proposal-numeric-separator": "^7.10.1", + "@babel/plugin-proposal-object-rest-spread": "^7.10.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.1", + "@babel/plugin-proposal-optional-chaining": "^7.10.1", + "@babel/plugin-proposal-private-methods": "^7.10.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.1", "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.10.1", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-json-strings": "^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.1", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.8.3", - "@babel/plugin-transform-async-to-generator": "^7.8.3", - "@babel/plugin-transform-block-scoped-functions": "^7.8.3", - "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.8.6", - "@babel/plugin-transform-computed-properties": "^7.8.3", - "@babel/plugin-transform-destructuring": "^7.8.3", - "@babel/plugin-transform-dotall-regex": "^7.8.3", - "@babel/plugin-transform-duplicate-keys": "^7.8.3", - "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.8.6", - "@babel/plugin-transform-function-name": "^7.8.3", - "@babel/plugin-transform-literals": "^7.8.3", - "@babel/plugin-transform-member-expression-literals": "^7.8.3", - "@babel/plugin-transform-modules-amd": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.8.3", - "@babel/plugin-transform-modules-systemjs": "^7.8.3", - "@babel/plugin-transform-modules-umd": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.10.1", + "@babel/plugin-transform-arrow-functions": "^7.10.1", + "@babel/plugin-transform-async-to-generator": "^7.10.1", + "@babel/plugin-transform-block-scoped-functions": "^7.10.1", + "@babel/plugin-transform-block-scoping": "^7.10.1", + "@babel/plugin-transform-classes": "^7.10.1", + "@babel/plugin-transform-computed-properties": "^7.10.1", + "@babel/plugin-transform-destructuring": "^7.10.1", + "@babel/plugin-transform-dotall-regex": "^7.10.1", + "@babel/plugin-transform-duplicate-keys": "^7.10.1", + "@babel/plugin-transform-exponentiation-operator": "^7.10.1", + "@babel/plugin-transform-for-of": "^7.10.1", + "@babel/plugin-transform-function-name": "^7.10.1", + "@babel/plugin-transform-literals": "^7.10.1", + "@babel/plugin-transform-member-expression-literals": "^7.10.1", + "@babel/plugin-transform-modules-amd": "^7.10.1", + "@babel/plugin-transform-modules-commonjs": "^7.10.1", + "@babel/plugin-transform-modules-systemjs": "^7.10.1", + "@babel/plugin-transform-modules-umd": "^7.10.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", - "@babel/plugin-transform-new-target": "^7.8.3", - "@babel/plugin-transform-object-super": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.8.7", - "@babel/plugin-transform-property-literals": "^7.8.3", - "@babel/plugin-transform-regenerator": "^7.8.7", - "@babel/plugin-transform-reserved-words": "^7.8.3", - "@babel/plugin-transform-shorthand-properties": "^7.8.3", - "@babel/plugin-transform-spread": "^7.8.3", - "@babel/plugin-transform-sticky-regex": "^7.8.3", - "@babel/plugin-transform-template-literals": "^7.8.3", - "@babel/plugin-transform-typeof-symbol": "^7.8.4", - "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/types": "^7.8.7", - "browserslist": "^4.8.5", + "@babel/plugin-transform-new-target": "^7.10.1", + "@babel/plugin-transform-object-super": "^7.10.1", + "@babel/plugin-transform-parameters": "^7.10.1", + "@babel/plugin-transform-property-literals": "^7.10.1", + "@babel/plugin-transform-regenerator": "^7.10.1", + "@babel/plugin-transform-reserved-words": "^7.10.1", + "@babel/plugin-transform-shorthand-properties": "^7.10.1", + "@babel/plugin-transform-spread": "^7.10.1", + "@babel/plugin-transform-sticky-regex": "^7.10.1", + "@babel/plugin-transform-template-literals": "^7.10.1", + "@babel/plugin-transform-typeof-symbol": "^7.10.1", + "@babel/plugin-transform-unicode-escapes": "^7.10.1", + "@babel/plugin-transform-unicode-regex": "^7.10.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.10.2", + "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "requires": { + "@babel/highlight": "^7.10.1" + } + }, + "@babel/compat-data": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.1.tgz", + "integrity": "sha512-CHvCj7So7iCkGKPRFUfryXIkU2gSBw7VSZFYLsqVhrS47269VK2Hfi9S/YcublPMW8k1u2bQBlbDruoQEm4fgw==", + "requires": { + "browserslist": "^4.12.0", + "invariant": "^2.2.4", + "semver": "^5.5.0" + } + }, + "@babel/generator": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", + "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "requires": { + "@babel/types": "^7.10.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", + "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.1.tgz", + "integrity": "sha512-cQpVq48EkYxUU0xozpGCLla3wlkdRRqLWu1ksFMXA9CM5KQmyyRpSEsYXbao7JUkOw/tAaYKCaYyZq6HOFYtyw==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz", + "integrity": "sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA==", + "requires": { + "@babel/compat-data": "^7.10.1", + "browserslist": "^4.12.0", + "invariant": "^2.2.4", + "levenary": "^1.1.1", + "semver": "^5.5.0" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz", + "integrity": "sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.1", + "@babel/helper-regex": "^7.10.1", + "regexpu-core": "^4.7.0" + } + }, + "@babel/helper-define-map": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.1.tgz", + "integrity": "sha512-+5odWpX+OnvkD0Zmq7panrMuAGQBu6aPUgvMzuMGo4R+jUOvealEj2hiqI6WhxgKrTpFoFj0+VdsuA8KDxHBDg==", + "requires": { + "@babel/helper-function-name": "^7.10.1", + "@babel/types": "^7.10.1", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.1.tgz", + "integrity": "sha512-vcUJ3cDjLjvkKzt6rHrl767FeE7pMEYfPanq5L16GRtrXIoznc0HykNW2aEYkcnP76P0isoqJ34dDMFZwzEpJg==", + "requires": { + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.1.tgz", + "integrity": "sha512-vLm5srkU8rI6X3+aQ1rQJyfjvCBLXP8cAGeuw04zeAM2ItKb1e7pmVmLyHb4sDaAYnLL13RHOZPLEtcGZ5xvjg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", + "integrity": "sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", + "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "requires": { + "@babel/helper-module-imports": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", + "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==" + }, + "@babel/helper-regex": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.1.tgz", + "integrity": "sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g==", + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.1.tgz", + "integrity": "sha512-RfX1P8HqsfgmJ6CwaXGKMAqbYdlleqglvVtht0HGPMSsy2V6MqLlOJVF/0Qyb/m2ZCi2z3q3+s6Pv7R/dQuZ6A==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.1", + "@babel/helper-wrap-function": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", + "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", + "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "requires": { + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", + "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-wrap-function": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz", + "integrity": "sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ==", + "requires": { + "@babel/helper-function-name": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/traverse": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==" + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.1.tgz", + "integrity": "sha512-vzZE12ZTdB336POZjmpblWfNNRpMSua45EYnRigE2XsZxcXcIyly2ixnTJasJE4Zq3U7t2d8rRF7XRUuzHxbOw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-remap-async-to-generator": "^7.10.1", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz", + "integrity": "sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz", + "integrity": "sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz", + "integrity": "sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz", + "integrity": "sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-numeric-separator": "^7.10.1" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.1.tgz", + "integrity": "sha512-Z+Qri55KiQkHh7Fc4BW6o+QBuTagbOp9txE+4U1i79u9oWlf2npkiDx+Rf3iK3lbcHBuNy9UOkwuR5wOMH3LIQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.10.1" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz", + "integrity": "sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.1.tgz", + "integrity": "sha512-dqQj475q8+/avvok72CF3AOSV/SGEcH29zT5hhohqqvvZ2+boQoOr7iGldBG5YXTO2qgCgc2B3WvVLUdbeMlGA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz", + "integrity": "sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz", + "integrity": "sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz", + "integrity": "sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz", + "integrity": "sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz", + "integrity": "sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg==", + "requires": { + "@babel/helper-module-imports": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-remap-async-to-generator": "^7.10.1" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz", + "integrity": "sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz", + "integrity": "sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.1.tgz", + "integrity": "sha512-P9V0YIh+ln/B3RStPoXpEQ/CoAxQIhRSUn7aXqQ+FZJ2u8+oCtjIXR3+X0vsSD8zv+mb56K7wZW1XiDTDGiDRQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.1", + "@babel/helper-define-map": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-optimise-call-expression": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.1.tgz", + "integrity": "sha512-mqSrGjp3IefMsXIenBfGcPXxJxweQe2hEIwMQvjtiDQ9b1IBvDUjkAtV/HMXX47/vXf14qDNedXsIiNd1FmkaQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz", + "integrity": "sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz", + "integrity": "sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz", + "integrity": "sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz", + "integrity": "sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz", + "integrity": "sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz", + "integrity": "sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw==", + "requires": { + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz", + "integrity": "sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz", + "integrity": "sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz", + "integrity": "sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw==", + "requires": { + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz", + "integrity": "sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg==", + "requires": { + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-simple-access": "^7.10.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.1.tgz", + "integrity": "sha512-ewNKcj1TQZDL3YnO85qh9zo1YF1CHgmSTlRQgHqe63oTrMI85cthKtZjAiZSsSNjPQ5NCaYo5QkbYqEw1ZBgZA==", + "requires": { + "@babel/helper-hoist-variables": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz", + "integrity": "sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA==", + "requires": { + "@babel/helper-module-transforms": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz", + "integrity": "sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz", + "integrity": "sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-replace-supers": "^7.10.1" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz", + "integrity": "sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz", + "integrity": "sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.1.tgz", + "integrity": "sha512-B3+Y2prScgJ2Bh/2l9LJxKbb8C8kRfsG4AdPT+n7ixBHIxJaIG8bi8tgjxUMege1+WqSJ+7gu1YeoMVO3gPWzw==", + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz", + "integrity": "sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz", + "integrity": "sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz", + "integrity": "sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz", + "integrity": "sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-regex": "^7.10.1" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.1.tgz", + "integrity": "sha512-t7B/3MQf5M1T9hPCRG28DNGZUuxAuDqLYS03rJrIk2prj/UV7Z6FOneijhQhnv/Xa039vidXeVbvjK2SK5f7Gg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz", + "integrity": "sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz", + "integrity": "sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.1" + } + }, + "@babel/template": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", + "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/traverse": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz", + "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==", + "requires": { + "@babel/code-frame": "^7.10.1", + "@babel/generator": "^7.10.1", + "@babel/helper-function-name": "^7.10.1", + "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/parser": "^7.10.1", + "@babel/types": "^7.10.1", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", + "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "requires": { + "object.assign": "^4.1.0" + } + }, + "browserslist": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", + "integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==", + "requires": { + "caniuse-lite": "^1.0.30001043", + "electron-to-chromium": "^1.3.413", + "node-releases": "^1.1.53", + "pkg-up": "^2.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001084", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001084.tgz", + "integrity": "sha512-ftdc5oGmhEbLUuMZ/Qp3mOpzfZLCxPYKcvGv6v2dJJ+8EdqcvZRbAGOiLmkM/PV1QGta/uwBs8/nCl6sokDW6w==" + }, + "electron-to-chromium": { + "version": "1.3.474", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.474.tgz", + "integrity": "sha512-fPkSgT9IBKmVJz02XioNsIpg0WYmkPrvU1lUJblMMJALxyE7/32NGvbJQKKxpNokozPvqfqkuUqVClYsvetcLw==" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "node-releases": { + "version": "1.1.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", + "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "requires": { + "find-up": "^2.1.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -959,6 +2179,18 @@ } } }, + "@babel/preset-modules": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", + "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, "@babel/preset-react": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.8.3.tgz", @@ -2767,6 +3999,15 @@ "source-map": "^0.5.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/preset-env": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.4.tgz", @@ -3057,8 +4298,8 @@ } }, "britecharts": { - "version": "git+https://github.com/cfpb/britecharts.git#e62b0159ff6a506cc254c6146c22833d8f67f310", - "from": "git+https://github.com/cfpb/britecharts.git#2.3.7", + "version": "git+https://github.com/cfpb/britecharts.git#ea4fad5b1fbc4200e03ff3f03293dd24638cfe38", + "from": "git+https://github.com/cfpb/britecharts.git#2.3.8", "requires": { "base-64": "^0.1.0", "canvg-browser": "^1.0.0", @@ -8366,9 +9607,9 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-api": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", - "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.7.tgz", + "integrity": "sha512-LYTOa2UrYFyJ/aSczZi/6lBykVMjCCvUmT64gOe+jPZFy4w6FYfPGqFT2IiQ2BxVHHDOvCD7qrIXb0EOh4uGWw==", "requires": { "async": "^2.6.2", "compare-versions": "^3.4.0", @@ -8378,7 +9619,7 @@ "istanbul-lib-instrument": "^3.3.0", "istanbul-lib-report": "^2.0.8", "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", + "istanbul-reports": "^2.2.5", "js-yaml": "^3.13.1", "make-dir": "^2.1.0", "minimatch": "^3.0.4", diff --git a/package.json b/package.json index 42d2f91f2..ce5f3e363 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ }, "license": "CC0-1.0", "dependencies": { - "@babel/core": "7.5.5", - "@babel/preset-env": "^7.8.7", + "@babel/core": "^7.9.0", + "@babel/plugin-proposal-class-properties": "^7.10.1", + "@babel/preset-env": "^7.9.5", "@babel/preset-react": "^7.8.3", "@svgr/webpack": "4.3.2", "@typescript-eslint/eslint-plugin": "1.13.0", @@ -26,7 +27,7 @@ "babel-loader": "8.0.6", "babel-plugin-named-asset-import": "^0.3.6", "babel-runtime": "^6.20.0", - "britecharts": "git+https://github.com/cfpb/britecharts.git#2.3.7", + "britecharts": "git+https://github.com/cfpb/britecharts.git#2.3.8", "camelcase": "^5.2.0", "case-sensitive-paths-webpack-plugin": "2.2.0", "cf-buttons": "^4.3.0", diff --git a/src/__tests__/__snapshots__/App.spec.jsx.snap b/src/__tests__/__snapshots__/App.spec.jsx.snap index e02f5df31..b127acdc1 100644 --- a/src/__tests__/__snapshots__/App.spec.jsx.snap +++ b/src/__tests__/__snapshots__/App.spec.jsx.snap @@ -370,7 +370,7 @@ exports[`initial state renders without crashing 1`] = ` onChange={[Function]} placeholder="mm/dd/yyyy" size="15" - value="12/31/2015" + value="1/2/2016" /> @@ -1310,28 +1311,6 @@ exports[`initial state renders without crashing 1`] = ` -
-

- Product - by highest complaint volume -

-
-
-
-

- Issue - by highest complaint volume -

-
-
diff --git a/src/__tests__/utils.spec.jsx b/src/__tests__/utils.spec.jsx index 5f6d97b6a..6169a2628 100644 --- a/src/__tests__/utils.spec.jsx +++ b/src/__tests__/utils.spec.jsx @@ -1,7 +1,7 @@ import { ariaReadoutNumbers, calculateDateRange, clamp, coalesce, debounce, - getFullUrl, hasFiltersEnabled, hashCode, shortIsoFormat, sortSelThenCount, - startOfToday + formatPercentage, getFullUrl, hasFiltersEnabled, hashCode, shortIsoFormat, + sortSelThenCount, startOfToday, parseCookies } from '../utils' import { DATE_RANGE_MIN } from '../constants' import React from 'react' @@ -138,6 +138,17 @@ describe('module::utils', () => { } ) }) + describe('formatPercentage', ()=>{ + it( 'handles regular values' , () => { + let actual = formatPercentage( 0.5 ) + expect(actual).toEqual(50.00) + } ); + it('handles NaN values', ()=>{ + let actual = formatPercentage( NaN ) + expect(actual).toEqual(0.0) + }) + }) + describe( 'getFullUrl', () => { it( 'adds a host if needed' , () => { const actual = getFullUrl( '/foo/bar#baz?qaz=a&b=c' ) @@ -243,5 +254,29 @@ describe('module::utils', () => { expect( actual.getMinutes() ).toEqual( 0 ) } ); } ); + + describe( 'parseCookies', () => { + it( 'handles an empty string' , () => { + const actual = parseCookies( '' ) + expect( actual ).toEqual( {} ) + } ); + + it( 'handles undefined' , () => { + const actual = parseCookies( undefined ) + expect( actual ).toEqual( {} ) + } ); + + it( 'creates a dictionary from a cookie string' , () => { + const actual = parseCookies( + '_ga=fooo; _gid=baaar; csrftoken=baz; showTrends=hide;' + ) + expect( actual ).toEqual( { + _ga: 'fooo', + _gid: 'baaar', + csrftoken: 'baz', + showTrends: 'hide' + } ) + } ); + } ); }) diff --git a/src/actions/__tests__/complaints.spec.jsx b/src/actions/__tests__/complaints.spec.jsx index 0a976a4c1..b84f0c93c 100644 --- a/src/actions/__tests__/complaints.spec.jsx +++ b/src/actions/__tests__/complaints.spec.jsx @@ -5,7 +5,55 @@ import * as sut from '../complaints' const middlewares = [thunk] const mockStore = configureMockStore(middlewares) +function setupStore(tab){ + return mockStore( { + map: { + }, + query: { + tab + }, + trends: { + activeCall: '', + }, + results: { + activeCall: '' + } + } ) +} + describe('action::complaints', () => { + describe('sendHitsQuery', () => { + it( 'calls the Complaints API', () => { + const store = setupStore( 'List' ) + store.dispatch( sut.sendHitsQuery() ) + const expectedActions = [ + { type: sut.COMPLAINTS_API_CALLED, url: expect.any(String) } + ] + + expect(store.getActions()).toEqual(expectedActions) + } ) + + it( 'calls the Map API', () => { + const store = setupStore( 'Map' ) + store.dispatch( sut.sendHitsQuery() ) + const expectedActions = [ + { type: sut.STATES_API_CALLED, url: expect.any(String) } + ] + + expect(store.getActions()).toEqual(expectedActions) + } ) + + it( 'calls the Trends API', () => { + const store = setupStore( 'Trends' ) + store.dispatch( sut.sendHitsQuery() ) + const expectedActions = [ + { type: sut.TRENDS_API_CALLED, url: expect.any(String) } + ] + + expect(store.getActions()).toEqual(expectedActions) + } ) + } ) + describe('getAggregations', () => { let onSuccess, onFail, store @@ -289,4 +337,100 @@ describe('action::complaints', () => { }) }) }) + + describe('getTrends', () => { + let onSuccess, onFail, store + + function setupStore( company, lens ) { + const mockState = { + query: { + company, + date_received_min: new Date(2013, 1, 3), + from: 0, + has_narrative: true, + queryString: '?foo', + searchText: '', + size: 10, + }, + trends: { + activeCall: '', + lens + } + } + return mockStore(mockState) + } + + beforeEach(() => { + global.fetch = jest.fn().mockImplementation((url) => { + expect(url).toContain( + '@@APItrends/?foo&no_aggs=true' + ) + + return { + then: (x) => { + x({ json: () => ({})}) + return { + then: (x) => { + onSuccess = (data) => x(data) + return { + catch: (y) => {onFail = y} + } + } + } + } + } + }) + + }) + + it('calls the API', () => { + store = setupStore() + store.dispatch(sut.getTrends()) + expect(global.fetch).toHaveBeenCalled() + }) + + it('discards invalid API calls', () => { + store = setupStore( [], 'Company' ) + const s = store.getState() + store = mockStore(s) + + store.dispatch(sut.getTrends()) + expect(global.fetch).not.toHaveBeenCalled() + }) + + it('discards duplicate API calls', () => { + store = setupStore() + const s = store.getState() + s.trends.activeCall = '@@APItrends/' + s.query.queryString + '&no_aggs=true' + store = mockStore(s) + + store.dispatch(sut.getTrends()) + expect(global.fetch).not.toHaveBeenCalled() + }) + + describe('when the API call is finished', () => { + it('sends a simple action when data is received', () => { + store = setupStore() + store.dispatch(sut.getTrends()) + const expectedActions = [ + { type: sut.TRENDS_API_CALLED, url: expect.any(String) }, + { type: sut.TRENDS_RECEIVED, data: ['123']} + ] + onSuccess(['123']) + expect(store.getActions()).toEqual(expectedActions) + }) + + it('sends a different simple action when an error occurs', () => { + store = setupStore() + store.dispatch(sut.getTrends()) + const expectedActions = [ + { type: sut.TRENDS_API_CALLED, url: expect.any(String) }, + { type: sut.TRENDS_FAILED, error: 'oops' } + ] + onFail('oops') + expect(store.getActions()).toEqual(expectedActions) + }) + }) + }) + }) diff --git a/src/actions/__tests__/filter.spec.jsx b/src/actions/__tests__/filter.spec.jsx index a558e4142..642302fd6 100644 --- a/src/actions/__tests__/filter.spec.jsx +++ b/src/actions/__tests__/filter.spec.jsx @@ -19,17 +19,6 @@ describe('action:filterActions', () => { }) }) - describe('changeDataLens', () => { - it('creates a simple action', () => { - const expectedAction = { - type: sut.DATA_LENS_CHANGED, - dataLens: 'bar', - requery: REQUERY_ALWAYS - } - expect(sut.changeDataLens('bar')).toEqual( expectedAction ) - }) - }) - describe('changeDateInterval', () => { it('creates a simple action', () => { const expectedAction = { diff --git a/src/actions/__tests__/trends.spec.jsx b/src/actions/__tests__/trends.spec.jsx new file mode 100644 index 000000000..76bb8302e --- /dev/null +++ b/src/actions/__tests__/trends.spec.jsx @@ -0,0 +1,104 @@ +import { REQUERY_ALWAYS, REQUERY_NEVER } from '../../constants' +import * as sut from '../trends' +import { TREND_COLLAPSED, TREND_EXPANDED } from '../trends' + +describe( 'action:trendsActions', () => { + describe( 'changeChartType', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.CHART_TYPE_CHANGED, + chartType: 'bar', + requery: REQUERY_NEVER + } + expect( sut.changeChartType( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'changeDataLens', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.DATA_LENS_CHANGED, + lens: 'bar', + requery: REQUERY_ALWAYS + } + expect( sut.changeDataLens( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'changeDataSubLens', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.DATA_SUBLENS_CHANGED, + subLens: 'bar', + requery: REQUERY_ALWAYS + } + expect( sut.changeDataSubLens( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'changeDepth', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.DEPTH_CHANGED, + depth: 1000, + requery: REQUERY_ALWAYS + } + expect( sut.changeDepth( 1000 ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'resetDepth', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.DEPTH_RESET, + requery: REQUERY_ALWAYS + } + expect( sut.resetDepth() ).toEqual( expectedAction ) + } ) + } ) + + describe( 'changeFocus', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.FOCUS_CHANGED, + focus: 'bar', + requery: REQUERY_ALWAYS + } + expect( sut.changeFocus( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'updateTooltip', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.TRENDS_TOOLTIP_CHANGED, + value: 'bar', + requery: REQUERY_NEVER + } + expect( sut.updateTrendsTooltip( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'collapseTrend', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.TREND_COLLAPSED, + value: 'bar', + requery: REQUERY_NEVER + } + expect( sut.collapseTrend( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + + describe( 'expandTrend', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.TREND_EXPANDED, + value: 'bar', + requery: REQUERY_NEVER + } + expect( sut.expandTrend( 'bar' ) ).toEqual( expectedAction ) + } ) + } ) + +} ) diff --git a/src/actions/__tests__/view.spec.jsx b/src/actions/__tests__/view.spec.jsx index 4e9d987df..2d9a9416b 100644 --- a/src/actions/__tests__/view.spec.jsx +++ b/src/actions/__tests__/view.spec.jsx @@ -12,13 +12,33 @@ describe( 'action:view', () => { } ) } ) - describe( 'printModeChanged', () => { + describe( 'mapWarningDismissed', () => { it( 'creates a simple action', () => { const expectedAction = { - type: sut.PRINT_MODE_CHANGED, + type: sut.MAP_WARNING_DISMISSED, requery: REQUERY_NEVER } - expect( sut.printModeChanged() ).toEqual( expectedAction ) + expect( sut.mapWarningDismissed() ).toEqual( expectedAction ) + } ) + } ) + + describe( 'printModeOn', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.PRINT_MODE_ON, + requery: REQUERY_NEVER + } + expect( sut.printModeOn() ).toEqual( expectedAction ) + } ) + } ) + + describe( 'printModeOff', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.PRINT_MODE_OFF, + requery: REQUERY_NEVER + } + expect( sut.printModeOff() ).toEqual( expectedAction ) } ) } ) @@ -43,4 +63,15 @@ describe( 'action:view', () => { expect( sut.tabChanged( 'Foo' ) ).toEqual( expectedAction ) } ) } ) + + describe( 'trendsDateWarningDismissed', () => { + it( 'creates a simple action', () => { + const expectedAction = { + type: sut.TRENDS_DATE_WARNING_DISMISSED, + requery: REQUERY_NEVER + } + expect( sut.trendsDateWarningDismissed() ).toEqual( expectedAction ) + } ) + } ) + } ) diff --git a/src/actions/complaints.jsx b/src/actions/complaints.jsx index a0b1d257e..b523f6917 100644 --- a/src/actions/complaints.jsx +++ b/src/actions/complaints.jsx @@ -1,3 +1,4 @@ +/* eslint complexity: ["error", 5] */ import { MODE_LIST, MODE_MAP, MODE_TRENDS } from '../constants' export const AGGREGATIONS_API_CALLED = 'AGGREGATIONS_API_CALLED' @@ -11,6 +12,9 @@ export const COMPLAINT_DETAIL_FAILED = 'COMPLAINT_DETAIL_FAILED' export const STATES_API_CALLED = 'STATES_API_CALLED' export const STATES_RECEIVED = 'STATES_RECEIVED' export const STATES_FAILED = 'STATES_FAILED' +export const TRENDS_API_CALLED = 'TRENDS_API_CALLED' +export const TRENDS_RECEIVED = 'TRENDS_RECEIVED' +export const TRENDS_FAILED = 'TRENDS_FAILED' // ---------------------------------------------------------------------------- // Routing action @@ -54,9 +58,9 @@ export function sendHitsQuery() { case MODE_MAP: dispatch( getStates() ) break - // case 'Trends': - // dispatch( getTrends() ) - // break + case MODE_TRENDS: + dispatch( getTrends() ) + break case MODE_LIST: dispatch( getComplaints() ) break @@ -158,6 +162,38 @@ export function getStates() { } } +/** + * Calls the trends endpoint of the API + * + * @returns {Promise} a chain of promises that will update the Redux store + */ +export function getTrends() { + return ( dispatch, getState ) => { + const store = getState() + const { query, trends } = store + const qs = 'trends/' + query.queryString + const uri = '@@API' + qs + '&no_aggs=true' + + // This call is already in process + if ( uri === trends.activeCall ) { + return null + } + + // kill query if Company param criteria aren't met + if ( trends.lens === 'Company' && + ( !query.company || !query.company.length ) ) { + return null + } + + + dispatch( callingApi( TRENDS_API_CALLED, uri ) ) + return fetch( uri ) + .then( result => result.json() ) + .then( items => dispatch( trendsReceived( items ) ) ) + .catch( error => dispatch( trendsFailed( error ) ) ) + } +} + /** * Notifies the application that an API call is happening * @@ -275,3 +311,29 @@ export function statesFailed( error ) { error } } + +/** + * Creates an action in response to trends results being received from the API + * + * @param {string} data the raw data returned from the API + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function trendsReceived( data ) { + return { + type: TRENDS_RECEIVED, + data + } +} + +/** + * Creates an action in response after trends results fails + * + * @param {string} error the error returned from `fetch`, not the API + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function trendsFailed( error ) { + return { + type: TRENDS_FAILED, + error + } +} diff --git a/src/actions/filter.jsx b/src/actions/filter.jsx index 2ee8c306d..f35a17dbe 100644 --- a/src/actions/filter.jsx +++ b/src/actions/filter.jsx @@ -1,6 +1,5 @@ import { REQUERY_ALWAYS } from '../constants' -export const DATA_LENS_CHANGED = 'DATA_LENS_CHANGED' export const DATE_INTERVAL_CHANGED = 'DATE_INTERVAL_CHANGED' export const DATE_RANGE_CHANGED = 'DATE_RANGE_CHANGED' export const DATES_CHANGED = 'DATES_CHANGED' @@ -14,19 +13,6 @@ export const FILTER_REMOVED = 'FILTER_REMOVED' // ---------------------------------------------------------------------------- // Simple actions -/** - * Notifies the application that data lens overview, product, issue was toggled - * - * @param {string} dataLens which lens was selected - * @returns {string} a packaged payload to be used by Redux reducers - */ -export function changeDataLens( dataLens ) { - return { - type: DATA_LENS_CHANGED, - dataLens, - requery: REQUERY_ALWAYS - } -} /** * Notifies that date interval (day, week, month, quarter, yr) was changed diff --git a/src/actions/index.jsx b/src/actions/index.jsx index a892c97e3..e501b5163 100644 --- a/src/actions/index.jsx +++ b/src/actions/index.jsx @@ -4,6 +4,7 @@ import * as filter from './filter' import * as map from './map' import * as paging from './paging' import * as search from './search' +import * as trends from './trends' import * as url from './url' import * as view from './view' @@ -20,6 +21,7 @@ function combineActions() { ...map, ...paging, ...search, + ...trends, ...url, ...view } diff --git a/src/actions/trends.jsx b/src/actions/trends.jsx new file mode 100644 index 000000000..144ea5b40 --- /dev/null +++ b/src/actions/trends.jsx @@ -0,0 +1,153 @@ +import { REQUERY_ALWAYS, REQUERY_NEVER } from '../constants' + +export const CHART_TYPE_CHANGED = 'CHART_TYPE_CHANGED' +export const DATA_LENS_CHANGED = 'DATA_LENS_CHANGED' +export const DATA_SUBLENS_CHANGED = 'DATA_SUBLENS_CHANGED' +export const DEPTH_CHANGED = 'DEPTH_CHANGED' +export const DEPTH_RESET = 'DEPTH_RESET' +export const FOCUS_CHANGED = 'FOCUS_CHANGED' +export const FOCUS_REMOVED = 'FOCUS_REMOVED' +export const TREND_COLLAPSED = 'TREND_COLLAPSED' +export const TREND_EXPANDED = 'TREND_EXPANDED' +export const TRENDS_TOOLTIP_CHANGED = 'TRENDS_TOOLTIP_CHANGED' + +/** + * Notifies the application that chart type toggled + * + * @param {string} chartType which chartType was selected, line or stacked area + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function changeChartType( chartType ) { + return { + type: CHART_TYPE_CHANGED, + chartType, + requery: REQUERY_NEVER + } +} + +/** + * Notifies the application that data lens overview, product, issue was toggled + * + * @param {string} lens which lens was selected + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function changeDataLens( lens ) { + return { + type: DATA_LENS_CHANGED, + lens, + requery: REQUERY_ALWAYS + } +} + +/** + * Indicates the data subLens selected + * + * @param {string} subLens the tab selected for row charts + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function changeDataSubLens( subLens ) { + return { + type: DATA_SUBLENS_CHANGED, + requery: REQUERY_ALWAYS, + subLens + } +} + +/** + * Notifies the application that depth is being changed + * + * @param {string} depth the max number of aggregations returned + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function changeDepth( depth ) { + return { + type: DEPTH_CHANGED, + requery: REQUERY_ALWAYS, + depth + } +} + +/** + * Notifies the application that depth is being reset + * + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function resetDepth() { + return { + type: DEPTH_RESET, + requery: REQUERY_ALWAYS + } +} + +/** + * Notifies the application that focus is being changed + * + * @param {string} focus the text to search for + * @param {string} lens the lens we're focusing on + * @param {array} filterValues the parent/child focus sub-aggs to apply + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function changeFocus( focus, lens, filterValues ) { + return { + type: FOCUS_CHANGED, + requery: REQUERY_ALWAYS, + filterValues, + focus, + lens + } +} + +/** + * Notifies the application that focus is being removed + * + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function removeFocus() { + return { + type: FOCUS_REMOVED, + requery: REQUERY_ALWAYS + } +} + +/** + * Notifies the application that the toolTip for stacked area chart has changed + * + * @param {string} value the new payload from the tooltip + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function updateTrendsTooltip( value ) { + return { + type: TRENDS_TOOLTIP_CHANGED, + value, + requery: REQUERY_NEVER + } +} + +/** + * Indicates a bar in row chart has been collapsed + * + * @param {string} value of trend agg that was toggled + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function collapseTrend( value ) { + return { + type: TREND_COLLAPSED, + requery: REQUERY_NEVER, + value + } +} + +/** + * Indicates a bar in row chart has been expanded + * + * @param {string} value of trend agg that was toggled + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function expandTrend( value ) { + return { + type: TREND_EXPANDED, + requery: REQUERY_NEVER, + value + } +} + diff --git a/src/actions/view.jsx b/src/actions/view.jsx index d6e09efeb..a1f18cd6f 100644 --- a/src/actions/view.jsx +++ b/src/actions/view.jsx @@ -1,13 +1,14 @@ import { REQUERY_HITS_ONLY, REQUERY_NEVER } from '../constants' export const MAP_WARNING_DISMISSED = 'MAP_WARNING_DISMISSED' -export const PRINT_MODE_CHANGED = 'PRINT_MODE_CHANGED' export const PRINT_MODE_ON = 'PRINT_MODE_ON' export const PRINT_MODE_OFF = 'PRINT_MODE_OFF' export const SCREEN_RESIZED = 'SCREEN_RESIZED' export const TAB_CHANGED = 'TAB_CHANGED' export const TOGGLE_FILTER_VISIBILITY = 'TOGGLE_FILTER_VISIBILITY' +export const TRENDS_DATE_WARNING_DISMISSED = 'TRENDS_DATE_WARNING_DISMISSED' + // ---------------------------------------------------------------------------- // Simple actions /** @@ -34,18 +35,6 @@ export function mapWarningDismissed() { } } -/** - * Notifies the application that the print mode has changed - * - * @returns {string} a packaged payload to be used by Redux reducers - */ -export function printModeChanged() { - return { - type: PRINT_MODE_CHANGED, - requery: REQUERY_NEVER - } -} - /** * Notifies the application that the print mode has changed * @@ -96,3 +85,15 @@ export function tabChanged( tab ) { requery: REQUERY_HITS_ONLY } } + +/** + * Notifies the application that user dismissed trends date warning + * + * @returns {string} a packaged payload to be used by Redux reducers + */ +export function trendsDateWarningDismissed() { + return { + type: TRENDS_DATE_WARNING_DISMISSED, + requery: REQUERY_NEVER + } +} diff --git a/src/components/Charts/LineChart.jsx b/src/components/Charts/LineChart.jsx new file mode 100644 index 000000000..ff52dae63 --- /dev/null +++ b/src/components/Charts/LineChart.jsx @@ -0,0 +1,158 @@ +/* eslint complexity: ["error", 6] */ +import './LineChart.less' +import * as d3 from 'd3' + +import { getLastLineDate, getTooltipTitle } from '../../utils/chart' +import { line, tooltip } from 'britecharts' +import { connect } from 'react-redux' +import { hashObject } from '../../utils' +import { isDateEqual } from '../../utils/formatDate' +import React from 'react' +import { updateTrendsTooltip } from '../../actions/trends' + +export class LineChart extends React.Component { + tip = null + constructor( props ) { + super( props ) + this._updateTooltip = this._updateTooltip.bind( this ) + this._updateInternalTooltip = this._updateInternalTooltip.bind( this ) + } + + componentDidMount() { + this._redrawChart() + } + + componentDidUpdate( prevProps ) { + const props = this.props + if ( hashObject( prevProps.data ) !== hashObject( props.data ) || + hashObject( prevProps.width ) !== hashObject( props.width ) || + hashObject( prevProps.printMode ) !== hashObject( props.printMode ) ) { + this._redrawChart() + } + } + + _updateTooltip( point ) { + if ( !isDateEqual( this.props.tooltip.date, point.date ) ) { + this.props.tooltipUpdated( { + date: point.date, + dateRange: this.props.dateRange, + interval: this.props.interval, + values: point.topics + } ) + } + } + + _updateInternalTooltip( dataPoint, topicColorMap, dataPointXPosition ) { + const { dateRange, interval } = this.props + this.tip.title( getTooltipTitle( dataPoint.date, interval, dateRange, + false ) ) + this.tip.update( dataPoint, topicColorMap, dataPointXPosition ) + } + + _chartWidth( chartID ) { + const { + lens, printMode + } = this.props + if ( printMode ) { + return lens === 'Overview' ? 750 : 540 + } + const container = d3.select( chartID ) + return container.node().getBoundingClientRect().width + } + + _redrawChart() { + const { + colorMap, data, dateRange, interval, lastDate, lens + } = this.props + if ( !data.dataByTopic || !data.dataByTopic.length ) { + return + } + + const chartID = '#line-chart' + const container = d3.select( chartID ) + const width = this._chartWidth( chartID ) + d3.select( chartID + ' .line-chart' ).remove() + const lineChart = line() + this.tip = tooltip() + .shouldShowDateInTitle( false ) + .topicLabel( 'topics' ) + .title( 'Complaints' ) + + const tip = this.tip + const colorScheme = data.dataByTopic + .map( o => colorMap[o.topic] ) + + lineChart.margin( { left: 70, right: 10, top: 10, bottom: 40 } ) + .initializeVerticalMarker( true ) + .isAnimated( true ) + .tooltipThreshold( 1 ) + .grid( 'horizontal' ) + .aspectRatio( 0.5 ) + .width( width ) + .dateLabel( 'date' ) + .colorSchema( colorScheme ) + + if ( lens === 'Overview' ) { + lineChart + .on( 'customMouseOver', tip.show ) + .on( 'customMouseMove', this._updateInternalTooltip ) + .on( 'customMouseOut', tip.hide ) + } else { + lineChart.on( 'customMouseMove', this._updateTooltip ) + } + + container.datum( data ).call( lineChart ) + const tooltipContainer = + d3.select( chartID + ' .metadata-group .vertical-marker-container' ) + tooltipContainer.datum( [] ).call( tip ) + + const config = { + dateRange, + interval, + lastDate + } + + if ( lens !== 'Overview' ) { + // get the last date and fire it off to redux + this.props.tooltipUpdated( getLastLineDate( data, config ) ) + } + } + + render() { + return ( +
+

Complaints

+
+
+

Date received by the CFPB

+
+ ) + } +} + +export const mapDispatchToProps = dispatch => ( { + tooltipUpdated: tipEvent => { + // Analytics.sendEvent( + // Analytics.getDataLayerOptions( 'Trend Event: add', + // selectedState.abbr, ) + // ) + dispatch( updateTrendsTooltip( tipEvent ) ) + } +} ) + +export const mapStateToProps = state => ( { + colorMap: state.trends.colorMap, + data: state.trends.results.dateRangeLine, + dateRange: { + from: state.query.date_received_min, + to: state.query.date_received_max + }, + interval: state.query.dateInterval, + lastDate: state.trends.lastDate, + lens: state.query.lens, + printMode: state.view.printMode, + tooltip: state.trends.tooltip, + width: state.view.width +} ) + +export default connect( mapStateToProps, mapDispatchToProps )( LineChart ) diff --git a/src/components/Charts/LineChart.less b/src/components/Charts/LineChart.less new file mode 100644 index 000000000..f98d2c3d9 --- /dev/null +++ b/src/components/Charts/LineChart.less @@ -0,0 +1,93 @@ +@import (less) "../../css/base.less"; + +#line-chart { + .y-axis-group { + path { + display: none; + } + } + + .x-axis-group { + .month-axis { + .domain { + display: none; + } + } + } + .masking-rectangle { + display: none; + } + + /*primary comparable company */ + + .chart-group { + .topic:nth-child(2), + .topic:nth-child(3) { + .line { + stroke-width: 2; + } + } + } +} + +#stacked-area-chart, +#line-chart { + svg { + overflow: visible; + } + + .topic .line { + fill: none; + stroke-width: 1; + stroke-linecap: round; + stroke-linejoin: round; + } + + .circle-container { + circle { + fill: @white; + } + } + .grid-lines-group { + stroke: @gray-20; + stroke-width: 0.1%; + } + .x-axis-group { + line { + display: none; + } + } + //line on hover for the tooltip. + .vertical-marker { + stroke: @gray-20; + stroke-width: 2px; + stroke-dasharray: 4, 4; + } + .chart-group { + .area { + path.area { + fill: @gray-10; + opacity: .4; + } + } + .area-outline { + fill: none; + } + } +} + +.chart-wrapper { + p { + font-size: 12px; + font-weight: 600; + color: @gray; + + &.y-axis-label { + margin-left: @gutter-normal; + } + + &.x-axis-label { + margin-left: 45%; + } + } +} diff --git a/src/components/Charts/RowChart.jsx b/src/components/Charts/RowChart.jsx index 5eb8835cf..aaad70d4b 100644 --- a/src/components/Charts/RowChart.jsx +++ b/src/components/Charts/RowChart.jsx @@ -1,17 +1,26 @@ +/* eslint complexity: ["error", 5] */ + import './RowChart.less' import * as d3 from 'd3' +import { changeFocus, collapseTrend, expandTrend } from '../../actions/trends' +import { cloneDeep, coalesce, getAllFilters, hashObject } from '../../utils' +import { miniTooltip, row } from 'britecharts' import { connect } from 'react-redux' import { max } from 'd3-array' +import { MODE_MAP } from '../../constants' +import PropTypes from 'prop-types' import React from 'react' -import { row } from 'britecharts' +import { scrollToFocus } from '../../utils/trends' export class RowChart extends React.Component { constructor( props ) { super( props ) - const aggType = props.aggtype - this.aggtype = aggType - // only capitalize first letter - this.chartTitle = aggType.charAt( 0 ).toUpperCase() + aggType.slice( 1 ) + this._selectFocus = this._selectFocus.bind( this ) + this._toggleRow = this._toggleRow.bind( this ) + } + + _formatTip( value ) { + return value.toLocaleString() + ' complaints' } _getHeight( numRows ) { @@ -21,8 +30,9 @@ export class RowChart extends React.Component { _wrapText( text, width ) { // ignore test coverage since this is code borrowed from d3 mbostock // text wrapping functions + + /* eslint-disable complexity */ /* istanbul ignore next */ - // eslint-disable-next-line complexity text.each( function() { const innerText = d3.select( this ) if ( innerText.node().children && innerText.node().children.length > 0 ) { @@ -58,40 +68,58 @@ export class RowChart extends React.Component { } } } ) - } + /* eslint-enable complexity */ + } - componentDidUpdate() { + componentDidMount() { this._redrawChart() } + componentDidUpdate( prevProps ) { + const props = this.props + if ( hashObject( prevProps ) !== hashObject( props ) ) { + this._redrawChart() + } + } + // -------------------------------------------------------------------------- // Event Handlers // eslint-disable-next-line complexity _redrawChart() { - const componentProps = this.props - const { data, printMode } = componentProps - if ( !data || !data.length ) { + const { + colorScheme, data, id, printMode, total + } = this.props + // deep copy + // do this to prevent REDUX pollution + const rows = cloneDeep( data ).filter( o => { + if ( this.props.showTrends ) { + return true + } + + return o.name ? o.name.indexOf( 'Visualize trends for' ) === -1 : true + } ) + + if ( !rows || !rows.length || !total ) { return } - const rowData = data.slice( 0, 5 ) - const total = this.props.total - const ratio = total / max( rowData, o => o.value ) - const chartID = '#row-chart-' + this.aggtype - d3.select( chartID + ' .row-chart' ).remove() + const tooltip = miniTooltip() + tooltip.valueFormatter( this._formatTip ) + + const ratio = total / max( rows, o => o.value ) + const chartID = '#row-chart-' + id + d3.selectAll( chartID + ' .row-chart' ).remove() const rowContainer = d3.select( chartID ) const width = printMode ? 750 : rowContainer.node().getBoundingClientRect().width - const height = this._getHeight( rowData.length ) + const height = this._getHeight( rows.length ) const chart = row() - const marginLeft = width / 3 + const marginLeft = width / 4 // tweak to make the chart full width at desktop // add space at narrow width const marginRight = width < 600 ? 20 : -80 - const colorScheme = rowData.map( () => '#20aa3f' ) - chart.margin( { left: marginLeft, right: marginRight, @@ -100,48 +128,124 @@ export class RowChart extends React.Component { } ) .colorSchema( colorScheme ) .backgroundColor( '#f7f8f9' ) + .paddingBetweenGroups( 25 ) .enableLabels( true ) .labelsTotalCount( total.toLocaleString() ) .labelsNumberFormat( ',d' ) .outerPadding( 0.1 ) .percentageAxisToMaxRatio( ratio ) .yAxisLineWrapLimit( 2 ) - .yAxisPaddingBetweenChart( 5 ) + .yAxisPaddingBetweenChart( 20 ) .width( width ) .wrapLabels( true ) .height( height ) - - rowContainer.datum( rowData ).call( chart ) + .on( 'customMouseOver', tooltip.show ) + .on( 'customMouseMove', tooltip.update ) + .on( 'customMouseOut', tooltip.hide ) + + rowContainer.datum( rows ).call( chart ) + const tooltipContainer = + d3.selectAll( chartID + ' .row-chart .metadata-group' ) + tooltipContainer.datum( [] ).call( tooltip ) this._wrapText( d3.select( chartID ).selectAll( '.tick text' ), marginLeft ) + + this._wrapText( d3.select( chartID ) + .selectAll( '.view-more-label' ), width / 2 ) + + rowContainer + .selectAll( '.y-axis-group .tick' ) + .on( 'click', this._toggleRow ) + + rowContainer + .selectAll( '.view-more-label' ) + .on( 'click', this._selectFocus ) + + } + + _selectFocus( element ) { + // make sure to assign a valid lens when a row is clicked + const lens = this.props.lens === 'Overview' ? 'Product' : this.props.lens + const filters = coalesce( this.props.aggs, lens.toLowerCase(), [] ) + this.props.selectFocus( element, lens, filters ) } + _toggleRow( rowName ) { + // fire off different action depending on if the row is expanded or not + const { expandableRows, expandedTrends } = this.props + if ( expandableRows.includes( rowName ) ) { + if ( expandedTrends.includes( rowName ) ) { + this.props.collapseRow( rowName ) + } else { + this.props.expandRow( rowName ) + } + } + } + + render() { return ( + this.props.total > 0 &&
-

{ this.chartTitle } by highest complaint volume

-
+

{ this.props.title }

+

{ this.props.helperText }

+
) } } -export const mapStateToProps = ( state, ownProps ) => { - const { printMode, width } = state.view - // use state.query to rip filter out the bars - const aggtype = ownProps.aggtype - const filters = state.query[aggtype] - let data = state.map[aggtype] +export const mapDispatchToProps = dispatch => ( { + selectFocus: ( element, lens, filters ) => { + scrollToFocus() + let values = [] + if ( lens === 'Company' ) { + values.push( element.parent ) + } else { + const filterGroup = filters.find( o => o.key === element.parent ) + const keyName = 'sub_' + lens.toLowerCase() + '.raw' + values = filterGroup ? + getAllFilters( element.parent, filterGroup[keyName].buckets ) : [] + } - if ( filters && filters.length ) { - data = data.filter( o => filters.includes( o.name ) ) + dispatch( changeFocus( element.parent, lens, [ ...values ] ) ) + }, + collapseRow: rowName => { + dispatch( collapseTrend( rowName.trim() ) ) + }, + expandRow: rowName => { + dispatch( expandTrend( rowName.trim() ) ) } +} ) + +export const mapStateToProps = state => { + const { tab } = state.query + const lens = tab === MODE_MAP ? 'Product' : state.query.lens + const { aggs } = state + const { expandableRows, expandedTrends } = + coalesce( state, tab.toLowerCase(), {} ) + const { printMode, showTrends, width } = state.view return { - data, + aggs, + expandableRows, + expandedTrends, + lens, printMode, - total: state.aggs.total, + showTrends, + tab, width } } -export default connect( mapStateToProps )( RowChart ) +RowChart.propTypes = { + id: PropTypes.string.isRequired, + colorScheme: PropTypes.oneOfType( [ + PropTypes.array, + PropTypes.bool + ] ).isRequired, + data: PropTypes.array.isRequired, + title: PropTypes.string.isRequired, + total: PropTypes.number.isRequired +} + +export default connect( mapStateToProps, mapDispatchToProps )( RowChart ) diff --git a/src/components/Charts/RowChart.less b/src/components/Charts/RowChart.less index 1565b1f04..991c6ec56 100644 --- a/src/components/Charts/RowChart.less +++ b/src/components/Charts/RowChart.less @@ -13,5 +13,13 @@ &.adjust-upwards { transform: translateY(-12px); } + &.hidden { + display:none; + } + } + .view-more-label { + font-size: 12px; + text-decoration: underline; + fill: @pacific; } } diff --git a/src/components/Charts/StackedAreaChart.jsx b/src/components/Charts/StackedAreaChart.jsx new file mode 100644 index 000000000..190964c4f --- /dev/null +++ b/src/components/Charts/StackedAreaChart.jsx @@ -0,0 +1,134 @@ +import './StackedAreaChart.less' +import * as colors from '../../constants/colors' +import * as d3 from 'd3' +import { connect } from 'react-redux' +import { getLastDate } from '../../utils/chart' +import { hashObject } from '../../utils' +import { isDateEqual } from '../../utils/formatDate' +import React from 'react' +import { stackedArea } from 'britecharts' +import { updateTrendsTooltip } from '../../actions/trends' + + +export class StackedAreaChart extends React.Component { + constructor( props ) { + super( props ) + this._updateTooltip = this._updateTooltip.bind( this ) + } + + componentDidMount() { + this._redrawChart() + } + + componentDidUpdate( prevProps ) { + const props = this.props + if ( hashObject( prevProps.data ) !== hashObject( props.data ) || + hashObject( prevProps.width ) !== hashObject( props.width ) || + hashObject( prevProps.printMode ) !== hashObject( props.printMode ) ) { + this._redrawChart() + } + } + + _updateTooltip( point ) { + if ( !isDateEqual( this.props.tooltip.date, point.date ) ) { + this.props.tooltipUpdated( { + date: point.date, + dateRange: this.props.dateRange, + interval: this.props.interval, + values: point.values + } ) + } + } + + _chartWidth( chartID ) { + const { printMode } = this.props + if ( printMode ) { + return 540 + } + const container = d3.select( chartID ) + return container.node().getBoundingClientRect().width + } + + + _redrawChart() { + const { + colorMap, data, dateRange, interval, lastDate + } = this.props + if ( !data || !data.length ) { + return + } + + const chartID = '#stacked-area-chart' + const container = d3.select( chartID ) + const width = this._chartWidth( chartID ) + d3.select( chartID + ' .stacked-area' ).remove() + const stackedAreaChart = stackedArea() + const colorData = data.filter( + item => item.name !== 'Other' + ) + const colorScheme = [ ...new Set( colorData.map( item => item.name ) ) ] + .map( o => colorMap[o] ) + colorScheme.push( colors.DataLens[10] ) + + stackedAreaChart.margin( { left: 70, right: 10, top: 10, bottom: 40 } ) + .areaCurve( 'linear' ) + .initializeVerticalMarker( true ) + .isAnimated( false ) + .tooltipThreshold( 1 ) + .grid( 'horizontal' ) + .aspectRatio( 0.5 ) + .width( width ) + .dateLabel( 'date' ) + .colorSchema( colorScheme ) + .on( 'customMouseMove', this._updateTooltip ) + + container.datum( data ).call( stackedAreaChart ) + + const config = { + dateRange, + interval, + lastDate + } + + this.props.tooltipUpdated( getLastDate( data, config ) ) + } + + render() { + return ( +
+

Complaints

+
+
+

Date received by the CFPB

+
+ ) + } +} + +export const mapDispatchToProps = dispatch => ( { + tooltipUpdated: selectedState => { + // Analytics.sendEvent( + // Analytics.getDataLayerOptions( 'Trend Event: add', + // selectedState.abbr, ) + // ) + dispatch( updateTrendsTooltip( selectedState ) ) + } +} ) + +export const mapStateToProps = state => ( { + colorMap: state.trends.colorMap, + data: state.trends.results.dateRangeArea, + dateRange: { + from: state.query.date_received_min, + to: state.query.date_received_max + }, + interval: state.query.dateInterval, + lastDate: state.trends.lastDate, + lens: state.trends.lens, + printMode: state.view.printMode, + tooltip: state.trends.tooltip, + width: state.view.width +} ) + +export default connect( mapStateToProps, + mapDispatchToProps )( StackedAreaChart ) diff --git a/src/components/Charts/StackedAreaChart.less b/src/components/Charts/StackedAreaChart.less new file mode 100644 index 000000000..266b54900 --- /dev/null +++ b/src/components/Charts/StackedAreaChart.less @@ -0,0 +1,28 @@ +@import (less) "../../css/base.less"; + +#stacked-area-chart { + .stacked-area { + .y-axis-group { + .domain { + display: none; + } + } + } + +} + +.chart-wrapper { + p { + font-size: 12px; + font-weight: 600; + color: @gray; + + &.y-axis-label { + margin-left: @gutter-normal; + } + + &.x-axis-label { + margin-left: 45%; + } + } +} diff --git a/src/components/Charts/TileChartMap.jsx b/src/components/Charts/TileChartMap.jsx index 42b1938d2..16eb47e38 100644 --- a/src/components/Charts/TileChartMap.jsx +++ b/src/components/Charts/TileChartMap.jsx @@ -58,7 +58,7 @@ export class TileChartMap extends React.Component { const componentProps = this.props const mapElement = document.getElementById( 'tile-chart-map' ) const { dataNormalization, hasTip } = componentProps - const width = mapElement ? mapElement.clientWidth : 700; + const width = mapElement.clientWidth || 700 const data = updateData( this.props ) const options = { @@ -119,7 +119,7 @@ export const getStateClass = ( statesFilter, name ) => { export const processStates = state => { const statesFilter = state.query.state || [] - const states = state.map.state + const states = state.map.results.state const stateData = states.map( o => { const stateInfo = STATE_DATA[o.name] || { name: '', population: 1 } o.abbr = o.name diff --git a/src/components/Charts/TileMap/__fixtures__/complaints.js b/src/components/Charts/TileMap/__fixtures__/complaints.js index debb87ffb..c8dcdb7df 100644 --- a/src/components/Charts/TileMap/__fixtures__/complaints.js +++ b/src/components/Charts/TileMap/__fixtures__/complaints.js @@ -1,2 +1,2 @@ -export const raw = [ { className: 'default', name: 'AK', fullName: 'Alaska', value: 713, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.97, displayValue: 713 }, { className: 'deselected', name: 'AL', fullName: 'Alabama', value: 10380, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.14, displayValue: 10380 }, { className: 'selected', name: 'AR', fullName: 'Arkansas', value: 4402, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.48, displayValue: 4402 }, { className: 'default', name: 'AZ', fullName: 'Arizona', value: 14708, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.16, displayValue: 14708 }, { className: 'default', name: 'CA', fullName: 'California', value: 98601, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.53, displayValue: 98601 }, { className: 'default', name: 'CO', fullName: 'Colorado', value: 10643, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.96, displayValue: 10643 }, { className: 'default', name: 'CT', fullName: 'Connecticut', value: 7897, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.2, displayValue: 7897 }, { className: 'default', name: 'DC', fullName: 'District of Columbia', value: 3704, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 5.51, displayValue: 3704 }, { className: 'default', name: 'DE', fullName: 'Delaware', value: 3387, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.59, displayValue: 3387 }, { className: 'default', className: 'default', name: 'FL', fullName: 'Florida', value: 86241, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.25, displayValue: 86241 }, { className: 'default', name: 'GA', fullName: 'Georgia', value: 46649, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.57, displayValue: 46649 }, { className: 'default', name: 'HI', fullName: 'Hawaii', value: 2081, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.46, displayValue: 2081 }, { className: 'default', name: 'IA', fullName: 'Iowa', value: 2647, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.85, displayValue: 2647 }, { className: 'default', name: 'ID', fullName: 'Idaho', value: 1849, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 1849 }, { className: 'default', name: 'IL', fullName: 'Illinois', value: 31170, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.42, displayValue: 31170 }, { className: 'default', name: 'IN', fullName: 'Indiana', value: 8446, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.28, displayValue: 8446 }, { className: 'default', name: 'KS', fullName: 'Kansas', value: 3553, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.22, displayValue: 3553 }, { className: 'default', name: 'KY', fullName: 'Kentucky', value: 5029, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.14, displayValue: 5029 }, { className: 'default', name: 'LA', fullName: 'Louisiana', value: 11253, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.41, displayValue: 11253 }, { className: 'default', name: 'MA', fullName: 'Massachusetts', value: 12010, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.77, displayValue: 12010 }, { className: 'default', name: 'MD', fullName: 'Maryland', value: 18317, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.05, displayValue: 18317 }, { className: 'default', name: 'ME', fullName: 'Maine', value: 1526, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.15, displayValue: 1526 }, { className: 'default', name: 'MI', fullName: 'Michigan', value: 16521, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.66, displayValue: 16521 }, { className: 'default', name: 'MN', fullName: 'Minnesota', value: 6902, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 6902 }, { className: 'default', name: 'MO', fullName: 'Missouri', value: 11735, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.93, displayValue: 11735 }, { className: 'default', name: 'MS', fullName: 'Mississippi', value: 5230, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.75, displayValue: 5230 }, { className: 'default', name: 'MT', fullName: 'Montana', value: 1093, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.06, displayValue: 1093 }, { className: 'default', name: 'NC', fullName: 'North Carolina', value: 25978, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.58, displayValue: 25978 }, { className: 'default', name: 'ND', fullName: 'North Dakota', value: 753, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.01, displayValue: 753 }, { className: 'default', name: 'NE', fullName: 'Nebraska', value: 1801, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.95, displayValue: 1801 }, { className: 'default', name: 'NH', fullName: 'New Hampshire', value: 1869, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.4, displayValue: 1869 }, { className: 'default', name: 'NJ', fullName: 'New Jersey', value: 25600, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.86, displayValue: 25600 }, { className: 'default', name: 'NM', fullName: 'New Mexico', value: 2725, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.31, displayValue: 2725 }, { className: 'default', name: 'NV', fullName: 'Nevada', value: 10974, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.8, displayValue: 10974 }, { className: 'default', name: 'NY', fullName: 'New York', value: 53008, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.68, displayValue: 53008 }, { className: 'default', name: 'OH', fullName: 'Ohio', value: 21901, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.89, displayValue: 21901 }, { className: 'default', name: 'OK', fullName: 'Oklahoma', value: 4329, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.11, displayValue: 4329 }, { className: 'default', name: 'OR', fullName: 'Oregon', value: 5838, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.45, displayValue: 5838 }, { className: 'default', name: 'PA', fullName: 'Pennsylvania', value: 25605, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2, displayValue: 25605 }, { className: 'default', name: 'RI', fullName: 'Rhode Island', value: 1884, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.78, displayValue: 1884 }, { className: 'default', name: 'SC', fullName: 'South Carolina', value: 13788, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.82, displayValue: 13788 }, { className: 'default', name: 'SD', fullName: 'South Dakota', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.82, displayValue: 702 }, { className: 'default', name: 'TN', fullName: 'Tennessee', value: 13730, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.08, displayValue: 13730 }, { className: 'default', name: 'TX', fullName: 'Texas', value: 72300, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.64, displayValue: 72300 }, { className: 'default', name: 'UT', fullName: 'Utah', value: 6252, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.09, displayValue: 6252 }, { className: 'default', name: 'VA', fullName: 'Virginia', value: 18919, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.26, displayValue: 18919 }, { className: 'default', name: 'VT', fullName: 'Vermont', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 702 }, { className: 'default', name: 'WA', fullName: 'Washington', value: 12217, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.7, displayValue: 12217 }, { className: 'default', name: 'WI', fullName: 'Wisconsin', value: 7234, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 7234 }, { className: 'default', name: 'WV', fullName: 'West Virginia', value: 1540, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.84, displayValue: 1540 }, { className: 'default', name: 'WY', fullName: 'Wyoming', value: 651, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 651 } ] +export const raw = [ { className: 'default', name: 'AK', fullName: 'Alaska', value: 713, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.97, displayValue: 713 }, { className: 'deselected', name: 'AL', fullName: 'Alabama', value: 10380, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.14, displayValue: 10380 }, { className: 'selected', name: 'AR', fullName: 'Arkansas', value: 4402, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.48, displayValue: 4402 }, { className: 'default', name: 'AZ', fullName: 'Arizona', value: 14708, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.16, displayValue: 14708 }, { className: 'default', name: 'CA', fullName: 'California', value: 98601, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.53, displayValue: 98601 }, { className: 'default', name: 'CO', fullName: 'Colorado', value: 10643, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.96, displayValue: 10643 }, { className: 'default', name: 'CT', fullName: 'Connecticut', value: 7897, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.2, displayValue: 7897 }, { className: 'default', name: 'DC', fullName: 'District of Columbia', value: 3704, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 5.51, displayValue: 3704 }, { className: 'default', name: 'DE', fullName: 'Delaware', value: 3387, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.59, displayValue: 3387 }, { className: 'default', name: 'FL', fullName: 'Florida', value: 86241, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.25, displayValue: 86241 }, { className: 'default', name: 'GA', fullName: 'Georgia', value: 46649, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.57, displayValue: 46649 }, { className: 'default', name: 'HI', fullName: 'Hawaii', value: 2081, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.46, displayValue: 2081 }, { className: 'default', name: 'IA', fullName: 'Iowa', value: 2647, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.85, displayValue: 2647 }, { className: 'default', name: 'ID', fullName: 'Idaho', value: 1849, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 1849 }, { className: 'default', name: 'IL', fullName: 'Illinois', value: 31170, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.42, displayValue: 31170 }, { className: 'default', name: 'IN', fullName: 'Indiana', value: 8446, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.28, displayValue: 8446 }, { className: 'default', name: 'KS', fullName: 'Kansas', value: 3553, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.22, displayValue: 3553 }, { className: 'default', name: 'KY', fullName: 'Kentucky', value: 5029, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.14, displayValue: 5029 }, { className: 'default', name: 'LA', fullName: 'Louisiana', value: 11253, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.41, displayValue: 11253 }, { className: 'default', name: 'MA', fullName: 'Massachusetts', value: 12010, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.77, displayValue: 12010 }, { className: 'default', name: 'MD', fullName: 'Maryland', value: 18317, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.05, displayValue: 18317 }, { className: 'default', name: 'ME', fullName: 'Maine', value: 1526, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.15, displayValue: 1526 }, { className: 'default', name: 'MI', fullName: 'Michigan', value: 16521, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.66, displayValue: 16521 }, { className: 'default', name: 'MN', fullName: 'Minnesota', value: 6902, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 6902 }, { className: 'default', name: 'MO', fullName: 'Missouri', value: 11735, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.93, displayValue: 11735 }, { className: 'default', name: 'MS', fullName: 'Mississippi', value: 5230, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.75, displayValue: 5230 }, { className: 'default', name: 'MT', fullName: 'Montana', value: 1093, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.06, displayValue: 1093 }, { className: 'default', name: 'NC', fullName: 'North Carolina', value: 25978, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.58, displayValue: 25978 }, { className: 'default', name: 'ND', fullName: 'North Dakota', value: 753, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.01, displayValue: 753 }, { className: 'default', name: 'NE', fullName: 'Nebraska', value: 1801, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.95, displayValue: 1801 }, { className: 'default', name: 'NH', fullName: 'New Hampshire', value: 1869, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.4, displayValue: 1869 }, { className: 'default', name: 'NJ', fullName: 'New Jersey', value: 25600, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.86, displayValue: 25600 }, { className: 'default', name: 'NM', fullName: 'New Mexico', value: 2725, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.31, displayValue: 2725 }, { className: 'default', name: 'NV', fullName: 'Nevada', value: 10974, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.8, displayValue: 10974 }, { className: 'default', name: 'NY', fullName: 'New York', value: 53008, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.68, displayValue: 53008 }, { className: 'default', name: 'OH', fullName: 'Ohio', value: 21901, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.89, displayValue: 21901 }, { className: 'default', name: 'OK', fullName: 'Oklahoma', value: 4329, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.11, displayValue: 4329 }, { className: 'default', name: 'OR', fullName: 'Oregon', value: 5838, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.45, displayValue: 5838 }, { className: 'default', name: 'PA', fullName: 'Pennsylvania', value: 25605, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2, displayValue: 25605 }, { className: 'default', name: 'RI', fullName: 'Rhode Island', value: 1884, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.78, displayValue: 1884 }, { className: 'default', name: 'SC', fullName: 'South Carolina', value: 13788, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.82, displayValue: 13788 }, { className: 'default', name: 'SD', fullName: 'South Dakota', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.82, displayValue: 702 }, { className: 'default', name: 'TN', fullName: 'Tennessee', value: 13730, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.08, displayValue: 13730 }, { className: 'default', name: 'TX', fullName: 'Texas', value: 72300, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.64, displayValue: 72300 }, { className: 'default', name: 'UT', fullName: 'Utah', value: 6252, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.09, displayValue: 6252 }, { className: 'default', name: 'VA', fullName: 'Virginia', value: 18919, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.26, displayValue: 18919 }, { className: 'default', name: 'VT', fullName: 'Vermont', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 702 }, { className: 'default', name: 'WA', fullName: 'Washington', value: 12217, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.7, displayValue: 12217 }, { className: 'default', name: 'WI', fullName: 'Wisconsin', value: 7234, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 7234 }, { className: 'default', name: 'WV', fullName: 'West Virginia', value: 1540, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.84, displayValue: 1540 }, { className: 'default', name: 'WY', fullName: 'Wyoming', value: 651, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 651 } ] export const perCapita = [ { name: 'AK', fullName: 'Alaska', value: 713, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.97, displayValue: 0.97 }, { className: 'default', name: 'AL', fullName: 'Alabama', value: 10380, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.14, displayValue: 2.14 }, { className: 'default', name: 'AR', fullName: 'Arkansas', value: 4402, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.48, displayValue: 1.48 }, { className: 'default', name: 'AZ', fullName: 'Arizona', value: 14708, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.16, displayValue: 2.16 }, { className: 'default', name: 'CA', fullName: 'California', value: 98601, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.53, displayValue: 2.53 }, { className: 'default', name: 'CO', fullName: 'Colorado', value: 10643, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.96, displayValue: 1.96 }, { className: 'default', name: 'CT', fullName: 'Connecticut', value: 7897, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.2, displayValue: 2.2 }, { className: 'default', name: 'DC', fullName: 'District of Columbia', value: 3704, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 5.51, displayValue: 5.51 }, { className: 'default', name: 'DE', fullName: 'Delaware', value: 3387, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.59, displayValue: 3.59 }, { className: 'default', name: 'FL', fullName: 'Florida', value: 86241, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.25, displayValue: 4.25 }, { className: 'default', name: 'GA', fullName: 'Georgia', value: 46649, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 4.57, displayValue: 4.57 }, { className: 'default', name: 'HI', fullName: 'Hawaii', value: 2081, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.46, displayValue: 1.46 }, { className: 'default', name: 'IA', fullName: 'Iowa', value: 2647, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.85, displayValue: 0.85 }, { className: 'default', name: 'ID', fullName: 'Idaho', value: 1849, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 1.12 }, { className: 'default', name: 'IL', fullName: 'Illinois', value: 31170, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.42, displayValue: 2.42 }, { className: 'default', name: 'IN', fullName: 'Indiana', value: 8446, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.28, displayValue: 1.28 }, { className: 'default', name: 'KS', fullName: 'Kansas', value: 3553, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.22, displayValue: 1.22 }, { className: 'default', name: 'KY', fullName: 'Kentucky', value: 5029, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.14, displayValue: 1.14 }, { className: 'default', name: 'LA', fullName: 'Louisiana', value: 11253, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.41, displayValue: 2.41 }, { className: 'default', name: 'MA', fullName: 'Massachusetts', value: 12010, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.77, displayValue: 1.77 }, { className: 'default', name: 'MD', fullName: 'Maryland', value: 18317, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.05, displayValue: 3.05 }, { className: 'default', name: 'ME', fullName: 'Maine', value: 1526, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.15, displayValue: 1.15 }, { className: 'default', name: 'MI', fullName: 'Michigan', value: 16521, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.66, displayValue: 1.66 }, { className: 'default', name: 'MN', fullName: 'Minnesota', value: 6902, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 1.26 }, { className: 'default', name: 'MO', fullName: 'Missouri', value: 11735, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.93, displayValue: 1.93 }, { className: 'default', name: 'MS', fullName: 'Mississippi', value: 5230, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.75, displayValue: 1.75 }, { className: 'default', name: 'MT', fullName: 'Montana', value: 1093, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.06, displayValue: 1.06 }, { className: 'default', name: 'NC', fullName: 'North Carolina', value: 25978, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.58, displayValue: 2.58 }, { className: 'default', name: 'ND', fullName: 'North Dakota', value: 753, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.01, displayValue: 1.01 }, { className: 'default', name: 'NE', fullName: 'Nebraska', value: 1801, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.95, displayValue: 0.95 }, { className: 'default', name: 'NH', fullName: 'New Hampshire', value: 1869, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.4, displayValue: 1.4 }, { className: 'default', name: 'NJ', fullName: 'New Jersey', value: 25600, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.86, displayValue: 2.86 }, { className: 'default', name: 'NM', fullName: 'New Mexico', value: 2725, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.31, displayValue: 1.31 }, { className: 'default', name: 'NV', fullName: 'Nevada', value: 10974, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 3.8, displayValue: 3.8 }, { className: 'default', name: 'NY', fullName: 'New York', value: 53008, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.68, displayValue: 2.68 }, { className: 'default', name: 'OH', fullName: 'Ohio', value: 21901, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.89, displayValue: 1.89 }, { className: 'default', name: 'OK', fullName: 'Oklahoma', value: 4329, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.11, displayValue: 1.11 }, { className: 'default', name: 'OR', fullName: 'Oregon', value: 5838, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.45, displayValue: 1.45 }, { className: 'default', name: 'PA', fullName: 'Pennsylvania', value: 25605, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2, displayValue: 2 }, { className: 'default', name: 'RI', fullName: 'Rhode Island', value: 1884, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.78, displayValue: 1.78 }, { className: 'default', name: 'SC', fullName: 'South Carolina', value: 13788, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.82, displayValue: 2.82 }, { className: 'default', name: 'SD', fullName: 'South Dakota', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.82, displayValue: 0.82 }, { className: 'default', name: 'TN', fullName: 'Tennessee', value: 13730, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.08, displayValue: 2.08 }, { className: 'default', name: 'TX', fullName: 'Texas', value: 72300, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.64, displayValue: 2.64 }, { className: 'default', name: 'UT', fullName: 'Utah', value: 6252, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.09, displayValue: 2.09 }, { className: 'default', name: 'VA', fullName: 'Virginia', value: 18919, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 2.26, displayValue: 2.26 }, { className: 'default', name: 'VT', fullName: 'Vermont', value: 702, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 1.12 }, { className: 'default', name: 'WA', fullName: 'Washington', value: 12217, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.7, displayValue: 1.7 }, { className: 'default', name: 'WI', fullName: 'Wisconsin', value: 7234, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.26, displayValue: 1.26 }, { className: 'default', name: 'WV', fullName: 'West Virginia', value: 1540, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 0.84, displayValue: 0.84 }, { className: 'default', name: 'WY', fullName: 'Wyoming', value: 651, issue: 'Incorrect information on your report', product: 'Credit reporting, credit repair services, or other personal consumer reports', perCapita: 1.12, displayValue: 1.12 } ] diff --git a/src/components/Charts/TileMap/__tests__/TileMap.spec.jsx b/src/components/Charts/TileMap/__tests__/TileMap.spec.jsx index 0eb07d1bd..844f09668 100644 --- a/src/components/Charts/TileMap/__tests__/TileMap.spec.jsx +++ b/src/components/Charts/TileMap/__tests__/TileMap.spec.jsx @@ -192,6 +192,18 @@ describe( 'Tile map', () => { } ); }) + describe( 'getColorByValue', () => { + let scaleFn + beforeEach( () => { + scaleFn = jest.fn( x => x ) + } ) + + it( 'returns WHITE when no value', () => { + const res = sut.getColorByValue( false, scaleFn ) + expect( res ).toEqual( '#ffffff' ) + } ) + } ) + it( 'formats a map tile', () => { sut.point = { className: 'default', @@ -305,6 +317,39 @@ describe( 'Tile map', () => { expect( scale ).toHaveBeenCalledTimes( 51 ) } ); + it( 'Processes the map data - empty shading', () => { + const scale = jest.fn().mockReturnValue( '#ffffff' ) + + const result = sut.processMapData( complaints.raw, scale ); + // test only the first one & 3rd for path, className, color are found + expect( result[0] ).toEqual( { + className: 'empty', + name: 'AK', + fullName: 'Alaska', + value: 713, + issue: 'Incorrect information on your report', + product: 'Credit reporting, credit repair services, or other personal consumer reports', + perCapita: 0.97, + displayValue: 713, + color: '#ffffff', + path: 'M92,-245L175,-245,175,-162,92,-162,92,-245' + } ); + + expect( result[2] ).toEqual( { + className: 'selected', + name: 'AR', + fullName: 'Arkansas', + value: 4402, + issue: 'Incorrect information on your report', + product: 'Credit reporting, credit repair services, or other personal consumer reports', + perCapita: 1.48, + displayValue: 4402, + color: '#ffffff', + path: 'M367,-428L450,-428,450,-345,367,-345,367,-428' + } ); + expect( scale ).toHaveBeenCalledTimes( 51 ) + } ); + describe( 'legend', () => { let chart; beforeEach( () => { @@ -409,6 +454,22 @@ describe( 'Tile map', () => { expect( drawSpy ).toHaveBeenCalled(); } ); + it( 'can construct a map with events', () => { + const options = { + el: document.createElement( 'div' ), + data: [], + events: { foo: jest.fn() }, + hasTip: true, + isPerCapita: false, + width: 400 + }; + + const drawSpy = jest.spyOn( TileMap.prototype, 'draw' ); + // eslint-disable-next-line no-unused-vars + const map = new TileMap( options ); + expect( drawSpy ).toHaveBeenCalled(); + } ); + it( 'can construct a perCapita map', () => { const options = { el: document.createElement( 'div' ), diff --git a/src/components/DateInput/DateInput.less b/src/components/DateInput/DateInput.less index ee5882d8c..067cfd602 100644 --- a/src/components/DateInput/DateInput.less +++ b/src/components/DateInput/DateInput.less @@ -4,4 +4,7 @@ .a-text-input { padding-right: @grid_gutter-width; } + .a-btn { + color: @gray; + } } diff --git a/src/components/Filters/AggregationBranch.jsx b/src/components/Filters/AggregationBranch.jsx index 394219efb..d6b89b50c 100644 --- a/src/components/Filters/AggregationBranch.jsx +++ b/src/components/Filters/AggregationBranch.jsx @@ -1,6 +1,6 @@ import './AggregationBranch.less' import { addMultipleFilters, removeMultipleFilters } from '../../actions/filter' -import { bindAll, coalesce, slugify } from '../../utils' +import { bindAll, coalesce, getAllFilters, slugify } from '../../utils' import AggregationItem from './AggregationItem' import { connect } from 'react-redux'; import { FormattedNumber } from 'react-intl' @@ -30,11 +30,7 @@ export class AggregationBranch extends React.Component { activeChildren, item, subitems, fieldName, checkedState } = this.props - const values = new Set() - // Add the parent - values.add( item.key ) - // Add the shown subitems - subitems.forEach( sub => { values.add( slugify( item.key, sub.key ) ) } ) + const values = getAllFilters( item.key, subitems ) // Add the active filters (that might be hidden) activeChildren.forEach( child => values.add( child ) ) @@ -56,6 +52,7 @@ export class AggregationBranch extends React.Component { // Fix up the subitems to prepend the current item key const buckets = subitems.map( sub => ( { + disabled: item.disabled, key: slugify( item.key, sub.key ), value: sub.key, // eslint-disable-next-line camelcase @@ -85,6 +82,7 @@ export class AggregationBranch extends React.Component {
  • {item.key} @@ -175,6 +172,7 @@ export const mapStateToProps = ( state, ownProps ) => { return { activeChildren, checkedState, + focus: state.query.focus, showChildren: activeChildren.length > 0 } } diff --git a/src/components/Filters/AggregationItem.jsx b/src/components/Filters/AggregationItem.jsx index e89db0373..f456d057a 100644 --- a/src/components/Filters/AggregationItem.jsx +++ b/src/components/Filters/AggregationItem.jsx @@ -8,11 +8,11 @@ export const AggregationItem = ( { item, fieldName, active, onClick } ) => { const value = item.value || item.key const liStyle = 'layout-row m-form-field m-form-field__checkbox' const id = fieldName + item.key.replace( ' ', '' ) - return (
  • - + ) } - - - // -------------------------------------------------------------------------- - // Typeahead interface - - _onInputChange( value ) { - const n = value.toLowerCase() - - const qs = this.props.queryString + '&text=' + value - - const uri = '@@API_suggest_company/' + qs - return fetch( uri ) - .then( result => result.json() ) - .then( items => items.map( x => ( { - key: x, - label: x, - position: x.toLowerCase().indexOf( n ), - value - } ) ) ) - } - - _renderOption( obj ) { - return { - value: obj.key, - component: - } - } - - _onOptionSelected( item ) { - this.props.typeaheadSelect( item.key ) - } -} - -Company.propTypes = { - debounceWait: PropTypes.number } -Company.defaultProps = { - debounceWait: 250 -} export const mapStateToProps = state => { - const options = state.aggs[FIELD_NAME] || [] + const options = cloneDeep( coalesce( state.aggs, FIELD_NAME, [] ) ) + const selections = coalesce( state.query, FIELD_NAME, [] ) + const { focus } = state.query + const isFocusPage = focus && state.query.lens === 'Company' + + options.forEach( o => { + o.disabled = Boolean( isFocusPage && o.key !== focus ) + } ) return { options, queryString: state.query.queryString, - selections: state.query[FIELD_NAME] || [] + selections } } -export const mapDispatchToProps = dispatch => ( { - typeaheadSelect: value => { - dispatch( addMultipleFilters( FIELD_NAME, [ value ] ) ) - } -} ) - -export default connect( mapStateToProps, mapDispatchToProps )( Company ) +export default connect( mapStateToProps )( Company ) diff --git a/src/components/Filters/CompanyTypeahead.jsx b/src/components/Filters/CompanyTypeahead.jsx new file mode 100644 index 000000000..db9ae64f6 --- /dev/null +++ b/src/components/Filters/CompanyTypeahead.jsx @@ -0,0 +1,89 @@ +import { addMultipleFilters } from '../../actions/filter' +import { bindAll } from '../../utils' +import { connect } from 'react-redux' +import HighlightingOption from '../Typeahead/HighlightingOption' +import PropTypes from 'prop-types' +import React from 'react' +import Typeahead from '../Typeahead' + +const FIELD_NAME = 'company' + +export class CompanyTypeahead extends React.Component { + constructor( props ) { + super( props ) + + // Bindings + bindAll( this, [ + '_onInputChange', + '_onOptionSelected', + '_renderOption' + ] ) + } + + render() { + return ( + + ) + } + + + // -------------------------------------------------------------------------- + // Typeahead interface + + _onInputChange( value ) { + const n = value.toLowerCase() + + const qs = this.props.queryString + '&text=' + value + + const uri = '@@API_suggest_company/' + qs + return fetch( uri ) + .then( result => result.json() ) + .then( items => items.map( x => ( { + key: x, + label: x, + position: x.toLowerCase().indexOf( n ), + value + } ) ) ) + } + + _renderOption( obj ) { + return { + value: obj.key, + component: + } + } + + _onOptionSelected( item ) { + this.props.typeaheadSelect( item.key ) + } +} + +CompanyTypeahead.propTypes = { + debounceWait: PropTypes.number +} + +CompanyTypeahead.defaultProps = { + debounceWait: 250 +} + +export const mapStateToProps = state => ( { + disabled: state.query.focus && state.query.lens === 'Company', + queryString: state.query.queryString +} ) + +export const mapDispatchToProps = dispatch => ( { + typeaheadSelect: value => { + dispatch( addMultipleFilters( FIELD_NAME, [ value ] ) ) + } +} ) + +export default connect( mapStateToProps, + mapDispatchToProps )( CompanyTypeahead ) diff --git a/src/components/Filters/FederalState.jsx b/src/components/Filters/FederalState.jsx index 209f07223..76819a525 100644 --- a/src/components/Filters/FederalState.jsx +++ b/src/components/Filters/FederalState.jsx @@ -21,7 +21,7 @@ export class FederalState extends React.Component { } render() { - const desc = 'The state of the mailing address provided by the consumer' + const desc = 'The state in the mailing address provided by the consumer' return (
    diff --git a/src/components/Filters/FilterPanelToggle.jsx b/src/components/Filters/FilterPanelToggle.jsx index 57e09c87e..bec07e0ef 100644 --- a/src/components/Filters/FilterPanelToggle.jsx +++ b/src/components/Filters/FilterPanelToggle.jsx @@ -11,8 +11,6 @@ export class FilterPanelToggle extends React.Component {

     

    diff --git a/src/components/Filters/Product.jsx b/src/components/Filters/Product.jsx index f93ee65ae..72fadf30f 100644 --- a/src/components/Filters/Product.jsx +++ b/src/components/Filters/Product.jsx @@ -1,9 +1,9 @@ +import { MODE_TRENDS, SLUG_SEPARATOR } from '../../constants' import AggregationBranch from './AggregationBranch' import CollapsibleFilter from './CollapsibleFilter' import { connect } from 'react-redux' import MoreOrLess from './MoreOrLess' import React from 'react' -import { SLUG_SEPARATOR } from '../../constants' import { sortSelThenCount } from '../../utils' export class Product extends React.Component { @@ -46,7 +46,8 @@ export class Product extends React.Component { export const mapStateToProps = state => { // See if there are an active product filters - const allProducts = state.query.product || [] + const { focus, lens, product, tab } = state.query + const allProducts = product || [] const selections = [] // Reduce the products to the parent keys (and dedup) @@ -60,6 +61,15 @@ export const mapStateToProps = state => { // Make a cloned, sorted version of the aggs const options = sortSelThenCount( state.aggs.product, selections ) + if ( focus ) { + const isProductFocus = tab === MODE_TRENDS && lens === 'Product' + options.forEach( o => { + o.disabled = isProductFocus ? o.key !== focus : false + o['sub_product.raw'].buckets.forEach( v => { + v.disabled = isProductFocus ? o.disabled : false + } ) + } ) + } return { options diff --git a/src/components/Filters/__tests__/Company.spec.jsx b/src/components/Filters/__tests__/Company.spec.jsx index 628d10283..9200e90ec 100644 --- a/src/components/Filters/__tests__/Company.spec.jsx +++ b/src/components/Filters/__tests__/Company.spec.jsx @@ -1,11 +1,10 @@ -import { shallow } from 'enzyme'; -import React from 'react' +import configureMockStore from 'redux-mock-store' import { IntlProvider } from 'react-intl' import { Provider } from 'react-redux' +import React from 'react' +import ReduxCompany, { mapStateToProps } from '../Company' import renderer from 'react-test-renderer' -import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import ReduxCompany, { mapDispatchToProps, Company } from '../Company' const fixture = [ { key: "Monocle Popper Inc", doc_count: 9999 }, @@ -14,22 +13,6 @@ const fixture = [ { key: "EZ Credit", doc_count: 9 } ] -function setupEnzyme() { - const props = { - forTypeahead: [], - options: [], - queryString: '?foo=bar&baz=qaz', - typeaheadSelect: jest.fn() - } - - const target = shallow(); - - return { - props, - target - } -} - function setupSnapshot(initialFixture) { const middlewares = [thunk] const mockStore = configureMockStore(middlewares) @@ -66,64 +49,62 @@ describe('component::Company', () => { }) }) - describe('Typeahead interface', () => { - beforeEach(() => { - global.fetch = jest.fn().mockImplementation((url) => { - expect(url).toContain('@@API_suggest_company/?foo=bar&baz=qaz&text=') - - return new Promise((resolve) => { - resolve({ - json: function() { - return ['foo', 'bar', 'baz', 'qaz'] - } - }) - }) - }) - }) - - describe('_onInputChange', () => { - it('provides a promise', () => { - const {target} = setupEnzyme() - const actual = target.instance()._onInputChange('mo') - expect(actual.then).toBeInstanceOf(Function) - }) - }) + describe('mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + aggs: { + company: [ + { key: 'a' }, + { key: 'b' }, + { key: 'c' } + ] + }, + query: { + company: [ { key: 'a' } ], + focus: '', + lens: '', + queryString: '?dsaf=fdas' + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + options: [ + { disabled: false, key: 'a' }, + { disabled: false, key: 'b' }, + { disabled: false, key: 'c' } + ], + queryString: '?dsaf=fdas', + selections: [ { key: 'a' } ] + } ) + } ) - describe('_renderOption', () => { - it('produces a custom component', () => { - const {target, props} = setupEnzyme() - const option = { - key: 'Foo', - label: 'foo', - position: 0, - value: 'FOO' + it( 'disables some options on Focus page', () => { + const state = { + aggs: { + company: [ + { key: 'a' }, + { key: 'b' }, + { key: 'c' } + ] + }, + query: { + company: [ { key: 'a' } ], + focus: 'a', + lens: 'Company', + queryString: '?dsaf=fdas' } - const actual = target.instance()._renderOption(option) - expect(actual.value).toEqual('Foo') - expect(actual.component).toMatchObject({ - props: { - label: 'foo', - position: 0, - value: 'FOO' - } - }) - }) - }) - - describe('_onOptionSelected', () => { - it('checks the filter associated with the option', () => { - const {target, props} = setupEnzyme() - target.instance()._onOptionSelected({key: 'foo'}) - expect(props.typeaheadSelect).toHaveBeenCalledWith('foo') - }) - }) - }) + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + options: [ + { disabled: false, key: 'a' }, + { disabled: true, key: 'b' }, + { disabled: true, key: 'c' } + ], + queryString: '?dsaf=fdas', + selections: [ { key: 'a' } ] + } ) + } ) - describe('mapDispatchToProps', () => { - it('hooks into addMultipleFilters', () => { - const dispatch = jest.fn() - mapDispatchToProps(dispatch).typeaheadSelect('foo') - expect(dispatch.mock.calls.length).toEqual(1) - }) }) }) diff --git a/src/components/Filters/__tests__/CompanyTypeahead.spec.jsx b/src/components/Filters/__tests__/CompanyTypeahead.spec.jsx new file mode 100644 index 000000000..38ab624ef --- /dev/null +++ b/src/components/Filters/__tests__/CompanyTypeahead.spec.jsx @@ -0,0 +1,123 @@ +import ReduxCompanyTypeahead, { CompanyTypeahead, mapDispatchToProps } from '../CompanyTypeahead' +import configureMockStore from 'redux-mock-store' +import { IntlProvider } from 'react-intl' +import { Provider } from 'react-redux' +import React from 'react' +import renderer from 'react-test-renderer' +import { shallow } from 'enzyme' +import thunk from 'redux-thunk' + +function setupEnzyme() { + const props = { + queryString: '?foo=bar&baz=qaz', + typeaheadSelect: jest.fn() + } + + const target = shallow(); + + return { + props, + target + } +} + +function setupSnapshot( { focus, lens, queryString } ) { + const middlewares = [thunk] + const mockStore = configureMockStore(middlewares) + const store = mockStore( { + query: { + focus, + lens, + queryString + } + }) + + return renderer.create( + + + + + + ) +} + +describe('component::CompanyTypeahead', () => { + describe('snapshots', () => { + it('renders without crashing', () => { + const target = setupSnapshot({}) + let tree = target.toJSON() + expect(tree).toMatchSnapshot() + }) + + it( 'renders disabled without crashing', () => { + const target = setupSnapshot( { + lens: 'Company', + focus: 'Acme' + } ) + + let tree = target.toJSON() + expect(tree).toMatchSnapshot() + }) + }) + + describe('Typeahead interface', () => { + beforeEach(() => { + global.fetch = jest.fn().mockImplementation((url) => { + expect(url).toContain('@@API_suggest_company/?foo=bar&baz=qaz&text=') + + return new Promise((resolve) => { + resolve({ + json: function() { + return ['foo', 'bar', 'baz', 'qaz'] + } + }) + }) + }) + }) + + describe('_onInputChange', () => { + it('provides a promise', () => { + const {target} = setupEnzyme() + const actual = target.instance()._onInputChange('mo') + expect(actual.then).toBeInstanceOf(Function) + }) + }) + + describe('_renderOption', () => { + it('produces a custom component', () => { + const {target, props} = setupEnzyme() + const option = { + key: 'Foo', + label: 'foo', + position: 0, + value: 'FOO' + } + const actual = target.instance()._renderOption(option) + expect(actual.value).toEqual('Foo') + expect(actual.component).toMatchObject({ + props: { + label: 'foo', + position: 0, + value: 'FOO' + } + }) + }) + }) + + describe('_onOptionSelected', () => { + it('checks the filter associated with the option', () => { + const {target, props} = setupEnzyme() + target.instance()._onOptionSelected({key: 'foo'}) + expect(props.typeaheadSelect).toHaveBeenCalledWith('foo') + }) + }) + }) + + describe('mapDispatchToProps', () => { + it('hooks into addMultipleFilters', () => { + const dispatch = jest.fn() + mapDispatchToProps(dispatch).typeaheadSelect('foo') + expect(dispatch.mock.calls.length).toEqual(1) + }) + }) +}) diff --git a/src/components/Filters/__tests__/Product.spec.jsx b/src/components/Filters/__tests__/Product.spec.jsx index 544c15625..6f4e25052 100644 --- a/src/components/Filters/__tests__/Product.spec.jsx +++ b/src/components/Filters/__tests__/Product.spec.jsx @@ -10,53 +10,53 @@ import { slugify } from '../../../utils' const fixture = [ { - "sub_product.raw": { - "buckets": [ - {"key": "Credit reporting","doc_count": 3200}, - {"key": "Other personal consumer report","doc_count": 67}, - {"key": "Credit repair services","doc_count": 10} + 'sub_product.raw': { + buckets: [ + {key: 'Credit reporting',doc_count: 3200}, + {key: 'Other personal consumer report',doc_count: 67}, + {key: 'Credit repair services',doc_count: 10} ], }, - "key": "Credit reporting, credit repair services, or other personal consumer reports", - "doc_count": 3277 + key: 'Credit reporting, credit repair services, or other personal consumer reports', + doc_count: 3277 }, { - "sub_product.raw": { - "buckets": [ - {"key": "Conventional home mortgage","doc_count": 652}, - {"key": "Conventional fixed mortgage","doc_count": 612} + 'sub_product.raw': { + buckets: [ + {key: 'Conventional home mortgage',doc_count: 652}, + {key: 'Conventional fixed mortgage',doc_count: 612} ], }, - "key": "Mortgage", - "doc_count": 2299 + key: 'Mortgage', + doc_count: 2299 }, { - "sub_product.raw": { - "buckets": [], + 'sub_product.raw': { + buckets: [], }, - "key": "Credit reporting", - "doc_count": 1052 + key: 'Credit reporting', + doc_count: 1052 }, { - "sub_product.raw": { - "buckets": [], + 'sub_product.raw': { + buckets: [], }, - "key": "Student loan", - "doc_count": 959 + key: 'Student loan', + doc_count: 959 }, { - "sub_product.raw": { - "buckets": [], + 'sub_product.raw': { + buckets: [], }, - "key": "Credit card or prepaid card", - "doc_count": 836 + key: 'Credit card or prepaid card', + doc_count: 836 }, { - "sub_product.raw": { - "buckets": [], + 'sub_product.raw': { + buckets: [], }, - "key": "Credit card", - "doc_count": 652 + key: 'Credit card', + doc_count: 652 } ] @@ -133,4 +133,144 @@ describe('component:Product', () => { expect(actual.options[0]).toEqual(fixture[1]) }) }) -}) + + describe( 'focus logic', () => { + it( 'disable the non-focus options', () => { + const state = { + aggs: { + product: fixture + }, + query: { + focus: 'Mortgage', + lens: 'Product', + product: [ 'Mortgage' ], + tab: 'Trends' + } + } + const actual = mapStateToProps( state ) + expect( actual ).toEqual( { + options: [ { + 'sub_product.raw': { + buckets: [ { + key: 'Conventional home mortgage', + doc_count: 652, + disabled: false + }, { + key: 'Conventional fixed mortgage', + doc_count: 612, + disabled: false + } ] + }, key: 'Mortgage', doc_count: 2299, disabled: false + }, { + 'sub_product.raw': { + buckets: [ { + key: 'Credit reporting', + doc_count: 3200, + disabled: true + }, { + key: 'Other personal consumer report', + doc_count: 67, + disabled: true + }, { + key: 'Credit repair services', + doc_count: 10, + disabled: true + } ] + }, + key: 'Credit reporting, credit repair services, or other personal consumer reports', + doc_count: 3277, + disabled: true + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit reporting', + doc_count: 1052, + disabled: true + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Student loan', + doc_count: 959, + disabled: true + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit card or prepaid card', + doc_count: 836, + disabled: true + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit card', + doc_count: 652, + disabled: true + } ] + } ) + } ) + } ) + + it( 'does nothing when lens not Product', () => { + const state = { + aggs: { + product: fixture + }, + query: { + focus: 'Mortgage', + lens: 'Company', + product: [ 'Mortgage' ], + tab: 'Trends' + } + } + const actual = mapStateToProps( state ) + expect( actual ).toEqual( { + options: [ { + 'sub_product.raw': { + buckets: [ { + key: 'Conventional home mortgage', + doc_count: 652, + disabled: false + }, { + key: 'Conventional fixed mortgage', + doc_count: 612, + disabled: false + } ] + }, key: 'Mortgage', doc_count: 2299, disabled: false + }, { + 'sub_product.raw': { + buckets: [ { + key: 'Credit reporting', + doc_count: 3200, + disabled: false + }, { + key: 'Other personal consumer report', + doc_count: 67, + disabled: false + }, { + key: 'Credit repair services', + doc_count: 10, + disabled: false + } ] + }, + key: 'Credit reporting, credit repair services, or other personal consumer reports', + doc_count: 3277, + disabled: false + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit reporting', + doc_count: 1052, + disabled: false + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Student loan', + doc_count: 959, + disabled: false + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit card or prepaid card', + doc_count: 836, + disabled: false + }, { + 'sub_product.raw': { buckets: [] }, + key: 'Credit card', + doc_count: 652, + disabled: false + } ] + } ) + } ) +} ) diff --git a/src/components/Filters/__tests__/__snapshots__/AggregationBranch.spec.jsx.snap b/src/components/Filters/__tests__/__snapshots__/AggregationBranch.spec.jsx.snap index 7ccfc570c..6ac6e9807 100644 --- a/src/components/Filters/__tests__/__snapshots__/AggregationBranch.spec.jsx.snap +++ b/src/components/Filters/__tests__/__snapshots__/AggregationBranch.spec.jsx.snap @@ -28,7 +28,6 @@ exports[`component::AggregationBranch snapshots renders with all checked 1`] = `
  • +
    +
    + + + +
    + + +
    + +`; + +exports[`component::CompanyTypeahead snapshots renders without crashing 1`] = ` +
    +
    +
    + + + +
    + + +
    +
    +`; diff --git a/src/components/Filters/__tests__/__snapshots__/FederalState.spec.jsx.snap b/src/components/Filters/__tests__/__snapshots__/FederalState.spec.jsx.snap index 4e2c073d6..250c190d8 100644 --- a/src/components/Filters/__tests__/__snapshots__/FederalState.spec.jsx.snap +++ b/src/components/Filters/__tests__/__snapshots__/FederalState.spec.jsx.snap @@ -34,7 +34,7 @@ exports[`component::FederalState initial state renders empty values without cras

    - The state of the mailing address provided by the consumer + The state in the mailing address provided by the consumer

    - The state of the mailing address provided by the consumer + The state in the mailing address provided by the consumer

    Incorrect information on credit report @@ -155,7 +155,6 @@ exports[`component:Issue snapshots only shows the first five items 1`] = ` + +
    + ) + } +} + +export const mapStateToProps = state => ( { + chartType: state.trends.chartType +} ) + +export const mapDispatchToProps = dispatch => ( { + toggleChartType: chartType => { + dispatch( changeChartType( chartType ) ) + } +} ) + +export default connect( mapStateToProps, mapDispatchToProps )( ChartToggles ) diff --git a/src/components/RefineBar/ChartToggles.less b/src/components/RefineBar/ChartToggles.less new file mode 100644 index 000000000..a910cf828 --- /dev/null +++ b/src/components/RefineBar/ChartToggles.less @@ -0,0 +1,42 @@ +@import (less) "../../css/base.less"; + +.chart-toggles { + .toggle { + width: 38px; + background-color: @pacific-40; + padding: 3px; + &.selected { + background-color: @pacific-80; + font-weight: 600; + } + svg { + height: 25px; + + &#line-chart-icon{ + .cls-1 { + opacity: 0.4; + } + + .cls-2 { + fill: none; + stroke: #101820; + stroke-miterlimit: 10; + stroke-width: 2px; + } + + .cls-3 { + fill: #101820; + } + } + &#area-chart-icon { + .cls-1 { + fill: #101820; + } + + .cls-2 { + opacity: 0.4; + } + } + } + } +} diff --git a/src/components/RefineBar/DateRanges.jsx b/src/components/RefineBar/DateRanges.jsx index 72909fb48..7b4610098 100644 --- a/src/components/RefineBar/DateRanges.jsx +++ b/src/components/RefineBar/DateRanges.jsx @@ -1,13 +1,13 @@ import './DateRanges.less' import { connect } from 'react-redux' -import { dateRanges } from '../../constants'; -import { dateRangeToggled } from '../../actions/filter'; +import { dateRanges } from '../../constants' +import { dateRangeToggled } from '../../actions/filter' import React from 'react' export class DateRanges extends React.Component { _setDateRange( page ) { - this.props.toggleDateRange( page ); + this.props.toggleDateRange( page ) } _btnClassName( dateRange ) { @@ -25,8 +25,8 @@ export class DateRanges extends React.Component { { dateRanges.map( dateRange => ) }
    @@ -36,12 +36,12 @@ export class DateRanges extends React.Component { export const mapStateToProps = state => ( { dateRange: state.query.dateRange -} ); +} ) export const mapDispatchToProps = dispatch => ( { toggleDateRange: range => { dispatch( dateRangeToggled( range ) ) } -} ); +} ) export default connect( mapStateToProps, mapDispatchToProps )( DateRanges ) diff --git a/src/components/RefineBar/RefineBar.less b/src/components/RefineBar/RefineBar.less index 389e9a9e4..7f80724b7 100644 --- a/src/components/RefineBar/RefineBar.less +++ b/src/components/RefineBar/RefineBar.less @@ -6,7 +6,7 @@ p { font-size: @size-vi; font-weight: 600; - color: @gray-40; + color: @gray; } button { &.selected { diff --git a/src/components/RefineBar/Select.jsx b/src/components/RefineBar/Select.jsx index 94f435462..6c9169084 100644 --- a/src/components/RefineBar/Select.jsx +++ b/src/components/RefineBar/Select.jsx @@ -2,17 +2,51 @@ import PropTypes from 'prop-types' import React from 'react' export class Select extends React.Component { + getValues() { + // different cases Array + // handle cases where an array of single entries + // case 1: values = [1,2,4] + // case 2: values = [ + // { name: 'Foo', disabled: false}, + // { name:'bar', disabled: true } + // ] + // object key val pair + // case 3: values = { + // created_date_desc: 'Newest to oldest', + // created_date_asc: 'Oldest to newest', + // relevance_desc: 'Relevance', + // relevance_asc: 'Relevance (asc)' + // } + // array of objects + let values + + if ( Array.isArray( this.props.values ) ) { + // do nothing, case 2 + if ( this.props.values[0].hasOwnProperty( 'name' ) ) { + values = this.props.values + } else { + // case 1 + values = this.props.values.map( o => ( { + name: o, + value: o, + disabled: false + } ) ) + } + } else { + // case 3 + values = Object.keys( this.props.values ).map( o => ( { + name: this.props.values[o], + value: o, + disabled: false + } ) ) + } + return values + } + render() { const id = 'choose-' + this.props.id + const values = this.getValues() - // handle cases where an array is passed in - const values = Array.isArray( this.props.values ) ? - Object.assign( - {}, - ...this.props.values.map( value => ( { - [value]: value - } ) ) ) : - this.props.values return (
    diff --git a/src/components/RefineBar/Separator.jsx b/src/components/RefineBar/Separator.jsx index f158e99ab..85887a518 100644 --- a/src/components/RefineBar/Separator.jsx +++ b/src/components/RefineBar/Separator.jsx @@ -6,3 +6,5 @@ export class Separator extends React.Component { return } } + +export default Separator diff --git a/src/components/ResultsPanel.jsx b/src/components/ResultsPanel.jsx index b356c3988..dcb9e00e2 100644 --- a/src/components/ResultsPanel.jsx +++ b/src/components/ResultsPanel.jsx @@ -1,4 +1,4 @@ -import { MODE_LIST, MODE_MAP } from '../constants' +import { MODE_LIST, MODE_MAP, MODE_TRENDS } from '../constants' import { printModeOff, printModeOn } from '../actions/view' import { connect } from 'react-redux' import ListPanel from './List/ListPanel' @@ -6,6 +6,7 @@ import MapPanel from './Map/MapPanel' import PrintInfo from './Print/PrintInfo' import PrintInfoFooter from './Print/PrintInfoFooter' import React from 'react' +import TrendsPanel from './Trends/TrendsPanel' export class ResultsPanel extends React.Component { constructor( props ) { @@ -29,14 +30,17 @@ export class ResultsPanel extends React.Component { const classes = [ 'content_main', this.props.tab.toLowerCase() ] return classes.join( ' ' ) } - /* eslint complexity: ["error", 5] */ + /* eslint complexity: ["error", 6] */ render() { let currentPanel switch ( this.props.tab ) { case MODE_LIST: currentPanel = - break; + break + case MODE_TRENDS: + currentPanel = + break case MODE_MAP: default: currentPanel = diff --git a/src/components/TabbedNavigation.jsx b/src/components/TabbedNavigation.jsx index bfa31de9a..fdb858bc5 100644 --- a/src/components/TabbedNavigation.jsx +++ b/src/components/TabbedNavigation.jsx @@ -4,15 +4,7 @@ import React from 'react' import { tabChanged } from '../actions/view' export class TabbedNavigation extends React.Component { - constructor( props ) { - super( props ); - this.state = { - tab: props.tab - }; - } - _setTab( tab ) { - window.scrollTo( 0, 0 ) this.props.onTab( tab ); } @@ -31,6 +23,15 @@ export class TabbedNavigation extends React.Component { Map + { + this.props.showTrends && + + } + +
    +
    +

    { focus }

    + +

    { total } Complaints

    +
    +
    + + + } +} + + +export const mapDispatchToProps = dispatch => ( { + clearFocus: () => { + dispatch( removeFocus() ) + } +} ) + +export const mapStateToProps = state => ( { + focus: state.query.focus, + lens: state.query.lens, + total: state.trends.total.toLocaleString() +} ) + + +export default connect( mapStateToProps, mapDispatchToProps )( FocusHeader ) diff --git a/src/components/Trends/FocusHeader.less b/src/components/Trends/FocusHeader.less new file mode 100644 index 000000000..f69e99c73 --- /dev/null +++ b/src/components/Trends/FocusHeader.less @@ -0,0 +1,23 @@ +@import (less) "../../css/base.less"; +@import (less) "../TabbedNavigation.less"; + +.focus-header { + + .clear-focus { + margin: @gutter-normal; + .cf-icon-svg { + margin-right: 5px; + } + } + + .focus { + text-align: center; + margin-left: ~"calc(10%)"; + .divider { + height: 5px; + width: 75px; + background-color: @green-60; + display: inline-block; + } + } +} diff --git a/src/components/Trends/LensTabs.jsx b/src/components/Trends/LensTabs.jsx new file mode 100644 index 000000000..7545d9c6d --- /dev/null +++ b/src/components/Trends/LensTabs.jsx @@ -0,0 +1,75 @@ +import './LensTabs.less' +import { changeDataSubLens } from '../../actions/trends' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import React from 'react' + +const lensMaps = { + Company: { + tab1: { displayName: 'Products', filterName: 'product' } + }, + Product: { + tab1: { displayName: 'Sub-products', filterName: 'sub_product' }, + tab2: { displayName: 'Issues', filterName: 'issue' } + } +} + +export class LensTabs extends React.Component { + _setTab( tab ) { + this.props.onTab( tab ) + } + + _getTabClass( tab ) { + tab = tab.toLowerCase() + const classes = [ 'tab', tab ] + const regex = new RegExp( this.props.subLens.toLowerCase(), 'g' ) + if ( tab.replace( '-', '_' ).match( regex ) ) { + classes.push( 'active' ) + } + return classes.join( ' ' ) + } + + render() { + const { lens } = this.props + if ( lens === 'Overview' ) { + return null + } + + return ( +
    +
    + + { lensMaps[lens].tab2 && + + } +
    +
    + ) + } +} + +export const mapStateToProps = state => ( { + lens: state.query.lens, + subLens: state.query.subLens +} ) + +export const mapDispatchToProps = dispatch => ( { + onTab: tab => { + dispatch( changeDataSubLens( tab.toLowerCase() ) ) + } +} ) + +LensTabs.propTypes = { + showTitle: PropTypes.bool.isRequired +} + + +export default connect( mapStateToProps, mapDispatchToProps )( LensTabs ) diff --git a/src/components/Trends/LensTabs.less b/src/components/Trends/LensTabs.less new file mode 100644 index 000000000..4d5303eb8 --- /dev/null +++ b/src/components/Trends/LensTabs.less @@ -0,0 +1,18 @@ +@import (less) "../../css/base.less"; +@import (less) "../TabbedNavigation.less"; + +.tabbed-navigation { + &.lens { + background: none; + border-bottom: 1px solid @gray-40; + .tab { + font-size: medium; + &.active { + background: @white; + } + &:not(.active) { + background: @pacific-20; + } + } + } +} diff --git a/src/components/Trends/TrendDepthToggle.jsx b/src/components/Trends/TrendDepthToggle.jsx new file mode 100644 index 000000000..af1e46394 --- /dev/null +++ b/src/components/Trends/TrendDepthToggle.jsx @@ -0,0 +1,93 @@ +/* eslint complexity: ["error", 5] */ +import './TrendDepthToggle.less' +import { changeDepth, resetDepth } from '../../actions/trends' +import { clamp, coalesce } from '../../utils' +import { connect } from 'react-redux' +import React from 'react' +import { SLUG_SEPARATOR } from '../../constants' + +const maxRows = 5 + +export class TrendDepthToggle extends React.Component { + + _showMore() { + const { queryCount, resultCount } = this.props + // scenarios where we want to show more: + // you have less visible rows that the max (5) + if ( resultCount <= maxRows ) { + return true + } + // or more filters count > max Rows and they aren't the same (visible) + return queryCount > maxRows && queryCount !== resultCount + } + + render() { + const { diff, increaseDepth, depthReset, showToggle } = this.props + if ( showToggle ) { + if ( this._showMore() ) { + return
    + +
    + } + return
    + +
    + } + return null + } +} + + +export const mapDispatchToProps = dispatch => ( { + increaseDepth: diff => { + dispatch( changeDepth( diff + 5 ) ) + }, + depthReset: () => { + dispatch( resetDepth() ) + } +} ) + +export const mapStateToProps = state => { + const { aggs, query, trends } = state + const { focus, lens } = query + const lensKey = lens.toLowerCase() + const resultCount = coalesce( trends.results, lensKey, [] ) + .filter( o => o.visible && o.isParent ).length + + // The total source depends on the lens. There are no aggs for companies + let totalResultsLength = 0 + if ( lensKey === 'product' ) { + totalResultsLength = coalesce( aggs, lensKey, [] ).length + } else { + totalResultsLength = clamp( coalesce( query, lensKey, [] ).length, 0, 10 ) + } + + // handle cases where some specified filters are selected + const queryCount = query[lensKey] ? query[lensKey] + .filter( o => o.indexOf( SLUG_SEPARATOR ) === -1 ).length : + totalResultsLength + + return { + diff: totalResultsLength - resultCount, + resultCount, + queryCount, + showToggle: !focus && ( resultCount > 5 || queryCount > 5 ) + } +} + +export default connect( mapStateToProps, + mapDispatchToProps )( TrendDepthToggle ) diff --git a/src/components/Trends/TrendDepthToggle.less b/src/components/Trends/TrendDepthToggle.less new file mode 100644 index 000000000..26a79bbf9 --- /dev/null +++ b/src/components/Trends/TrendDepthToggle.less @@ -0,0 +1,24 @@ +@import (less) "../../css/base.less"; +@import (less) "../TabbedNavigation.less"; + +.trend-depth-toggle { + background: @gray-10; + text-align: center; + + button { + margin: 5px; + } + + span { + font-weight: bold; + margin-right: 5px; + &.plus::before { + content: "+"; + } + + &.minus::before { + content: "-"; + } + } + +} diff --git a/src/components/Trends/TrendsPanel.jsx b/src/components/Trends/TrendsPanel.jsx new file mode 100644 index 000000000..f3a42aebb --- /dev/null +++ b/src/components/Trends/TrendsPanel.jsx @@ -0,0 +1,274 @@ +/* eslint-disable complexity, camelcase */ +import '../RefineBar/RefineBar.less' +import './TrendsPanel.less' + +import { changeChartType, changeDataLens } from '../../actions/trends' +import { getIntervals, showCompanyOverLay } from '../../utils/trends' +import ActionBar from '../ActionBar' +import { changeDateInterval } from '../../actions/filter' +import ChartToggles from '../RefineBar/ChartToggles' +import CompanyTypeahead from '../Filters/CompanyTypeahead' +import { connect } from 'react-redux' +import DateRanges from '../RefineBar/DateRanges' +import ExternalTooltip from './ExternalTooltip' +import FilterPanel from '../Filters/FilterPanel' +import FilterPanelToggle from '../Filters/FilterPanelToggle' +import FocusHeader from './FocusHeader' +import { formatDateView } from '../../utils/formatDate' +import LensTabs from './LensTabs' +import LineChart from '../Charts/LineChart' +import Loading from '../Dialogs/Loading' +import { processRows } from '../../utils/chart' +import React from 'react' +import RowChart from '../Charts/RowChart' +import Select from '../RefineBar/Select' +import Separator from '../RefineBar/Separator' +import StackedAreaChart from '../Charts/StackedAreaChart' +import TrendDepthToggle from './TrendDepthToggle' +import { trendsDateWarningDismissed } from '../../actions/view' +import Warning from '../Warnings/Warning' + +const WARNING_MESSAGE = '“Day” interval is disabled when the date range is' + + ' longer than one year' + +const lenses = [ 'Overview', 'Company', 'Product' ] +const subLensMap = { + sub_product: 'Sub-products', + sub_issue: 'Sub-issues', + issue: 'Issues', + product: 'Products' +} + +const lensHelperTextMap = { + product: 'Product the consumer identified in the complaint.' + + ' Click on a company name to expand products.', + company: 'Product the consumer identified in the complaint. Click on' + + ' a company name to expand products.', + sub_product: 'Product and sub-product the consumer identified in the ' + + ' complaint. Click on a product to expand sub-products.', + issue: 'Product and issue the consumer identified in the complaint.' + + ' Click on a product to expand issues.', + overview: 'Product the consumer identified in the complaint. Click on a ' + + ' product to expand sub-products' +} + +const focusHelperTextMap = { + sub_product: 'Sub-products the consumer identified in the complaint', + product: 'Product the consumer identified in the complaint', + issue: 'Issues the consumer identified in the complaint' +} + +export class TrendsPanel extends React.Component { + _areaChartTitle() { + const { focus, overview, subLens } = this.props + if ( overview ) { + return 'Complaints by date received by the CFPB' + } else if ( focus ) { + return 'Complaints by ' + subLensMap[subLens].toLowerCase() + + ', by date received by the CFPB' + } + return 'Complaints by date received by the CFPB' + } + + _className() { + const classes = [ 'trends-panel' ] + if ( !this.props.overview ) { + classes.push( 'external-tooltip' ) + } + return classes.join( ' ' ) + } + + _phaseMap() { + const { + companyOverlay, dataLensData, focusData, focusHelperText, overview, lens, + lensHelperText, minDate, maxDate, productData, subLensTitle, total + } = this.props + + if ( companyOverlay ) { + return null + } + + if ( overview ) { + return + } + + if ( this.props.focus ) { + return + } + + return [ + , + + ] + } + + render() { + const { + chartType, companyOverlay, dateInterval, focus, intervals, + isLoading, lens, + onInterval, onLens, overview, showMobileFilters, total, + trendsDateWarningEnabled + } = this.props + return ( +
    + + { trendsDateWarningEnabled && + } + { showMobileFilters && } +
    + + + + { !overview && [ + , + + ] } +
    + + { companyOverlay && +
    +
    +

    Choose a company to start your visualization + using the type-ahead menu below. You can add more than + one company to your view +

    + +
    +
    + } + + { focus && } + + { !companyOverlay && total > 0 && +
    +
    +

    {this._areaChartTitle()}

    +

    A time series graph of the + (up to five) highest volume complaints for the selected date + range. However, you can view all of your selections in the + bar chart, below. Hover on the chart to see the count for + each date interval. Your filter selections will update + what you see on the graph. +

    +
    +
    + } + + { !companyOverlay && total > 0 && +
    +
    + { chartType === 'line' && + } + { chartType === 'area' && + } +
    + { !overview && } +
    + } + { total > 0 && this._phaseMap() } + + +
    + ) + } +} + +const mapStateToProps = state => { + const { query, trends } = state + const { + company: companyFilters, + dateInterval, + date_received_max, + date_received_min, + lens, + subLens, + trendsDateWarningEnabled + } = query + + const { + chartType, colorMap, focus, isLoading, results, total + } = trends + + const lensKey = lens.toLowerCase() + const focusKey = subLens.replace( '_', '-' ) + const lensHelperText = subLens === '' ? + lensHelperTextMap[lensKey] : lensHelperTextMap[subLens] + const focusHelperText = subLens === '' ? + focusHelperTextMap[lensKey] : focusHelperTextMap[subLens] + + const minDate = formatDateView( date_received_min ) + const maxDate = formatDateView( date_received_max ) + + return { + chartType, + companyData: processRows( results.company, false, lens ), + companyOverlay: showCompanyOverLay( lens, companyFilters, isLoading ), + dateInterval, + focus, + focusData: processRows( results[focusKey], colorMap, lens ), + intervals: getIntervals( date_received_min, date_received_max ), + isLoading, + productData: processRows( results.product, false, lens ), + dataLensData: processRows( results[lensKey], colorMap, lens ), + lens, + maxDate, + minDate, + overview: lens === 'Overview', + showMobileFilters: state.view.width < 750, + subLens, + subLensTitle: subLensMap[subLens] + ', by ' + lens.toLowerCase() + ' from', + lensHelperText: lensHelperText, + focusHelperText: focusHelperText, + total, + trendsDateWarningEnabled + } +} + +export const mapDispatchToProps = dispatch => ( { + onChartType: ev => { + dispatch( changeChartType( ev.target.value ) ) + }, + onDismissWarning: () => { + dispatch( trendsDateWarningDismissed() ) + }, + onInterval: ev => { + dispatch( changeDateInterval( ev.target.value ) ) + }, + onLens: ev => { + dispatch( changeDataLens( ev.target.value ) ) + } + +} ) + +export default connect( mapStateToProps, mapDispatchToProps )( TrendsPanel ) diff --git a/src/components/Trends/TrendsPanel.less b/src/components/Trends/TrendsPanel.less new file mode 100644 index 000000000..54ad640ee --- /dev/null +++ b/src/components/Trends/TrendsPanel.less @@ -0,0 +1,297 @@ +@import (less) "../../css/base.less"; + +.trends-panel { + .refine-bar { + .separator { + display: inline-block; + } + } + + .company-overlay { + justify-content: center; + .company-search { + margin: @gutter-wide; + .typeahead { + width: 100%; + } + } + } + + .chart { + width: 100%; + } + &.external-tooltip { + + section { + &.chart { + width: 70%; + } + &.tooltip-container { + width: 25%; + &.legend { + margin-top:20px; + .tooltip-ul { + border-bottom: none; + } + } + padding: 0 1%; + position: relative; + + .scrollable { + max-height: 300px; + overflow: hidden; + overflow-y: auto; + border-bottom: solid 1px @black; + ul.tooltip-ul { + cursor: pointer; + color: @pacific; + border-bottom-color: @gray-10; + li { + &:before { + opacity: 0; + } + } + } + } + p.a-micro-copy { + display: inline-block; + width: 100%; + border-bottom: solid 1px @block__border-bottom; + padding-top: 5px; + margin-bottom: 0; + font-weight: 600; + font-size: 12px; + + span { + &.heading { + color: @gray-80; + } + &.date { + float: right; + } + + } + } + + ul.tooltip-ul { + list-style: none; + &:extend(.m-list__unstyled); + margin-bottom: 0; + border-bottom: solid 1px @block__border-bottom; + padding-bottom: 2px; + padding-top: 2px; + padding-left: 0; + &.recommended { + background: rgba(231,232,233, .4); + } + &.active { + color: @black; + &.color__23 { + background-color: @purple-20; + } + &.color__24 { + background-color: @red-20; + } + &.color__25 { + background-color: @gold-20; + } + li { + &:before { + opacity: 1; + } + } + } + + li { + span { + border: none; + &.u-left { + display: inline-block; + text-align: left; + width: 70%; + } + &.u-right { + &.close { + padding-left: 10px; + } + } + } + margin-bottom: 0; + padding-left: 14px; + position: relative; + border-bottom: 1px solid @gray-20; + &:before { + height: 8px; + width: 8px; + border-radius: 50%; + content: ''; + position: absolute; + left: 0; + top: 4px; + display: block; + } + + &.color__0:before { + background-color: #2cb34a; + } + + &.color__1:before { + background-color: #addc91; + } + + &.color__2:before { + background-color: #257675; + } + + &.color__3:before { + background-color: #9ec4c3; + } + + &.color__4:before { + background-color: #0072ce; + } + + &.color__5:before { + background-color: #96c4ed; + } + + &.color__6:before { + background-color: #254b87; + } + + &.color__7:before { + background-color: #9daecc; + } + + &.color__8:before { + background-color: #b4267a; + } + + &.color__9:before { + background-color: #dc9cbf; + } + + &.color__10:before { + background-color: #a2a3a4; + } + + &.color__12:before { + background-color: #93cf7c; + } + + &.color__13:before { + background-color: @purple-60; + } + + &.color__14:before { + background-color: @red-60; + } + + &.color__15:before { + background-color: @gold-80; + } + } + li:last-child { + border: none; + } + .dot { + height: 8px; + width: 8px; + background-color: red; + border-radius: 50%; + display: inline-block; + margin-right: 5px; + } + font-size: 12px; + font-weight: 500; + &.total { + font-size: 16px; + border-bottom: none; + li { + .u-left { + width: 50%; + } + } + } + } + + .tooltip-button-panel { + .reset-set { + > button { + width: 100%; + + span.pull-left { + display: none; + } + &:before { + content: "Reset recommended set"; + border-right: solid 1px @white; + padding-right: 10px; + } + > .caret { + border-top-color: @white; + border-right-color: @white; + border-style: solid; + border-width: 2px 2px 0 0; + height: 8px; + right: -10px; + position: relative; + top: 0.15em; + vertical-align: top; + width: 8px; + display: inline-block; + transform: rotate(135deg); + } + } + } + } + } + + &.tooltip-container:not(.focus) { + ul.tooltip-ul { + .u-left.a-btn__link { + text-decoration: underline; + } + } + } + } + } + + h2.area-chart-title { + padding: @gutter-normal; + margin-bottom: 0; + } + + .chart-helper-text { + padding-left: @gutter-normal; + padding-right: @gutter-normal; + padding-bottom: @gutter-normal; + margin-bottom: @gutter-normal; + } + + + @media @phone { + .refine-bar { + .cf-select { + width: 40%; + } + } + } + + @media @phone, @tablet { + .chart { + width: 100%; + } + &.external-tooltip { + section { + &.chart { + width: 100%; + } + + &.tooltip-container { + width: 100%; + } + } + } + } +} diff --git a/src/components/Typeahead/index.jsx b/src/components/Typeahead/index.jsx index a0b42132e..e9ce5b58a 100644 --- a/src/components/Typeahead/index.jsx +++ b/src/components/Typeahead/index.jsx @@ -156,6 +156,7 @@ export default class Typeahead extends React.Component { ( { - params: { - ...state.query, - dataNormalization: state.map.dataNormalization +export const mapStateToProps = state => { + const { map, query, trends } = state + + const expandedTrends = [ + ...new Set( [ + ...map.expandedTrends, + ...trends.expandedTrends + ] ) + ] + + return { + params: { + ...query, + expandedTrends, + dataNormalization: map.dataNormalization + } } -} ) +} export const mapDispatchToProps = dispatch => ( { onUrlChanged: location => { diff --git a/src/components/__tests__/ChartToggles.spec.jsx b/src/components/__tests__/ChartToggles.spec.jsx new file mode 100644 index 000000000..1eecdfcb6 --- /dev/null +++ b/src/components/__tests__/ChartToggles.spec.jsx @@ -0,0 +1,78 @@ +import configureMockStore from 'redux-mock-store' +import { + mapDispatchToProps, mapStateToProps, ChartToggles +} from '../RefineBar/ChartToggles' +import { Provider } from 'react-redux' +import React from 'react' +import renderer from 'react-test-renderer' +import { shallow } from 'enzyme' +import thunk from 'redux-thunk' +import { changeChartType } from '../../actions/trends' + +function setupSnapshot() { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( {} ) + + return renderer.create( + + + + ) +} + +describe( 'component: ChartToggles', () => { + describe( 'initial state', () => { + it( 'renders without crashing', () => { + const target = setupSnapshot() + let tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + } ) + + describe( 'buttons', () => { + let cb = null + let target = null + + beforeEach( () => { + cb = jest.fn() + + target = shallow( ) + } ) + + it( 'Line - changeChartType is called the button is clicked', () => { + const prev = target.find( '.chart-toggles .line' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith( 'line' ) + } ) + + it( 'Area - changeChartType is called the button is clicked', () => { + const prev = target.find( '.chart-toggles .area' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith( 'area' ) + } ) + } ) + + + describe( 'mapDispatchToProps', () => { + it( 'provides a way to call changeChartType', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).toggleChartType() + expect( dispatch.mock.calls.length ).toEqual( 1 ) + } ) + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + trends: { + chartType: 'foo' + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { chartType: 'foo' } ) + } ) + } ) + + +} ) diff --git a/src/components/__tests__/ExternalTooltip.spec.jsx b/src/components/__tests__/ExternalTooltip.spec.jsx new file mode 100644 index 000000000..ea847e2fd --- /dev/null +++ b/src/components/__tests__/ExternalTooltip.spec.jsx @@ -0,0 +1,198 @@ +import * as trendsUtils from '../../utils/trends' +import ReduxExternalTooltip, { + ExternalTooltip, + mapDispatchToProps, + mapStateToProps +} from '../Trends/ExternalTooltip' +import React from 'react' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import renderer from 'react-test-renderer' +import { shallow } from 'enzyme' + + +function setupSnapshot( query, tooltip ) { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + + const store = mockStore( { + query, + trends: { + chartType: 'area', + tooltip + } + } ) + + return renderer.create( + + + + ) +} + +describe( 'initial state', () => { + let query, tooltip + beforeEach(()=>{ + query = { + focus: '', + lens: '' + } + tooltip = { + title: 'Date Range: 1/1/1900 - 1/1/2000', + total: 2900, + values: [ + { colorIndex: 1, name: 'foo', value: 1000 }, + { colorIndex: 2, name: 'bar', value: 1000 }, + { colorIndex: 3, name: 'All other', value: 900 }, + { colorIndex: 4, name: "Eat at Joe's", value: 1000 } + ] + } + }) + it( 'renders without crashing', () => { + const target = setupSnapshot( query, tooltip ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders nothing without crashing', () => { + const target = setupSnapshot( query, false ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders Company typehead without crashing', () => { + query.lens = 'Company' + const target = setupSnapshot( query, tooltip ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders "Other" without crashing', () => { + tooltip.values.push( { colorIndex: 5, name: 'Other', value: 900 } ) + const target = setupSnapshot( query, tooltip ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders focus without crashing', () => { + query.focus = 'foobar' + const target = setupSnapshot( query, tooltip ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + +} ) + +describe( 'buttons', () => { + let cb = null + let cbFocus + let target = null + + beforeEach( () => { + cb = jest.fn() + cbFocus = jest.fn() + target = shallow( ) + } ) + + it( 'remove is called the button is clicked', () => { + const prev = target.find( '.tooltip-ul .color__1 .close' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith( 'foo' ) + } ) + +} ) + +describe( 'mapDispatchToProps', () => { + it( 'provides a way to call remove', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).remove( 'Foo' ) + expect( dispatch.mock.calls ).toEqual( [ [ { + filterName: 'company', + filterValue: 'Foo', + requery: 'REQUERY_ALWAYS', + type: 'FILTER_REMOVED' + } ] ] ) + expect( trendsUtils.scrollToFocus ).not.toHaveBeenCalled() + } ) +} ) + + +describe( 'mapStateToProps', () => { + let state + beforeEach(()=>{ + state = { + query: { + focus: '', + lens: 'Overview' + }, + trends: { + tooltip: { + title: 'Date: 1/1/2015', + total: 100, + values: [] + } + } + } + }) + it( 'maps state and props', () => { + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + focus: '', + lens: 'Overview', + showCompanyTypeahead: false, + showTotal: false, + tooltip: { + date: '1/1/2015', + heading: 'Date:', + title: 'Date: 1/1/2015', + total: 100, + values: [] + } + } ) + } ) + + it( 'maps state and props - focus', () => { + state.query.focus = 'something else' + let actual = mapStateToProps( state ) + expect( actual.focus ).toEqual( 'focus' ) + } ) + + it( 'handles broken tooltip title', () => { + state.trends.tooltip.title = 'something else' + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + focus: '', + lens: 'Overview', + showCompanyTypeahead: false, + showTotal: false, + tooltip: { + date: '', + heading: 'something else:', + title: 'something else', + total: 100, + values: [] + } + } ) + } ) +} ) diff --git a/src/components/__tests__/FocusHeader.spec.jsx b/src/components/__tests__/FocusHeader.spec.jsx new file mode 100644 index 000000000..2f072b878 --- /dev/null +++ b/src/components/__tests__/FocusHeader.spec.jsx @@ -0,0 +1,99 @@ +import configureMockStore from 'redux-mock-store' +import { IntlProvider } from 'react-intl' +import { Provider } from 'react-redux' +import ReduxFocusHeader, { + FocusHeader, + mapDispatchToProps, + mapStateToProps +} from '../Trends/FocusHeader' +import React from 'react' +import renderer from 'react-test-renderer' +import { REQUERY_ALWAYS } from '../../constants' +import thunk from 'redux-thunk' +import { shallow } from 'enzyme' + +function setupSnapshot() { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( { + query: { + focus: 'Foo Bar', + lens: 'Product', + subLens: 'sub_product' + }, + trends: { + total: 90120 + } + } ) + + return renderer.create( + + + + + + ) +} + +describe( 'component:FocusHeader', () => { + it( 'renders without crashing', () => { + const target = setupSnapshot() + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + describe( 'buttons', () => { + let cb = null + let target = null + + beforeEach( () => { + cb = jest.fn() + target = shallow( ) + } ) + + it( 'changeFocus is called when the button is clicked', () => { + const prev = target.find( '#clear-focus' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalled() + } ) + + } ) + + describe( 'mapDispatchToProps', () => { + it( 'hooks into removeFocus', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).clearFocus() + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: REQUERY_ALWAYS, + type: 'FOCUS_REMOVED' + } ] + ] ) + } ) + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + query: { + focus: 'Foo', + lens: 'Bar' + }, + trends: { + total: 1000 + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + focus: 'Foo', + lens: 'Bar', + total: '1,000' + } ) + } ) + } ) + +} ) diff --git a/src/components/__tests__/LensTabs.spec.jsx b/src/components/__tests__/LensTabs.spec.jsx new file mode 100644 index 000000000..d3ae1d33c --- /dev/null +++ b/src/components/__tests__/LensTabs.spec.jsx @@ -0,0 +1,99 @@ +import configureMockStore from 'redux-mock-store' +import { IntlProvider } from 'react-intl' +import { Provider } from 'react-redux' +import ReduxLensTabs, { + LensTabs, + mapDispatchToProps, + mapStateToProps +} from '../Trends/LensTabs' +import React from 'react' +import renderer from 'react-test-renderer' +import { REQUERY_ALWAYS } from '../../constants' +import thunk from 'redux-thunk' +import { shallow } from 'enzyme' + +function setupSnapshot( lens ) { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( { + query: { + lens, + subLens: 'sub_product' + } + } ) + + return renderer.create( + + + + + + ) +} + +describe( 'component:LensTabs', () => { + it( 'does not render when Overview', () => { + const target = setupSnapshot( 'Overview' ) + const tree = target.toJSON() + expect( tree ).toBeNull() + } ) + + it( 'renders Product without crashing', () => { + const target = setupSnapshot( 'Product' ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + describe( 'buttons', () => { + let cb = null + let target = null + + beforeEach( () => { + cb = jest.fn() + target = shallow( ) + } ) + + it( 'tabChanged is called with Product when the button is clicked', () => { + const prev = target.find( '.tabbed-navigation button.sub_product' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith( 'sub_product' ) + } ) + + it( 'tabChanged is called with Issue when the button is clicked', () => { + const prev = target.find( '.tabbed-navigation button.issue' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith( 'issue' ) + } ) + } ) + + describe( 'mapDispatchToProps', () => { + it( 'hooks into changeDataSubLens', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).onTab( 'What' ) + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: REQUERY_ALWAYS, + subLens: 'what', + type: 'DATA_SUBLENS_CHANGED' + } ] + ] ) + } ) + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + query: { + lens: 'foo', + subLens: 'bar' + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { lens: 'foo', subLens: 'bar' } ) + } ) + } ) + +} ) diff --git a/src/components/__tests__/LineChart.spec.jsx b/src/components/__tests__/LineChart.spec.jsx new file mode 100644 index 000000000..17690b9ba --- /dev/null +++ b/src/components/__tests__/LineChart.spec.jsx @@ -0,0 +1,390 @@ +import configureMockStore from 'redux-mock-store' +import { + mapDispatchToProps, + mapStateToProps, + LineChart +} from '../Charts/LineChart' +import { mount, shallow } from 'enzyme' +import { Provider } from 'react-redux' +import React from 'react' +import renderer from 'react-test-renderer' +import thunk from 'redux-thunk' + +// this is how you override and mock an imported constructor +jest.mock( 'britecharts', () => { + const props = [ + 'line', 'margin', 'backgroundColor', 'colorSchema', 'enableLabels', + 'labelsSize', 'labelsTotalCount', 'labelsNumberFormat', 'outerPadding', + 'percentageAxisToMaxRatio', 'yAxisLineWrapLimit', 'grid', 'dateLabel', + 'initializeVerticalMarker', 'yAxisPaddingBetweenChart', 'width', 'on', + 'wrapLabels', 'height', 'isAnimated', 'tooltipThreshold', 'aspectRatio', + // tooltip specifics + 'tooltip', 'title', 'update', 'shouldShowDateInTitle', 'topicLabel' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + return mock +} ) + +jest.mock( 'd3', () => { + const props = [ + 'select', 'each', 'node', 'getBoundingClientRect', 'width', 'datum', 'call', + 'remove', 'selectAll' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + // set narrow width value for 100% test coverage + mock.width = 100 + + return mock +} ) + +function setupSnapshot(lens) { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( {} ) + + const data = { + dataByTopic: [ { + topic: 'Complaints', + topicName: 'Complaints', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 29068 }, + { date: '2020-04-01T00:00:00.000Z', value: 35112 }, + { date: '2020-05-01T00:00:00.000Z', value: 9821 } + ] + } ] + } + const colorMap = { Complaints: '#ADDC91', 'Other': '#a2a3a4' } + + return renderer.create( + + + + ) +} + +describe( 'component: LineChart', () => { + describe( 'initial state', () => { + it( 'renders without crashing', () => { + const target = setupSnapshot( 'Overview' ) + let tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders data lens without crashing', () => { + const target = setupSnapshot( 'Issue' ) + let tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + } ) + + describe( 'componentDidUpdate', () => { + let mapDiv + const lastDate = '2020-05-01T00:00:00.000Z' + const colorMap = { + 'Credit reporting': '#2cb34a', + 'Debt collection': '#addc91', + 'Credit card or prepaid card': '#257675', + Mortgage: '#9ec4c3', + 'Checking or savings account': '#0072ce', + Complaints: '#ADDC91', + 'All other products': '#a2a3a4' + } + const data = { + dataByTopic: [ + { + topic: 'Credit reporting', + topicName: 'Credit reporting', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 17231 }, + { date: '2020-04-01T00:00:00.000Z', value: 21179 }, + { date: '2020-05-01T00:00:00.000Z', value: 6868 } + ] + }, + { + topic: 'Debt collection', + topicName: 'Debt collection', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 4206 }, + { date: '2020-04-01T00:00:00.000Z', value: 4508 }, + { date: '2020-05-01T00:00:00.000Z', value: 1068 } + ] + }, + { + topic: 'Credit card or prepaid card', + topicName: 'Credit card or prepaid card', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 2435 }, + { date: '2020-04-01T00:00:00.000Z', value: 3137 }, + { date: '2020-05-01T00:00:00.000Z', value: 712 } + ] + }, + { + topic: 'Mortgage', + topicName: 'Mortgage', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 2132 }, + { date: '2020-04-01T00:00:00.000Z', value: 2179 }, + { date: '2020-05-01T00:00:00.000Z', value: 365 } ] + }, + { + topic: 'Checking or savings account', + topicName: 'Checking or savings account', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 1688 }, + { date: '2020-04-01T00:00:00.000Z', value: 2030 }, + { date: '2020-05-01T00:00:00.000Z', value: 383 } ] + } ] + } + beforeEach( () => { + mapDiv = document.createElement( 'div' ) + mapDiv.setAttribute( 'id', 'line-chart-foo' ) + window.domNode = mapDiv + document.body.appendChild( mapDiv ) + } ) + + afterEach( () => { + const div = document.getElementById( 'line-chart-foo' ) + if ( div ) { + document.body.removeChild( div ) + } + jest.clearAllMocks() + } ) + + it( 'does nothing when no data', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + target.setProps( { data: [] } ) + expect( target._redrawChart ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'trigger a new update when data changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + const newData = { + dataByTopic: [ + { + topic: 'Mortgage', + topicName: 'Mortgage', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 2132 }, + { date: '2020-04-01T00:00:00.000Z', value: 2179 }, + { date: '2020-05-01T00:00:00.000Z', value: 365 } ] + }, + { + topic: 'Checking or savings account', + topicName: 'Checking or savings account', + dashed: false, + show: true, + dates: [ + { date: '2020-03-01T00:00:00.000Z', value: 1688 }, + { date: '2020-04-01T00:00:00.000Z', value: 2030 }, + { date: '2020-05-01T00:00:00.000Z', value: 383 } ] + } ] + } + + target.setProps( { data: newData } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + + it( 'trigger a new update when printMode changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { printMode: true } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + + it( 'trigger a new update when width changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { width: 600 } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + } ) + + describe( 'mapDispatchToProps', () => { + it( 'provides a way to call updateTrendsTooltip', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).tooltipUpdated( 'foo' ) + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: 'REQUERY_NEVER', + type: 'TRENDS_TOOLTIP_CHANGED', + value: 'foo' + } ] + ] ) + } ) + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + query: { + dateInterval: 'Month', + date_received_min: '', + date_received_max: '', + lens: 'Overview' + }, + trends: { + colorMap: {}, + results: { + dateRangeLine: [] + }, + tooltip: false + }, + view: { + printMode: false, + width: 1000 + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + colorMap: {}, + data: [], + interval: 'Month', + dateRange: { + from: '', + to: '' + }, + lens: 'Overview', + printMode: false, + tooltip: false, + width: 1000 + } ) + } ) + } ) + + describe( 'tooltip events', () => { + let target, cb + + beforeEach( () => { + cb = jest.fn() + } ) + + it( 'updates external tooltip with different data', () => { + const data = { + dataByTopic: [] + } + target = shallow( ) + const instance = target.instance() + instance._updateTooltip( { date: '2012', value: 2000 } ) + expect( cb ).toHaveBeenCalledTimes( 1 ) + } ) + + it( 'does not external tooltip with same data', () => { + const data = { + dataByTopic: [] + } + target = shallow( ) + const instance = target.instance() + instance._updateTooltip( { date: '2000', value: 100 } ) + expect( cb ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'internal tooltip', () => { + const data = { + dataByTopic: [] + } + target = shallow( ) + const instance = target.instance() + instance.tip = { + title: jest.fn(), + update: jest.fn() + } + instance._updateInternalTooltip( { date: '2012', value: 200 }, {}, 0 ) + expect( instance.tip.title ).toHaveBeenCalledTimes( 1 ) + } ) + } ) + + describe( 'helpers', () => { + describe( '_chartWidth', () => { + it( 'gets print width - Overview', () => { + const data = { + dataByTopic: [] + } + const target = shallow( ) + expect( target.instance()._chartWidth( '#foo' ) ) + .toEqual( 750 ) + } ) + } ) + } ) + +} ) diff --git a/src/components/__tests__/MapPanel.spec.jsx b/src/components/__tests__/MapPanel.spec.jsx index 567c98149..fbd5ef2c3 100644 --- a/src/components/__tests__/MapPanel.spec.jsx +++ b/src/components/__tests__/MapPanel.spec.jsx @@ -1,13 +1,13 @@ import configureMockStore from 'redux-mock-store' import { IntlProvider } from 'react-intl' import { Provider } from 'react-redux' -import { MapPanel, mapDispatchToProps } from '../Map/MapPanel' -import { MODE_MAP } from '../../constants' +import ReduxMapPanel, { MapPanel, mapDispatchToProps } from '../Map/MapPanel' import React from 'react' import renderer from 'react-test-renderer' import thunk from 'redux-thunk' +import { MODE_MAP } from '../../constants' -function setupSnapshot( printMode ) { +function setupSnapshot( { enablePer1000, printMode } ) { const items = [ { key: 'CA', doc_count: 62519 }, { key: 'FL', doc_count: 47358 } @@ -21,48 +21,54 @@ function setupSnapshot( printMode ) { total: items.length }, map: { - product: [], - state: [] + error: false, + results: { + issue: [], + product: [], + state: [] + } }, query: { - enablePer1000: false, - from: 0, + date_received_min: new Date('7/10/2017'), + date_received_max: new Date('7/10/2020'), + enablePer1000, mapWarningEnabled: true, - product: [ - { name: 'foo' } - ], - size: 10, + issue: [], + product: [], tab: MODE_MAP }, - results: { - items - }, view: { - printMode + printMode, + width: 1000 } } ) return renderer.create( - + ) } describe( 'component:MapPanel', () => { + let target, tree it( 'renders without crashing', () => { - const printMode = false - const target = setupSnapshot( printMode ) - const tree = target.toJSON() + target = setupSnapshot( { enablePer1000: true, printMode: false } ) + tree = target.toJSON() expect( tree ).toMatchSnapshot() } ) it( 'renders Print without crashing', () => { - const printMode = true - const target = setupSnapshot( printMode ) - const tree = target.toJSON() + target = setupSnapshot( { enablePer1000: true, printMode: true } ) + tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders warning without crashing', () => { + target = setupSnapshot( { enablePer1000: false } ) + tree = target.toJSON() expect( tree ).toMatchSnapshot() } ) @@ -72,6 +78,5 @@ describe( 'component:MapPanel', () => { mapDispatchToProps(dispatch).onDismissWarning(); expect(dispatch.mock.calls.length).toEqual(1); }) - }) } ) diff --git a/src/components/__tests__/ResultsPanel.spec.jsx b/src/components/__tests__/ResultsPanel.spec.jsx index 1bb0f58c6..bb4c4021e 100644 --- a/src/components/__tests__/ResultsPanel.spec.jsx +++ b/src/components/__tests__/ResultsPanel.spec.jsx @@ -1,4 +1,4 @@ -import { mapDispatchToProps, ResultsPanel } from '../ResultsPanel' +import ReduxResultsPanel, { mapDispatchToProps, ResultsPanel } from '../ResultsPanel' import React from 'react'; import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' @@ -6,7 +6,7 @@ import thunk from 'redux-thunk' import { IntlProvider } from 'react-intl'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme' - +import { trendsResults } from '../../reducers/__fixtures__/trendsResults' const fixture = [ { company: 'foo', @@ -45,11 +45,22 @@ function setupSnapshot(items=[], initialStore={}, tab = 'List', printMode) { total: items.length }, map: { - state: [] + results: { + issue: [], + product: [], + state: [] + } }, results, query: { - tab: tab + lens: 'Overview', + subLens: '', + tab: tab, + date_received_min: new Date('7/10/2017'), + date_received_max: new Date('7/10/2020') + }, + trends: { + results: {} }, view: { printMode @@ -59,7 +70,7 @@ function setupSnapshot(items=[], initialStore={}, tab = 'List', printMode) { return renderer.create( - + ) @@ -74,6 +85,12 @@ describe('component:Results', () => { expect(tree).toMatchSnapshot(); }); + it('renders trends panel without crashing', () => { + const target = setupSnapshot( fixture, trendsResults, 'Trends' ); + const tree = target.toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('renders list panel without crashing', () => { const target = setupSnapshot( fixture, null, 'List' ); const tree = target.toJSON(); diff --git a/src/components/__tests__/RowChart.spec.jsx b/src/components/__tests__/RowChart.spec.jsx index 9882d8c89..de4c21f40 100644 --- a/src/components/__tests__/RowChart.spec.jsx +++ b/src/components/__tests__/RowChart.spec.jsx @@ -1,18 +1,25 @@ +import * as trendsUtils from '../../utils/trends' import configureMockStore from 'redux-mock-store' -import { mapStateToProps, RowChart } from '../Charts/RowChart' +import { + mapDispatchToProps, + mapStateToProps, + RowChart +} from '../Charts/RowChart' import { mount, shallow } from 'enzyme' import { Provider } from 'react-redux' import React from 'react' import renderer from 'react-test-renderer' import thunk from 'redux-thunk' +import { SLUG_SEPARATOR } from '../../constants' // this is how you override and mock an imported constructor jest.mock( 'britecharts', () => { const props = [ 'row', 'margin', 'backgroundColor', 'colorSchema', 'enableLabels', 'labelsSize', 'labelsTotalCount', 'labelsNumberFormat', 'outerPadding', - 'percentageAxisToMaxRatio', 'yAxisLineWrapLimit', - 'yAxisPaddingBetweenChart', 'width', 'wrapLabels', 'height' + 'percentageAxisToMaxRatio', 'yAxisLineWrapLimit', 'miniTooltip', + 'yAxisPaddingBetweenChart', 'width', 'wrapLabels', 'height', 'on', + 'valueFormatter', 'paddingBetweenGroups' ] const mock = {} @@ -30,7 +37,7 @@ jest.mock( 'britecharts', () => { jest.mock( 'd3', () => { const props = [ 'select', 'each', 'node', 'getBoundingClientRect', 'width', 'datum', 'call', - 'remove', 'selectAll' + 'remove', 'selectAll', 'on' ] const mock = {} @@ -52,12 +59,18 @@ function setupSnapshot() { const middlewares = [ thunk ] const mockStore = configureMockStore( middlewares ) const store = mockStore( { - map: {} + printMode: false, + width: 1000 } ) return renderer.create( - + ) } @@ -90,80 +103,399 @@ describe( 'component: RowChart', () => { } ) it( 'does nothing when no data', () => { - const target = shallow( ) + const target = shallow( ) target._redrawChart = jest.fn() target.setProps( { data: [] } ) - expect( target._redrawChart ).toHaveBeenCalledTimes( 0 ) + expect( target._redrawChart ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'handles Trend cookie flag', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { + data: [ + { name: 'More Information about xy', value: 10 }, + { name: 'More Information about z', value: 10 }, + { name: 'Something else nformation abou', value: 10 } + ] + } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) } ) it( 'trigger a new update when data changes', () => { - const target = shallow( ) + const target = shallow( ) target._redrawChart = jest.fn() - const sp = jest.spyOn(target.instance(), '_redrawChart') + const sp = jest.spyOn( target.instance(), '_redrawChart' ) target.setProps( { data: [ 2, 5 ] } ) expect( sp ).toHaveBeenCalledTimes( 1 ) } ) it( 'trigger a new update when printMode changes', () => { - const target = shallow( ) target._redrawChart = jest.fn() - const sp = jest.spyOn(target.instance(), '_redrawChart') + const sp = jest.spyOn( target.instance(), '_redrawChart' ) target.setProps( { printMode: true } ) expect( sp ).toHaveBeenCalledTimes( 1 ) } ) it( 'trigger a new update when width changes', () => { - const target = shallow( ) target._redrawChart = jest.fn() - const sp = jest.spyOn(target.instance(), '_redrawChart') + const sp = jest.spyOn( target.instance(), '_redrawChart' ) target.setProps( { width: 600 } ) expect( sp ).toHaveBeenCalledTimes( 1 ) } ) + + it( 'calls select Focus with a lens', () => { + const cb = jest.fn() + const target = shallow( ) + target.instance()._selectFocus( { name: 'foo' } ) + expect( cb ).toHaveBeenCalledTimes( 1 ) + expect( cb ).toHaveBeenCalledWith( { name: 'foo' }, 'Product', + [ 1, 2, 3 ] ) + } ) + + it( 'calls select Focus with product lens - Overview', () => { + const cb = jest.fn() + const target = shallow( ) + target.instance()._selectFocus( { name: 'foo' } ) + expect( cb ).toHaveBeenCalledTimes( 1 ) + expect( cb ).toHaveBeenCalledWith( { name: 'foo' }, 'Product', + [ 1, 2, 3 ] ) + } ) + + describe( 'row toggles', () => { + let expandCb, collapseCb + beforeEach( () => { + collapseCb = jest.fn() + expandCb = jest.fn() + } ) + it( 'ignores values not in expandable rows', () => { + const target = shallow( ) + target.instance()._toggleRow( 'not a expandable row' ) + expect( collapseCb ).toHaveBeenCalledTimes( 0 ) + expect( expandCb ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'collapses a row', () => { + const target = shallow( ) + target.instance()._toggleRow( 'foo' ) + expect( collapseCb ).toHaveBeenCalledTimes( 1 ) + expect( collapseCb ).toHaveBeenCalledWith( 'foo' ) + expect( expandCb ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'expands a row', () => { + const target = shallow( ) + target.instance()._toggleRow( 'foo' ) + expect( expandCb ).toHaveBeenCalledTimes( 1 ) + expect( expandCb ).toHaveBeenCalledWith( 'foo' ) + expect( collapseCb ).toHaveBeenCalledTimes( 0 ) + } ) + } ) } ) - describe( 'mapStateToProps', () => { - it( 'maps state and props', () => { - const state = { - aggs: { - total: 100 + describe( 'mapDispatchToProps', () => { + let dispatch + beforeEach( () => { + dispatch = jest.fn() + } ) + + afterEach( () => { + jest.clearAllMocks() + } ) + + it( 'hooks into changeFocus', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + const filters = [ + { + key: 'A', + 'sub_product.raw': { + buckets: [ + { key: 'B' }, + { key: 'C' }, + { key: 'D' }, + { key: 'E' } ] + } }, + { + key: 'Debt collection', + 'sub_product.raw': { + buckets: [ + { key: 'Other debt' }, + { key: 'Credit card debt' }, + { key: 'I do not know' }, + { key: 'Medical debt' }, + { key: 'Auto debt' }, + { key: 'Payday loan debt' } + ] + } + } ] + + const element = { + name: 'Visualize trends for A', + parent: 'A' + } + mapDispatchToProps( dispatch ) + .selectFocus( element, 'Product', filters ) + expect( dispatch.mock.calls ).toEqual( [ [ { + filterValues: [ 'A', 'A•B', 'A•C', 'A•D', 'A•E' ], + focus: 'A', + lens: 'Product', + requery: 'REQUERY_ALWAYS', + type: 'FOCUS_CHANGED' + } ] ] ) + expect( trendsUtils.scrollToFocus ).toHaveBeenCalled() + } ) + + it( 'hooks into changeFocus - no filter found', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + const filters = [ + { + key: 'Debt collection', + 'sub_product.raw': { + buckets: [ + { key: 'Other debt' }, + { key: 'Credit card debt' }, + { key: 'I do not know' }, + { key: 'Medical debt' }, + { key: 'Auto debt' }, + { key: 'Payday loan debt' } + ] + } + } ] + + const element = { + name: 'Visualize trends for A', + parent: 'A' + } + mapDispatchToProps( dispatch ) + .selectFocus( element, 'Product', filters ) + expect( dispatch.mock.calls ).toEqual( [ [ { + filterValues: [], + focus: 'A', + lens: 'Product', + requery: 'REQUERY_ALWAYS', + type: 'FOCUS_CHANGED' + } ] ] ) + expect( trendsUtils.scrollToFocus ).toHaveBeenCalled() + } ) + + it( 'hooks into changeFocus - Company', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + const filters = [ + { key: 'Acme' }, + { key: 'Beta' } + ] + + const element = { + name: 'Visualize trends for Acme', + parent: 'Acme' + } + mapDispatchToProps( dispatch ) + .selectFocus( element, 'Company', filters ) + expect( dispatch.mock.calls ).toEqual( [ [ { + filterValues: [ 'Acme' ], + focus: 'Acme', + lens: 'Company', + requery: 'REQUERY_ALWAYS', + type: 'FOCUS_CHANGED' + } ] ] ) + expect( trendsUtils.scrollToFocus ).toHaveBeenCalled() + } ) + + it( 'hooks into collapseTrend', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + mapDispatchToProps( dispatch ).collapseRow( 'Some Expanded row' ) + expect( dispatch.mock.calls ).toEqual( [ [ { + requery: 'REQUERY_NEVER', + type: 'TREND_COLLAPSED', + value: 'Some Expanded row' + } ] ] ) + expect( trendsUtils.scrollToFocus ).not.toHaveBeenCalled() + } ) + + it( 'hooks into expandTrend', () => { + spyOn( trendsUtils, 'scrollToFocus' ) + mapDispatchToProps( dispatch ).expandRow( 'collapse row name' ) + expect( dispatch.mock.calls ).toEqual( [ [ { + requery: 'REQUERY_NEVER', + type: 'TREND_EXPANDED', + value: 'collapse row name' + } ] ] ) + expect( trendsUtils.scrollToFocus ).not.toHaveBeenCalled() + } ) + } ) + + describe( 'mapStateToProps', () => { + let state + beforeEach( () => { + state = { map: { - baz: [ 1, 2, 3 ] + expandableRows: [], + expandedTrends: [] }, query: { - baz: [ 1, 2, 3 ] + lens: 'Foo', + tab: 'Map' + }, + trends: { + expandableRows: [], + expandedTrends: [] }, view: { - printMode: false + printMode: false, + showTrends: true, + width: 1000 } } + } ) + it( 'maps state and props - Map', () => { + const ownProps = { + id: 'baz' + } + let actual = mapStateToProps( state, ownProps ) + expect( actual ).toEqual( { + expandableRows: [], + expandedTrends: [], + lens: 'Product', + printMode: false, + showTrends: true, + tab: 'Map', + width: 1000 + } ) + } ) + + it( 'maps state and props - Other', () => { const ownProps = { - aggtype: 'baz' + id: 'baz' } + + state.query.tab = 'Trends' + let actual = mapStateToProps( state, ownProps ) expect( actual ).toEqual( { - data: [], + expandableRows: [], + expandedTrends: [], + lens: 'Foo', printMode: false, - total: 100 + tab: 'Trends', + showTrends: true, + width: 1000 } ) } ) } ) - describe('helper functions', ()=>{ - it('gets height based on number of rows', ()=>{ - const target = mount() - let res = target.instance()._getHeight(1) - expect(res).toEqual(100) - res = target.instance()._getHeight(5) - expect(res).toEqual(300) - }) - }) + describe( 'helper functions', () => { + it( 'gets height based on number of rows', () => { + const target = mount( ) + let res = target.instance()._getHeight( 1 ) + expect( res ).toEqual( 100 ) + res = target.instance()._getHeight( 5 ) + expect( res ).toEqual( 300 ) + } ) + + it( 'formats text of the tooltip', () => { + const target = mount( ) + let res = target.instance()._formatTip( 100000 ) + expect( res ).toEqual( '100,000 complaints' ) + } ) + + } ) } ) diff --git a/src/components/__tests__/Select.spec.jsx b/src/components/__tests__/Select.spec.jsx index 4bd5571d6..7e16e5fd5 100644 --- a/src/components/__tests__/Select.spec.jsx +++ b/src/components/__tests__/Select.spec.jsx @@ -1,10 +1,11 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { Select } from '../RefineBar/Select'; +import React from 'react' +import renderer from 'react-test-renderer' +import { Select } from '../RefineBar/Select' -describe('component:Select', () => { - it('renders array values without crashing', () => { - const options = ['Uno', 'Dos', 'Tres'] +describe( 'component:Select', () => { + + it( 'renders array values without crashing', () => { + const options = [ 'Uno', 'Dos', 'Tres' ] const target = renderer.create( + ) + + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) - const tree = target.toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); +} ) diff --git a/src/components/__tests__/StackedAreaChart.spec.jsx b/src/components/__tests__/StackedAreaChart.spec.jsx new file mode 100644 index 000000000..9a7a86f76 --- /dev/null +++ b/src/components/__tests__/StackedAreaChart.spec.jsx @@ -0,0 +1,266 @@ +import configureMockStore from 'redux-mock-store' +import { + mapDispatchToProps, + mapStateToProps, + StackedAreaChart +} from '../Charts/StackedAreaChart' +import { mount, shallow } from 'enzyme' +import { Provider } from 'react-redux' +import React from 'react' +import renderer from 'react-test-renderer' +import thunk from 'redux-thunk' + +// this is how you override and mock an imported constructor +jest.mock( 'britecharts', () => { + const props = [ + 'stackedArea', 'margin', 'initializeVerticalMarker', 'colorSchema', + 'dateLabel', 'tooltipThreshold', 'grid', 'aspectRatio', 'isAnimated', 'on', + 'yAxisPaddingBetweenChart', 'width', 'height', 'areaCurve' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + return mock +} ) + +jest.mock( 'd3', () => { + const props = [ + 'select', 'each', 'node', 'getBoundingClientRect', 'width', 'datum', 'call', + 'remove', 'selectAll' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + // set narrow width value for 100% test coverage + mock.width = 100 + + return mock +} ) + +function setupSnapshot() { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( {} ) + const colorMap = { + foo: '#fff', + bar: '#eee' + } + + return renderer.create( + + + + ) +} + +describe( 'component: StackedAreaChart', () => { + describe( 'initial state', () => { + it( 'renders without crashing', () => { + const target = setupSnapshot() + let tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + } ) + + describe( 'componentDidUpdate', () => { + let mapDiv + + beforeEach( () => { + mapDiv = document.createElement( 'div' ) + mapDiv.setAttribute( 'id', 'stacked-area-chart-foo' ) + window.domNode = mapDiv + document.body.appendChild( mapDiv ) + } ) + + afterEach( () => { + const div = document.getElementById( 'stacked-area-chart-foo' ) + if ( div ) { + document.body.removeChild( div ) + } + jest.clearAllMocks() + } ) + + it( 'does nothing when no data', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + target.setProps( { data: [] } ) + expect( target._redrawChart ).toHaveBeenCalledTimes( 0 ) + } ) + + it( 'trigger a new update when data changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { data: [ 2, 5 ] } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + + it( 'trigger a new update when printMode changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { printMode: true } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + + it( 'trigger a new update when width changes', () => { + const target = shallow( ) + target._redrawChart = jest.fn() + const sp = jest.spyOn( target.instance(), '_redrawChart' ) + target.setProps( { width: 600 } ) + expect( sp ).toHaveBeenCalledTimes( 1 ) + } ) + } ) + + describe( 'mapDispatchToProps', () => { + it( 'provides a way to call updateTrendsTooltip', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).tooltipUpdated( 'foo' ) + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: 'REQUERY_NEVER', + type: 'TRENDS_TOOLTIP_CHANGED', + value: 'foo' + } ] + ] ) + } ) + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + query: { + dateInterval: 'Month', + date_received_min: '', + date_received_max: '' + }, + trends: { + colorMap: {}, + lens: 'Overview', + results: { + dateRangeArea: [] + }, + tooltip: {} + }, + view: { + printMode: false, + width: 1000 + } + } + + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + colorMap: {}, + data: [], + dateRange: { + from: '', + to: '' + }, + interval: 'Month', + lens: 'Overview', + printMode: false, + tooltip: {}, + width: 1000 + } ) + } ) + } ) + + describe( 'tooltip events', () => { + let target, cb + + beforeEach( () => { + cb = jest.fn() + } ) + + it( 'updates external tooltip with different data', () => { + target = shallow( ) + const instance = target.instance() + instance._updateTooltip( { date: '2012', values: [ 1, 2, 3 ] } ) + expect( cb ).toHaveBeenCalledTimes( 2 ) + } ) + + it( 'Only updates external tooltip on init', () => { + target = shallow( ) + const instance = target.instance() + instance._updateTooltip( { date: '2000', value: 200 } ) + expect( cb ).toHaveBeenCalledTimes( 1 ) + } ) + } ) + describe( 'helpers', () => { + describe( '_chartWidth', () => { + it( 'gets print width', () => { + const target = shallow( ) + expect( target.instance()._chartWidth( '#foo' ) ) + .toEqual( 540 ) + } ) + } ) + } ) + +} ) diff --git a/src/components/__tests__/TabbedNavigation.spec.jsx b/src/components/__tests__/TabbedNavigation.spec.jsx index 93d80ef09..88011d7b7 100644 --- a/src/components/__tests__/TabbedNavigation.spec.jsx +++ b/src/components/__tests__/TabbedNavigation.spec.jsx @@ -8,16 +8,14 @@ import renderer from 'react-test-renderer' import { shallow } from 'enzyme' import thunk from 'redux-thunk' -function setupSnapshot() { +function setupSnapshot(showTrends) { const middlewares = [ thunk ] const mockStore = configureMockStore( middlewares ) - const store = mockStore( { - map: {} - } ) + const store = mockStore() return renderer.create( - + ) } @@ -25,7 +23,13 @@ function setupSnapshot() { describe( 'component: TabbedNavigation', () => { describe( 'initial state', () => { it( 'renders without crashing', () => { - const target = setupSnapshot() + const target = setupSnapshot(false) + let tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'shows the Trends tab', () => { + const target = setupSnapshot(true) let tree = target.toJSON() expect( tree ).toMatchSnapshot() } ) @@ -37,9 +41,7 @@ describe( 'component: TabbedNavigation', () => { beforeEach( () => { cb = jest.fn() - window.scrollTo = jest.fn(); - - target = shallow( ) + target = shallow( ) } ) it( 'tabChanged is called with Map when the button is clicked', () => { @@ -48,6 +50,12 @@ describe( 'component: TabbedNavigation', () => { expect( cb ).toHaveBeenCalledWith('Map') } ) + it( 'tabChanged is called with Trends when the button is clicked', () => { + const prev = target.find( '.tabbed-navigation button.trends' ) + prev.simulate( 'click' ) + expect( cb ).toHaveBeenCalledWith('Trends') + } ) + it( 'tabChanged is called with List when the button is clicked', () => { const prev = target.find( '.tabbed-navigation button.list' ) prev.simulate( 'click' ) @@ -68,10 +76,13 @@ describe( 'component: TabbedNavigation', () => { const state = { query: { tab: 'foo' + }, + view: { + showTrends: true } } let actual = mapStateToProps( state ) - expect( actual ).toEqual( { tab: 'foo' } ) + expect( actual ).toEqual( { tab: 'foo', showTrends: true } ) } ) } ) diff --git a/src/components/__tests__/TileChartMap.spec.jsx b/src/components/__tests__/TileChartMap.spec.jsx index de87d8f6f..7e8200c10 100644 --- a/src/components/__tests__/TileChartMap.spec.jsx +++ b/src/components/__tests__/TileChartMap.spec.jsx @@ -166,13 +166,20 @@ describe( 'component: TileChartMap', () => { const state = { map: { dataNormalization: false, - state: [ - // name comes from agg api - { name: 'TX', issue: 'something', product: 'a prod', value: 100000 }, - { name: 'LA', issue: 'something', product: 'b prod', value: 2 }, - { name: 'CA', issue: 'something', product: 'c prod', value: 3 }, - { name: 'MH', issue: 'real data', product: 'is messy', value: 9 }, - ] + results: { + state: [ + // name comes from agg api + { + name: 'TX', + issue: 'something', + product: 'a prod', + value: 100000 + }, + { name: 'LA', issue: 'something', product: 'b prod', value: 2 }, + { name: 'CA', issue: 'something', product: 'c prod', value: 3 }, + { name: 'MH', issue: 'real data', product: 'is messy', value: 9 }, + ] + } }, query: { state: [ 'TX' ] @@ -235,5 +242,85 @@ describe( 'component: TileChartMap', () => { width: 1000 } ) } ) + + it( 'maps state and props - no filters', () => { + const state = { + map: { + dataNormalization: false, + results: { + state: [ + // name comes from agg api + { + name: 'TX', + issue: 'something', + product: 'a prod', + value: 100000 + }, + { name: 'LA', issue: 'something', product: 'b prod', value: 2 }, + { name: 'CA', issue: 'something', product: 'c prod', value: 3 }, + { name: 'MH', issue: 'real data', product: 'is messy', value: 9 }, + ] + } + }, + query: { + }, + view: { + printMode: false, + width: 1000 + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + data: [ + [ + { + abbr: 'TX', + name: 'TX', + fullName: 'Texas', + className: '', + issue: 'something', + perCapita: '3.65', + product: 'a prod', + value: 100000 + }, + { + abbr: 'LA', + name: 'LA', + fullName: 'Louisiana', + className: '', + issue: 'something', + perCapita: '0.00', + product: 'b prod', + value: 2 + }, + { + abbr: 'CA', + name: 'CA', + fullName: 'California', + className: '', + issue: 'something', + perCapita: '0.00', + product: 'c prod', + value: 3 + }, + { + abbr: 'MH', + name: 'MH', + fullName: '', + className: '', + issue: 'real data', + perCapita: '9000.00', + product: 'is messy', + value: 9 + } + ] + ], + dataNormalization: false, + hasTip: true, + printClass: '', + stateFilters: [], + width: 1000 + } ) + } ) } ) } ) diff --git a/src/components/__tests__/TrendDepthToggle.spec.jsx b/src/components/__tests__/TrendDepthToggle.spec.jsx new file mode 100644 index 000000000..2cb1a5710 --- /dev/null +++ b/src/components/__tests__/TrendDepthToggle.spec.jsx @@ -0,0 +1,271 @@ +import configureMockStore from 'redux-mock-store' +import { IntlProvider } from 'react-intl' +import { Provider } from 'react-redux' +import ReduxTrendDepthToggle, { + TrendDepthToggle, + mapDispatchToProps, + mapStateToProps +} from '../Trends/TrendDepthToggle' +import React from 'react' +import renderer from 'react-test-renderer' +import { REQUERY_ALWAYS } from '../../constants' +import { shallow } from 'enzyme' +import thunk from 'redux-thunk' + +function setupEnzyme( { cbIncrease, cbReset, diff, queryCount, resultCount } ) { + return shallow( ) +} + +function setupSnapshot( { focus, lens, productAggs, productResults } ) { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( { + aggs: { + product: productAggs + }, + query: { + focus, + lens + }, + trends: { + results: { + product: productResults + } + } + } ) + + return renderer.create( + + + + + + ) +} + +describe( 'component:TrendDepthToggle', () => { + let params + + beforeEach( () => { + params = { + focus: '', + lens: '', + productAggs: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ], + productResults: [ { name: 'a', visible: true }, { name: 'b', visible: true }, + { name: 'c', visible: true }, { name: 'd', visible: true }, + { name: 'e', visible: true }, { name: 'f', visible: true }, + { name: 'g', visible: true }, { name: 'h', visible: true } + ] + } + } ) + + it( 'does not render when Focus', () => { + params.focus = 'A focus item' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toBeNull() + } ) + + it( 'does not render lens is not Product', () => { + params.lens = 'Cannot See' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toBeNull() + } ) + + + it( 'renders Product view more without crashing', () => { + params.lens = 'Product' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders Product view less without crashing', () => { + params.lens = 'Product' + params.productAggs = [ 1, 2, 3, 4, 5 ] + params.productResults = [ + { name: 'a', visible: true, isParent: true }, + { name: 'b', visible: true, isParent: true }, + { name: 'c', visible: true, isParent: true }, + { name: 'd', visible: true, isParent: true }, + { name: 'e', visible: true, isParent: true }, + { name: 'f', visible: true, isParent: true }, + { name: 'g', visible: true, isParent: true }, + { name: 'h', visible: true, isParent: true } + ] + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + describe( 'buttons', () => { + let cbIncrease = null + let cbReset = null + let target + + beforeEach( () => { + cbIncrease = jest.fn() + cbReset = jest.fn() + + } ) + + it( 'increaseDepth is called when the increase button is clicked', () => { + target = setupEnzyme( { cbIncrease, cbReset, diff: 1000, resultCount: 5 } ) + const prev = target.find( '#trend-depth-button' ) + prev.simulate( 'click' ) + expect( cbIncrease ).toHaveBeenCalledWith( 1000 ) + } ) + + it( 'reset depth is called when the reset button is clicked', () => { + target = setupEnzyme( { + cbIncrease, + cbReset, + diff: 0, + queryCount: 10, + resultCount: 10 + } ) + const prev = target.find( '#trend-depth-button' ) + prev.simulate( 'click' ) + expect( cbReset ).toHaveBeenCalled() + } ) + } ) + + describe( 'mapDispatchToProps', () => { + it( 'hooks into changeDepth', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).increaseDepth( 13 ) + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: REQUERY_ALWAYS, + depth: 18, + type: 'DEPTH_CHANGED' + } ] + ] ) + } ) + + it( 'hooks into resetDepth', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).depthReset() + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: REQUERY_ALWAYS, + type: 'DEPTH_RESET' + } ] + ] ) + } ) + + } ) + + describe( 'mapStateToProps', () => { + it( 'maps state and props', () => { + const state = { + aggs: { + product: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] + }, + query: { + focus: '', + lens: 'Product' + }, + trends: { + results: { + product: [ { name: 'a', visible: true }, { + name: 'b', + visible: true + }, + { name: 'c', visible: true }, { name: 'd', visible: true }, + { name: 'e', visible: true }, { name: 'f', visible: true }, + { name: 'g', visible: true }, { name: 'h', visible: true } + ] + } + } + } + let actual = mapStateToProps( state ) + expect( actual ).toEqual( { + diff: 11, + queryCount: 11, + resultCount: 0, + showToggle: true + } ) + } ) + + describe( 'when lens = Company', () => { + let state + beforeEach( () => { + state = { + aggs: {}, + query: { + focus: '', + lens: 'Company', + company: [ + 'I', 'I', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' + ] + }, + trends: { + results: { + company: [ + { name: 'a', visible: true }, { name: 'b', visible: true }, + { name: 'c', visible: true }, { name: 'd', visible: true }, + { name: 'e', visible: true }, { name: 'f', visible: true }, + { name: 'g', visible: true }, { name: 'h', visible: true }, + { name: 'i', visible: true }, { name: 'j', visible: true } + ] + } + } + } + } ) + + it( 'caps the maximum number of companies at 10' , () => { + const actual = mapStateToProps( state ) + expect( actual ).toEqual( { + diff: 10, + queryCount: 11, + resultCount: 0, + showToggle: true + } ) + } ) + + it( 'shows the toggle when results < 10' , () => { + state.trends.results.company.splice(4, 5) + + const actual = mapStateToProps( state ) + expect( actual ).toEqual( { + diff: 10, + queryCount: 11, + resultCount: 0, + showToggle: true + } ) + } ) + + it( 'hides the toggle when only parent count < 5 item' , () => { + state.query.company = [ 'a' ] + state.trends.results.company = [ + { name: 'a', visible: true, isParent: true}, + { name: 'b', visible: true, isParent: false }, + { name: 'c', visible: true, isParent: false }, + { name: 'd', visible: true, isParent: false }, + { name: 'e', visible: true, isParent: false }, + { name: 'f', visible: true, isParent: false }, + { name: 'g', visible: true, isParent: false }, + { name: 'h', visible: true, isParent: false }, + { name: 'i', visible: true, isParent: false }, + { name: 'j', visible: true, isParent: false } + ] + + const actual = mapStateToProps( state ) + expect( actual ).toEqual( { + diff: 0, + queryCount: 1, + resultCount: 1, + showToggle: false + } ) + } ) + } ) + } ) +} ) diff --git a/src/components/__tests__/TrendsPanel.spec.jsx b/src/components/__tests__/TrendsPanel.spec.jsx new file mode 100644 index 000000000..8bd549827 --- /dev/null +++ b/src/components/__tests__/TrendsPanel.spec.jsx @@ -0,0 +1,309 @@ +import configureMockStore from 'redux-mock-store' +import { IntlProvider } from 'react-intl' +import { Provider } from 'react-redux' +import ReduxTrendsPanel, { + TrendsPanel, + mapDispatchToProps +} from '../Trends/TrendsPanel' +import { MODE_TRENDS } from '../../constants' +import React from 'react' +import renderer from 'react-test-renderer' +import { shallow } from 'enzyme' +import thunk from 'redux-thunk' + +jest.mock( 'britecharts', () => { + const props = [ + 'brush', 'line', 'tooltip', 'margin', 'backgroundColor', 'colorSchema', + 'enableLabels', 'labelsSize', 'labelsTotalCount', 'labelsNumberFormat', + 'outerPadding', 'percentageAxisToMaxRatio', 'yAxisLineWrapLimit', + 'dateRange', 'yAxisPaddingBetweenChart', 'width', 'wrapLabels', 'height', + 'on', 'initializeVerticalMarker', 'isAnimated', 'tooltipThreshold', 'grid', + 'aspectRatio', 'dateLabel', 'shouldShowDateInTitle', 'topicLabel', 'title' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + return mock +} ) + +jest.mock( 'd3', () => { + const props = [ + 'select', 'each', 'node', 'getBoundingClientRect', 'width', 'datum', 'call', + 'remove', 'selectAll' + ] + + const mock = {} + + for ( let i = 0; i < props.length; i++ ) { + const propName = props[i] + mock[propName] = jest.fn().mockImplementation( () => { + return mock + } ) + } + + // set narrow width value for 100% test coverage + mock.width = 100 + + return mock +} ) + +function setupEnzyme( { focus, overview, lens, subLens } ) { + const props = { + focus, + lenses: [ 'Foo', 'Baz', 'Bar' ], + intervals: [ 'Month', 'Quarter', 'Nickel', 'Day' ], + overview, + lens, + subLens, + onChartType: jest.fn(), + onInterval: jest.fn(), + onLens: jest.fn() + } + const target = shallow( ) + return target +} + +function setupSnapshot( { chartType, + company, + focus, + lens, + subLens, + trendsDateWarningEnabled, + width }) { + const middlewares = [ thunk ] + const mockStore = configureMockStore( middlewares ) + const store = mockStore( { + aggs: { + doc_count: 10000, + total: 1000 + }, + query: { + company, + date_received_min: "2018-01-01T00:00:00.000Z", + date_received_max: "2020-01-01T00:00:00.000Z", + lens, + subLens, + tab: MODE_TRENDS, + trendsDateWarningEnabled + }, + trends: { + chartType, + colorMap: { "Complaints": "#ADDC91", "Other": "#a2a3a4" }, + focus, + lens, + results: { + "dateRangeBrush": [ + { + "date": new Date( "2020-01-01T00:00:00.000Z" ), + "value": 26413 + }, { + "date": new Date( "2020-02-01T00:00:00.000Z" ), + "value": 25096 + }, { + "date": new Date( "2020-03-01T00:00:00.000Z" ), + "value": 29506 + }, { + "date": new Date( "2020-04-01T00:00:00.000Z" ), + "value": 35112 + }, { + "date": new Date( "2020-05-01T00:00:00.000Z" ), + "value": 9821 + } ], + "dateRangeLine": { + "dataByTopic": [ { + "topic": "Complaints", + "topicName": "Complaints", + "dashed": false, + "show": true, + "dates": [ + { "date": "2020-03-01T00:00:00.000Z", "value": 29506 }, + { "date": "2020-04-01T00:00:00.000Z", "value": 35112 }, + { "date": "2020-05-01T00:00:00.000Z", "value": 9821 } + ] + } ] + }, + issue: [ { name: 'adg', value: 123 } ], + product: [ { name: 'adg', value: 123 } ], + company: [ { name: 'adg', value: 123 } ] + }, + total: 10000 + }, + view: { + width + } + } ) + + return renderer.create( + + + + + + ) +} + +describe( 'component:TrendsPanel', () => { + describe( 'Snapshots', () => { + let params + beforeEach(()=>{ + params = { + chartType: 'line', + company: false, + focus: '', + lens: 'Overview', + printMode: false, + showMobileFilters: false, + subLens: 'sub_product', + trendsDateWarningEnabled: false, + width: 1000 + } + }) + + it( 'renders company Overlay without crashing', () => { + params.company = [] + params.lens = 'Company' + + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders lineChart Overview without crashing', () => { + params.lens = 'Overview' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders area without crashing', () => { + params.chartType = 'area' + params.lens = 'Product' + params.subLens = 'sub_product' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders date warning without crashing', () => { + params.trendsDateWarningEnabled = true + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders Focus without crashing', () => { + params.focus = 'Yippe' + params.lens = 'Product' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders print mode without crashing', () => { + params.printMode = true + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders mobile filters without crashing', () => { + params.width = 600 + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + + it( 'renders external Tooltip without crashing', () => { + params.lens = 'Product' + const target = setupSnapshot( params ) + const tree = target.toJSON() + expect( tree ).toMatchSnapshot() + } ) + } ) + + describe( 'helpers', () => { + describe( 'areaChartTitle', () => { + let params + beforeEach( () => { + params = { + focus: '', + overview: false, + lens: 'Overview', + subLens: 'sub_product' + } + } ) + it( 'gets area chart title - Overview', () => { + params.overview = true + const target = setupEnzyme( params ) + expect( target.instance()._areaChartTitle() ) + .toEqual( 'Complaints by date received by the CFPB' ) + } ) + + it( 'gets area chart title - Data Lens', () => { + params.lens = 'Something' + const target = setupEnzyme( params ) + expect( target.instance()._areaChartTitle() ) + .toEqual( 'Complaints by date received by the CFPB' ) + } ) + + it( 'gets area chart title - Focus', () => { + params.focus = 'Hello' + params.lens = 'Product' + const target = setupEnzyme( params ) + expect( target.instance()._areaChartTitle() ) + .toEqual( 'Complaints by sub-products, by date received by the CFPB' ) + } ) + } ) + } ) + + describe( 'mapDispatchToProps', () => { + it( 'hooks into changeChartType', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ) + .onChartType( { + target: { + value: 'foo' + } + } ) + expect( dispatch.mock.calls.length ).toEqual( 1 ) + } ) + it( 'hooks into changeDateInterval', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ) + .onInterval( { + target: { + value: 'foo' + } + } ) + expect( dispatch.mock.calls.length ).toEqual( 1 ) + } ) + it( 'hooks into changeDataLens', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ) + .onLens( { + target: { + value: 'foo' + } + } ) + expect( dispatch.mock.calls.length ).toEqual( 1 ) + } ) + + it( 'hooks into dismissWarning', () => { + const dispatch = jest.fn() + mapDispatchToProps( dispatch ).onDismissWarning() + expect( dispatch.mock.calls ).toEqual( [ + [ { + requery: 'REQUERY_NEVER', + type: 'TRENDS_DATE_WARNING_DISMISSED' + } ] + ] ) + } ) + } ) +} ) diff --git a/src/components/__tests__/__snapshots__/ChartToggles.spec.jsx.snap b/src/components/__tests__/__snapshots__/ChartToggles.spec.jsx.snap new file mode 100644 index 000000000..b90a9052e --- /dev/null +++ b/src/components/__tests__/__snapshots__/ChartToggles.spec.jsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component: ChartToggles initial state renders without crashing 1`] = ` +
    +

    + Chart type +

    + + +
    +`; diff --git a/src/components/__tests__/__snapshots__/ExternalTooltip.spec.jsx.snap b/src/components/__tests__/__snapshots__/ExternalTooltip.spec.jsx.snap new file mode 100644 index 000000000..9648208f4 --- /dev/null +++ b/src/components/__tests__/__snapshots__/ExternalTooltip.spec.jsx.snap @@ -0,0 +1,521 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`initial state renders "Other" without crashing 1`] = ` +
    +

    + + Date Range: + + + 1/1/1900 - 1/1/2000 + +

    +
    +
      +
    • + + foo + + + 1,000 + +
    • +
    • + + bar + + + 1,000 + +
    • +
    • + + All other + + + 900 + +
    • +
    • + + Eat at Joe's + + + 1,000 + +
    • +
    • + + All other + + + 900 + +
    • +
    +
      +
    • + + Total + + + 2,900 + +
    • +
    +
    +
    +`; + +exports[`initial state renders Company typehead without crashing 1`] = ` +
    +
    +
    +
    + + + +
    + + +
    +
    +

    + + Date Range: + + + 1/1/1900 - 1/1/2000 + +

    +
    +
      +
    • + + foo + + + + + + + + 1,000 + +
    • +
    • + + bar + + + + + + + + 1,000 + +
    • +
    • + + All other + + + + + + + + 900 + +
    • +
    • + + Eat at Joe's + + + + + + + + 1,000 + +
    • +
    +
      +
    • + + Total + + + 2,900 + +
    • +
    +
    +
    +`; + +exports[`initial state renders focus without crashing 1`] = ` +
    +

    + + Date Range: + + + 1/1/1900 - 1/1/2000 + +

    +
    +
      +
    • + + foo + + + 1,000 + +
    • +
    • + + bar + + + 1,000 + +
    • +
    • + + All other + + + 900 + +
    • +
    • + + Eat at Joe's + + + 1,000 + +
    • +
    +
      +
    • + + Total + + + 2,900 + +
    • +
    +
    +
    +`; + +exports[`initial state renders nothing without crashing 1`] = `null`; + +exports[`initial state renders without crashing 1`] = ` +
    +

    + + Date Range: + + + 1/1/1900 - 1/1/2000 + +

    +
    +
      +
    • + + foo + + + 1,000 + +
    • +
    • + + bar + + + 1,000 + +
    • +
    • + + All other + + + 900 + +
    • +
    • + + Eat at Joe's + + + 1,000 + +
    • +
    +
      +
    • + + Total + + + 2,900 + +
    • +
    +
    +
    +`; diff --git a/src/components/__tests__/__snapshots__/FilterPanelToggle.spec.jsx.snap b/src/components/__tests__/__snapshots__/FilterPanelToggle.spec.jsx.snap index 8d0f160be..581ba1f84 100644 --- a/src/components/__tests__/__snapshots__/FilterPanelToggle.spec.jsx.snap +++ b/src/components/__tests__/__snapshots__/FilterPanelToggle.spec.jsx.snap @@ -13,7 +13,6 @@ exports[`initial state renders without crashing 1`] = ` diff --git a/src/components/__tests__/__snapshots__/FocusHeader.spec.jsx.snap b/src/components/__tests__/__snapshots__/FocusHeader.spec.jsx.snap new file mode 100644 index 000000000..59f365978 --- /dev/null +++ b/src/components/__tests__/__snapshots__/FocusHeader.spec.jsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component:FocusHeader renders without crashing 1`] = ` +
    + +
    +
    +

    + Foo Bar +

    + +

    + 90,120 + Complaints +

    +
    +
    +
    +
    + + +
    +
    +
    +`; diff --git a/src/components/__tests__/__snapshots__/LensTabs.spec.jsx.snap b/src/components/__tests__/__snapshots__/LensTabs.spec.jsx.snap new file mode 100644 index 000000000..f34cdf341 --- /dev/null +++ b/src/components/__tests__/__snapshots__/LensTabs.spec.jsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component:LensTabs renders Product without crashing 1`] = ` +
    +
    + + +
    +
    +`; diff --git a/src/components/__tests__/__snapshots__/LineChart.spec.jsx.snap b/src/components/__tests__/__snapshots__/LineChart.spec.jsx.snap new file mode 100644 index 000000000..f69c8522d --- /dev/null +++ b/src/components/__tests__/__snapshots__/LineChart.spec.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component: LineChart initial state renders data lens without crashing 1`] = ` +
    +

    + Complaints +

    +
    +

    + Date received by the CFPB +

    +
    +`; + +exports[`component: LineChart initial state renders without crashing 1`] = ` +
    +

    + Complaints +

    +
    +

    + Date received by the CFPB +

    +
    +`; diff --git a/src/components/__tests__/__snapshots__/ListPanel.spec.jsx.snap b/src/components/__tests__/__snapshots__/ListPanel.spec.jsx.snap index f8248d2e5..0b2a6de73 100644 --- a/src/components/__tests__/__snapshots__/ListPanel.spec.jsx.snap +++ b/src/components/__tests__/__snapshots__/ListPanel.spec.jsx.snap @@ -72,7 +72,6 @@ exports[`component:ListPanel displays a message when an error has occurred 1`] = @@ -99,21 +98,25 @@ exports[`component:ListPanel displays a message when an error has occurred 1`] = value="10" >
    -
    - - - -
    -
    - “Complaints per 1,000 population” is not available with your filter selections. -
    -
    - -
    @@ -95,7 +72,6 @@ exports[`component:MapPanel renders Print without crashing 1`] = ` @@ -157,7 +133,7 @@ exports[`component:MapPanel renders Print without crashing 1`] = ` Complaints
    - + + + + +
    Filter results @@ -411,22 +391,196 @@ exports[`component:MapPanel renders without crashing 1`] = ` className="row-chart-section" >

    - Product - by highest complaint volume + Sub-products, by product from 07/10/2017 to 07/10/2020

    +

    + Product and sub-product the consumer identified in the complaint. Click on a product to expand sub-products. +

    + +`; + +exports[`component:MapPanel renders without crashing 1`] = ` +
    +
    + +
    +

    + Showing  + + 2 + +  matches out of  + + 100 + +  total complaints +

    +
    +
    +

    + + +

    +
    +
    +
    +
    +
    +
    +

    +   +

    + +
    +
    + +
    +

    + Date range (Click to modify range) +

    + + + + + +
    +
    +
    +

    + Map shading +

    + + +
    +
    +
    +
    +
    +
    +
    +
    + + United States of America + + + + + +
    + +

    - Issue - by highest complaint volume + Sub-products, by product from 07/10/2017 to 07/10/2020

    +

    + Product and sub-product the consumer identified in the complaint. Click on a product to expand sub-products. +

    diff --git a/src/components/__tests__/__snapshots__/ResultsPanel.spec.jsx.snap b/src/components/__tests__/__snapshots__/ResultsPanel.spec.jsx.snap index 0a4c58483..014b7c3a8 100644 --- a/src/components/__tests__/__snapshots__/ResultsPanel.spec.jsx.snap +++ b/src/components/__tests__/__snapshots__/ResultsPanel.spec.jsx.snap @@ -12,7 +12,7 @@ exports[`component:Results renders List print mode without crashing 1`] = ` Dates: - - + 7/10/2017 - 7/10/2020

    @@ -101,7 +101,6 @@ exports[`component:Results renders List print mode without crashing 1`] = ` @@ -127,21 +126,25 @@ exports[`component:Results renders List print mode without crashing 1`] = ` onChange={[Function]} >