├── app ├── utils │ ├── .gitkeep │ ├── testHelpers.js │ ├── osHelpers.js │ ├── writeSummaryOutputUtils.js │ ├── fileUtils.js │ ├── formatSummaryOutputUtils.js │ └── gogenUtils.js ├── app.global.scss ├── app.icns ├── components │ ├── CountySelect.css │ ├── PageContainer.css │ ├── PrivacyPolicyFormCard.css │ ├── DojFileSelectFormCard.css │ ├── ImpactItem.css │ ├── TermsOfServiceFormCard.css │ ├── PageHeader.css │ ├── DOJFileInput.css │ ├── DojFileItem.css │ ├── PageContent.js │ ├── Checkbox.css │ ├── IntroductionFormCard.css │ ├── FormCardFooter.js │ ├── GoBackButton.js │ ├── FormCardContent.js │ ├── StartOverButton.js │ ├── ImpactItem.js │ ├── FormCard.js │ ├── PageFooter.css │ ├── FormCardHeader.js │ ├── FaqFormCard.css │ ├── RadioButton.css │ ├── ContinueButton.js │ ├── ErrorSection.js │ ├── PageContainer.js │ ├── RadioButton.js │ ├── PageHeader.js │ ├── DojFileItem.js │ ├── Checkbox.js │ ├── BaselineEligibilityOption.js │ ├── CountySelectFormCard.js │ ├── CountySelect.js │ ├── FormCard.css │ ├── DojFileInput.js │ ├── NumberSelect.js │ ├── ProcessingFormCard.js │ ├── ResultsFormCard.css │ ├── ProgressBar.js │ ├── DojFileSelectFormCard.js │ ├── SummaryReportPdfStyles.js │ ├── ErrorFormCard.js │ ├── IntroductionFormCard.js │ ├── PageFooter.js │ ├── EligibilityOptionsFormCard.js │ ├── AdditionalReliefFormCard.js │ └── ResultsFormCard.js ├── assets │ ├── icons │ │ └── win │ │ │ └── cfa.ico │ └── images │ │ ├── bullet.png │ │ ├── teal_check.png │ │ ├── cmr_black_logo.png │ │ ├── cmr_logo_black.png │ │ ├── white_check_large.png │ │ └── cmr_logo_black_cropped.png ├── app.global.css ├── constants │ ├── nonLinearScreenNumbers.js │ ├── defaultAnalysisOptions.js │ └── californiaCounties.js ├── containers │ └── Root.js ├── index.js ├── app.html └── main.dev.js ├── .gogen-version ├── .yarnclean ├── test ├── setup.js ├── .eslintrc.json ├── components │ ├── __snapshots__ │ │ ├── DojFileItem.spec.js.snap │ │ ├── DojFileInput.spec.js.snap │ │ ├── ProgressBar.spec.js.snap │ │ ├── NumberSelect.spec.js.snap │ │ ├── FormCardHeader.spec.js.snap │ │ ├── RadioButton.spec.js.snap │ │ ├── BaselineEligibilityOption.spec.js.snap │ │ ├── Checkbox.spec.js.snap │ │ ├── IntroductionFormCard.spec.js.snap │ │ ├── DojFileSelectFormCard.spec.js.snap │ │ └── CountySelect.spec.js.snap │ ├── ContinueButton.spec.js │ ├── IntroductionFormCard.spec.js │ ├── CountySelect.spec.js │ ├── DojFileItem.spec.js │ ├── FormCardHeader.spec.js │ ├── DojFileInput.spec.js │ ├── NumberSelect.spec.js │ ├── BaselineEligibilityOption.spec.js │ ├── ProcessingFormCard.spec.js │ ├── RadioButton.spec.js │ ├── CountySelectFormCard.spec.js │ ├── ResultsFormCard.spec.js │ ├── Checkbox.spec.js │ ├── ErrorFormCard.spec.js │ ├── DojFileSelectFormCard.spec.js │ ├── EligibilityOptionsFormCard.spec.js │ └── ProgressBar.spec.js ├── utils │ ├── fileUtils.spec.js │ ├── writeSummaryOutputUtils.spec.js │ └── formatSummaryOutputUtils.spec.js ├── e2e │ └── helpers.js └── ci-e2e │ └── ciHomeIntegration.spec.js ├── internals ├── mocks │ └── fileMock.js ├── flow │ ├── WebpackAsset.js.flow │ └── CSSModule.js.flow ├── img │ ├── js.png │ ├── flow.png │ ├── jest.png │ ├── npm.png │ ├── react.png │ ├── redux.png │ ├── yarn.png │ ├── eslint.png │ ├── webpack.png │ ├── erb-banner.png │ ├── erb-logo.png │ ├── js-padded.png │ ├── flow-padded.png │ ├── jest-padded.png │ ├── react-padded.png │ ├── react-router.png │ ├── redux-padded.png │ ├── yarn-padded.png │ ├── eslint-padded.png │ ├── flow-padded-90.png │ ├── jest-padded-90.png │ ├── react-padded-90.png │ ├── redux-padded-90.png │ ├── webpack-padded.png │ ├── yarn-padded-90.png │ ├── eslint-padded-90.png │ ├── webpack-padded-90.png │ ├── react-router-padded.png │ └── react-router-padded-90.png └── scripts │ ├── CheckYarn.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ └── CheckBuiltsExist.js ├── resources ├── icon.ico ├── icon.png ├── icon.icns └── icons │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 96x96.png │ ├── 128x128.png │ ├── 256x256.png │ ├── 512x512.png │ └── 1024x1024.png ├── .testcafe-electron-rc ├── .stylelintrc ├── .gitattributes ├── flow-typed ├── module_vx.x.x.js ├── electronExtensions.js └── domainTypes.js ├── .github ├── ISSUE_TEMPLATE.md ├── config.yml ├── ISSUE_TEMPLATE │ ├── 2-Feature_request.md │ └── 1-Bug_report.md └── stale.yml ├── renovate.json ├── configs ├── webpack.config.eslint.js ├── webpack.config.base.js ├── webpack.config.renderer.dev.dll.babel.js ├── webpack.config.main.prod.babel.js └── webpack.config.renderer.prod.babel.js ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc.json ├── .editorconfig ├── .eslintrc.json ├── CHANGELOG.md ├── Dockerfile ├── __mocks__ └── fs.js ├── appveyor.yml ├── .flowconfig ├── .dockerignore ├── .gitignore ├── .eslintignore ├── LICENSE ├── .circleci └── config.yml ├── .travis.yml └── babel.config.js /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gogen-version: -------------------------------------------------------------------------------- 1 | 0.2.13 2 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | paralleljs/package.json 2 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | window.scrollTo = () => {}; 2 | -------------------------------------------------------------------------------- /app/app.global.scss: -------------------------------------------------------------------------------- 1 | @import '~bourbon/core/_bourbon.scss'; 2 | -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/app.icns -------------------------------------------------------------------------------- /internals/flow/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string 3 | -------------------------------------------------------------------------------- /app/components/CountySelect.css: -------------------------------------------------------------------------------- 1 | .countySelect { 2 | font-size: 2.3rem !important; 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icon.png -------------------------------------------------------------------------------- /.testcafe-electron-rc: -------------------------------------------------------------------------------- 1 | { 2 | "mainWindowUrl": "./app/app.html", 3 | "appPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /app/components/PageContainer.css: -------------------------------------------------------------------------------- 1 | .pageContainer { 2 | padding-bottom: 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /internals/flow/CSSModule.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare export default { [key: string]: string } -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/js.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icon.icns -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /internals/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/flow.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/npm.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/yarn.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/webpack.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/96x96.png -------------------------------------------------------------------------------- /app/assets/icons/win/cfa.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/icons/win/cfa.ico -------------------------------------------------------------------------------- /app/assets/images/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/bullet.png -------------------------------------------------------------------------------- /internals/img/erb-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/erb-banner.png -------------------------------------------------------------------------------- /internals/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/erb-logo.png -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/js-padded.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/512x512.png -------------------------------------------------------------------------------- /app/components/PrivacyPolicyFormCard.css: -------------------------------------------------------------------------------- 1 | .bulletList { 2 | list-style: disc; 3 | padding-left: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /internals/img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/flow-padded.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /app/assets/images/teal_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/teal_check.png -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/flow-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /app/assets/images/cmr_black_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/cmr_black_logo.png -------------------------------------------------------------------------------- /app/assets/images/cmr_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/cmr_logo_black.png -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | *.pdf binary 8 | -------------------------------------------------------------------------------- /app/assets/images/white_check_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/white_check_large.png -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/internals/img/react-router-padded-90.png -------------------------------------------------------------------------------- /app/assets/images/cmr_logo_black_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/BEAR/master/app/assets/images/cmr_logo_black_cropped.png -------------------------------------------------------------------------------- /flow-typed/module_vx.x.x.js: -------------------------------------------------------------------------------- 1 | /* eslint flowtype/no-weak-types: 0 */ 2 | 3 | declare module 'module' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /app/utils/testHelpers.js: -------------------------------------------------------------------------------- 1 | export default function sleep(seconds) { 2 | return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 4 | labels: 'enhancement' 5 | --- 6 | -------------------------------------------------------------------------------- /app/components/DojFileSelectFormCard.css: -------------------------------------------------------------------------------- 1 | .semibold { 2 | font-weight: 500; 3 | } 4 | 5 | .outlineBox { 6 | padding: 20px; 7 | border: 2px solid #f7f5f4; 8 | } 9 | -------------------------------------------------------------------------------- /flow-typed/electronExtensions.js: -------------------------------------------------------------------------------- 1 | declare type ElectronProcess = Process & { resourcesPath: string }; 2 | declare type File = File & { path: string }; 3 | declare var process: ElectronProcess; 4 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | @import '~cfa-styleguide/styles.css'; 2 | 3 | .grid { 4 | max-width: none; /* override 960px max-width from styleguide */ 5 | } 6 | 7 | body { 8 | margin-bottom: 0; 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "rangeStrategy": "bump", 4 | "baseBranches": ["next"], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "dzannotti.vscode-babel-coloring", 5 | "EditorConfig.EditorConfig", 6 | "flowtype.flow-for-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/components/ImpactItem.css: -------------------------------------------------------------------------------- 1 | .impactItem { 2 | text-align: center; 3 | display: inline-block; 4 | width: 33%; 5 | padding: 0 33px; 6 | margin: 20px 0; 7 | } 8 | 9 | .impactNumber { 10 | font-weight: bolder; 11 | } 12 | -------------------------------------------------------------------------------- /app/components/TermsOfServiceFormCard.css: -------------------------------------------------------------------------------- 1 | ol ul { 2 | padding-left: 40px; 3 | } 4 | 5 | li { 6 | padding: 10px; 7 | } 8 | 9 | .header { 10 | text-align: center; 11 | } 12 | .firstIndent { 13 | margin-left: 20px; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/PageHeader.css: -------------------------------------------------------------------------------- 1 | .mainHeader { 2 | padding-top: 0 !important; 3 | padding-bottom: 0 !important; 4 | } 5 | 6 | .logoLink { 7 | display: inline-block; 8 | } 9 | 10 | .mainHeaderLogo { 11 | height: 80px; 12 | } 13 | -------------------------------------------------------------------------------- /internals/scripts/CheckYarn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 4 | console.warn( 5 | "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m" 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "singleQuote": true 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /app/components/DOJFileInput.css: -------------------------------------------------------------------------------- 1 | .emptyMessage { 2 | display: inline-block; 3 | padding-left: 10px; 4 | font-size: 1.6rem; 5 | line-height: 1.5em; 6 | color: #5f5854; 7 | } 8 | 9 | .noBottomMargin { 10 | margin-top: 10px !important; 11 | } 12 | -------------------------------------------------------------------------------- /app/components/DojFileItem.css: -------------------------------------------------------------------------------- 1 | .fileName { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 0; 5 | } 6 | 7 | .fileRemove { 8 | padding-top: 2px; 9 | padding-left: 5px; 10 | cursor: pointer; 11 | } 12 | 13 | .addPadding { 14 | padding: 6px; 15 | } 16 | -------------------------------------------------------------------------------- /app/constants/nonLinearScreenNumbers.js: -------------------------------------------------------------------------------- 1 | // These numbers depend on the order in which the non-linear screens are rendered inside the page container in the home component 2 | const nonLinearScreenNumbers = { 3 | faq: 7, 4 | errorScreen: 8, 5 | termsOfService: 9 6 | }; 7 | 8 | export default nonLinearScreenNumbers; 9 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:testcafe/recommended", 3 | "env": { 4 | "jest/globals": true 5 | }, 6 | "plugins": ["jest", "testcafe"], 7 | "rules": { 8 | "jest/no-disabled-tests": "warn", 9 | "jest/no-focused-tests": "error", 10 | "jest/no-identical-title": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/PageContent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | 4 | type Props = { 5 | children: React.Node 6 | }; 7 | 8 | export default class PageContent extends React.Component { 9 | render() { 10 | const { children } = this.props; 11 | 12 | return
{children}
; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["erb", "prettier"], 3 | "settings": { 4 | "import/resolver": { 5 | "webpack": { 6 | "config": "configs/webpack.config.eslint.js" 7 | } 8 | } 9 | }, 10 | "rules": { 11 | "react/jsx-boolean-value": "off", 12 | "no-underscore-dangle": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/components/Checkbox.css: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | display: inline-block !important; 3 | background-color: #ebfffa !important; 4 | padding: 0 !important; 5 | height: 54px; 6 | width: 54px; 7 | } 8 | 9 | .checkbox:focus-within { 10 | box-shadow: none !important; 11 | } 12 | 13 | .checkbox input[type='checkbox'] { 14 | top: 14px !important; 15 | left: 14px !important; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/IntroductionFormCard.css: -------------------------------------------------------------------------------- 1 | .introductionTitle { 2 | max-width: 600px; 3 | margin: 1em auto !important; 4 | } 5 | 6 | .stepBody { 7 | max-width: 300px; 8 | margin: 0 auto; 9 | } 10 | 11 | .stepTitle { 12 | max-width: 250px; 13 | margin: 0.5em auto; 14 | } 15 | 16 | .contactInfo { 17 | max-width: 550px; 18 | margin-left: auto !important; 19 | margin-right: auto !important; 20 | } 21 | -------------------------------------------------------------------------------- /app/components/FormCardFooter.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Props = { 4 | children: React.Node, 5 | className?: string 6 | }; 7 | 8 | export default class FormCardHeader extends React.Component { 9 | static defaultProps = { className: '' }; 10 | 11 | render() { 12 | const { children, className } = this.props; 13 | 14 | return
{children}
; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/components/__snapshots__/DojFileItem.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DojFileItem component should match exact snapshot 1`] = ` 4 |
7 |

10 | File imported: 11 | file 12 | 17 |

18 |
19 | `; 20 | -------------------------------------------------------------------------------- /app/components/GoBackButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | onGoBack: () => void 6 | }; 7 | 8 | export default class GoBackButton extends Component { 9 | render() { 10 | const { onGoBack } = this.props; 11 | return ( 12 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/FormCardContent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | 4 | type Props = { 5 | children: React.Node, 6 | className?: string 7 | }; 8 | 9 | export default class FormCardContent extends React.Component { 10 | static defaultProps = { className: '' }; 11 | 12 | render() { 13 | const { children, className } = this.props; 14 | 15 | return ( 16 |
{children}
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | 4 | export default function CheckNodeEnv(expectedEnv: string) { 5 | if (!expectedEnv) { 6 | throw new Error('"expectedEnv" not set'); 7 | } 8 | 9 | if (process.env.NODE_ENV !== expectedEnv) { 10 | console.log( 11 | chalk.whiteBright.bgRed.bold( 12 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 13 | ) 14 | ); 15 | process.exit(2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (7/17/19) 2 | 3 | #### Features 4 | 5 | - **Baseline Prop64 Eligibility:** Dismiss or reduce each code section 6 | - **Additional Relief:** Ability to dismiss code sections that would otherwise be reduced, under certain circumstances 7 | - **CSV Output:** Full DOJ input fill with appended analysis in csv format, plus two different condensed csv outputs for easier analysis of results 8 | - **Summary Report Output:** Text report of the overall impact the chosen Prop64 eligibility will have on individuals in the county 9 | -------------------------------------------------------------------------------- /app/components/StartOverButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | onStartOver: () => void 6 | }; 7 | 8 | export default class StartOverButton extends Component { 9 | render() { 10 | const { onStartOver } = this.props; 11 | return ( 12 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/containers/Root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { spawn } from 'child_process'; 4 | import Home from '../components/Home'; 5 | 6 | type Props = {}; 7 | 8 | export default class Root extends Component { 9 | render() { 10 | const preserveEligibilityConfig = 11 | process.env.PRESERVE_ELIGIBILITY_CONFIG === 'true'; 12 | return ( 13 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ImpactItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styles from './ImpactItem.css'; 4 | 5 | type Props = { 6 | value: number, 7 | description: string 8 | }; 9 | 10 | export default class ImpactItem extends Component { 11 | render() { 12 | const { value, description } = this.props; 13 | 14 | return ( 15 |
16 |

{value.toLocaleString()}

17 |

{description}

18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/components/__snapshots__/DojFileInput.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DojFileInput component should match exact snapshot 1`] = ` 4 |
5 | 20 |
21 | `; 22 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | import detectPort from 'detect-port'; 4 | 5 | (function CheckPortInUse() { 6 | const port: string = process.env.PORT || '1212'; 7 | 8 | detectPort(port, (err: ?Error, availablePort: number) => { 9 | if (port !== String(availablePort)) { 10 | throw new Error( 11 | chalk.whiteBright.bgRed.bold( 12 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 13 | ) 14 | ); 15 | } else { 16 | process.exit(0); 17 | } 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /test/components/__snapshots__/ProgressBar.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ProgressBar component should match exact snapshot 1`] = ` 4 |
7 |
10 |
18 |
21 | 0 22 | % 23 |
24 |
25 |
26 | `; 27 | -------------------------------------------------------------------------------- /app/components/FormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import FormCardHeader from './FormCardHeader'; 4 | import FormCardFooter from './FormCardFooter'; 5 | import FormCardContent from './FormCardContent'; 6 | import styles from './FormCard.css'; 7 | 8 | type Props = { 9 | children: React.Node 10 | }; 11 | 12 | export default class FormCard extends React.Component { 13 | render() { 14 | const { children } = this.props; 15 | 16 | return
{children}
; 17 | } 18 | } 19 | 20 | export { FormCardFooter, FormCardContent, FormCardHeader }; 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/node:12.9-browsers 2 | 3 | RUN sudo dpkg --add-architecture i386 4 | RUN whoami 5 | RUN wget -nc https://dl.winehq.org/wine-builds/winehq.key -O $HOME/winehq.key 6 | RUN sudo apt-key add $HOME/winehq.key 7 | RUN sudo sh -c 'echo "deb https://dl.winehq.org/wine-builds/debian/ stretch main" >> /etc/apt/sources.list.d/wine.list' 8 | RUN sudo apt install apt-transport-https 9 | RUN sudo apt update 10 | RUN sudo apt install --install-recommends winehq-stable 11 | RUN echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections 12 | RUN sudo apt-get install -y wine 13 | -------------------------------------------------------------------------------- /app/components/PageFooter.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | margin-top: 100px; 3 | font-size: 10px; 4 | font-weight: bolder; 5 | background-color: #cfc5bf; 6 | } 7 | 8 | .footerContent { 9 | max-width: 960px; 10 | margin-left: auto; 11 | margin-right: auto; 12 | padding-top: 20px; 13 | padding-bottom: 30px; 14 | } 15 | 16 | .footerMedia { 17 | margin-bottom: 0 !important; 18 | } 19 | 20 | .footerMessage { 21 | padding-top: 5px; 22 | } 23 | 24 | .footerLink { 25 | display: inline-block; 26 | margin-right: 50px; 27 | } 28 | 29 | .footerLink:focus { 30 | box-shadow: none; 31 | } 32 | 33 | .logoLink { 34 | display: inline-block; 35 | } 36 | -------------------------------------------------------------------------------- /__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0 */ 2 | 3 | const fs = jest.genMockFromModule('fs'); 4 | 5 | let pathExists = true; 6 | let fileContent = ''; 7 | 8 | function __setExistsSync(exists) { 9 | pathExists = exists; 10 | } 11 | 12 | function __setFileContent(content) { 13 | fileContent = content; 14 | } 15 | 16 | function mockExistsSync() { 17 | return pathExists; 18 | } 19 | 20 | const mockReadFileSync = jest.fn().mockImplementation(() => fileContent); 21 | 22 | fs.__setExistsSync = __setExistsSync; 23 | fs.__setFileContent = __setFileContent; 24 | fs.existsSync = mockExistsSync; 25 | fs.readFileSync = mockReadFileSync; 26 | 27 | module.exports = fs; 28 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | platform: 4 | - x64 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: 10 9 | 10 | cache: 11 | - '%LOCALAPPDATA%/Yarn' 12 | - node_modules 13 | - flow-typed 14 | - '%USERPROFILE%\.electron' 15 | 16 | matrix: 17 | fast_finish: true 18 | 19 | build: off 20 | 21 | version: '{build}' 22 | 23 | shallow_clone: true 24 | 25 | clone_depth: 1 26 | 27 | install: 28 | - ps: Install-Product node $env:nodejs_version x64 29 | - set CI=true 30 | - yarn 31 | 32 | test_script: 33 | - yarn package-ci 34 | - yarn lint 35 | # - yarn flow 36 | - yarn test 37 | - yarn build-e2e 38 | - yarn test-e2e 39 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pr 8 | - discussion 9 | - e2e 10 | - enhancement 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /app/components/FormCardHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | 4 | type Props = { 5 | children: string | React.Node, 6 | helpText?: string 7 | }; 8 | 9 | export default class FormCardHeader extends React.Component { 10 | static defaultProps = { 11 | helpText: '' 12 | }; 13 | 14 | render() { 15 | const { children, helpText } = this.props; 16 | let helpTextDiv; 17 | 18 | if (helpText) { 19 | helpTextDiv =
{helpText}
; 20 | } 21 | 22 | return ( 23 |
24 |

{children}

25 | {helpTextDiv} 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/components/__snapshots__/NumberSelect.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NumberSelect component should match exact snapshot 1`] = ` 4 |
5 |
35 | `; 36 | -------------------------------------------------------------------------------- /app/utils/osHelpers.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | export default function openFolder(path: string): boolean { 4 | if (process.platform === 'darwin') { 5 | return openOnMac(path); 6 | } 7 | return openOnWindows(path); 8 | } 9 | 10 | export function openOnMac(path: string): boolean { 11 | exec(`open ${path}`, err => { 12 | if (err) { 13 | console.log('cannot open folder at path:', path); 14 | return false; 15 | } 16 | }); 17 | return true; 18 | } 19 | 20 | export function openOnWindows(path: string): boolean { 21 | exec(`start c:${path} `, err => { 22 | if (err) { 23 | console.log('cannot open folder at path:', path); 24 | return false; 25 | } 26 | }); 27 | return true; 28 | } 29 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; 4 | import Root from './containers/Root'; 5 | import './app.global.css'; 6 | 7 | const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; 8 | 9 | render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | 16 | if (module.hot) { 17 | module.hot.accept('./containers/Root', () => { 18 | // eslint-disable-next-line global-require 19 | const NextRoot = require('./containers/Root').default; 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app/components/FaqFormCard.css: -------------------------------------------------------------------------------- 1 | .headerText { 2 | font-weight: normal; 3 | font-size: medium; 4 | line-height: 1.4em; 5 | } 6 | 7 | .accordionHeading { 8 | text-decoration: underline; 9 | color: #008060; 10 | cursor: pointer; 11 | padding-bottom: 18px; 12 | } 13 | 14 | .accordionHeading:focus { 15 | outline: none; 16 | } 17 | 18 | .accordionHeading div:focus { 19 | outline: none; 20 | } 21 | 22 | .accordionPanel { 23 | padding: 0 0 20px 20px; 24 | animation: fadein 0.35s ease-in; 25 | } 26 | 27 | .numberedList { 28 | list-style-type: decimal; 29 | padding-left: 40px; 30 | } 31 | 32 | .bulletedList { 33 | list-style-type: disc; 34 | padding-left: 40px; 35 | } 36 | 37 | @keyframes fadein { 38 | 0% { 39 | opacity: 0; 40 | } 41 | 42 | 100% { 43 | opacity: 1; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/components/__snapshots__/FormCardHeader.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FormCardHeader component should match exact snapshot 1`] = ` 4 |
5 |
8 |

11 | Hello! This is a title. 12 |

13 |
14 |
15 | `; 16 | 17 | exports[`FormCardHeader component with optional helper text should match exact snapshot 1`] = ` 18 |
19 |
22 |

25 | Hello! This is a title. 26 |

27 |
30 | this is some helper text 31 |
32 |
33 |
34 | `; 35 | -------------------------------------------------------------------------------- /app/components/RadioButton.css: -------------------------------------------------------------------------------- 1 | .radioButtonWrapper { 2 | display: inline-block !important; 3 | padding: 0 !important; 4 | height: 54px; 5 | width: 54px; 6 | border: 2px solid black; 7 | } 8 | 9 | .radioButtonWrapper:focus-within { 10 | box-shadow: none !important; 11 | } 12 | 13 | .selected { 14 | background-color: #ebfffa !important; 15 | } 16 | 17 | .radioButton { 18 | -webkit-appearance: none; 19 | top: 14px; 20 | left: 14px; 21 | height: 22px; 22 | width: 22px; 23 | padding: 0.3em !important; 24 | border: 2px solid #30302f !important; 25 | border-radius: 50% !important; 26 | } 27 | 28 | .radioButton:checked::before { 29 | content: ''; 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | margin-top: -6px; 34 | margin-left: -6px; 35 | height: 12px; 36 | width: 12px; 37 | border-radius: 50%; 38 | background-color: #017f6a; 39 | } 40 | -------------------------------------------------------------------------------- /app/components/ContinueButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | onContinue: () => void, 6 | disabled: boolean 7 | }; 8 | 9 | export default class ContinueButton extends Component { 10 | classes = () => { 11 | const buttonClasses = 'button button--primary'; 12 | 13 | const { disabled } = this.props; 14 | if (disabled) { 15 | return buttonClasses.concat(' button--disabled'); 16 | } 17 | return buttonClasses; 18 | }; 19 | 20 | render() { 21 | const { onContinue, disabled } = this.props; 22 | return ( 23 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/utils/fileUtils.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { createJsonFile, getDateTime } from '../../app/utils/fileUtils'; 3 | 4 | jest.mock('fs'); 5 | 6 | afterEach(() => { 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | describe('createJsonFile', () => { 11 | it('writes json data to a new file', () => { 12 | createJsonFile({ hello: 'goodbye' }, './file'); 13 | expect(fs.writeFileSync.mock.calls.length).toEqual(1); 14 | expect(fs.writeFileSync.mock.calls[0]).toEqual([ 15 | './file', 16 | '{"hello":"goodbye"}' 17 | ]); 18 | }); 19 | }); 20 | 21 | describe('getDateTime', () => { 22 | it('converts date to formatted string, including seconds', () => { 23 | const date = new Date('01 Feb 1970 01:20:30'); // in local timezone 24 | const convertedDate = getDateTime(date); 25 | 26 | expect(convertedDate).toMatch('02_01_1970_01.20.30.AM'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /app/constants/defaultAnalysisOptions.js: -------------------------------------------------------------------------------- 1 | const defaultAnalysisOptions = { 2 | currentScreen: 0, 3 | previousScreenInFlow: 0, 4 | county: { name: '', code: '' }, 5 | baselineEligibilityOptions: { 6 | '11357': 'dismiss', 7 | '11358': 'dismiss', 8 | '11359': 'dismiss', 9 | '11360': 'dismiss' 10 | }, 11 | additionalReliefOptions: { 12 | subjectUnder21AtConviction: false, 13 | dismissOlderThanAgeThreshold: false, 14 | subjectAgeThreshold: 0, 15 | dismissYearsSinceConvictionThreshold: true, 16 | yearsSinceConvictionThreshold: 5, 17 | dismissYearsCrimeFreeThreshold: true, 18 | yearsCrimeFreeThreshold: 5, 19 | subjectHasOnlyProp64Charges: true, 20 | subjectIsDeceased: false 21 | }, 22 | impactStatistics: { 23 | noFelony: 0, 24 | noConvictionLast7: 0, 25 | noConviction: 0 26 | } 27 | }; 28 | 29 | export default defaultAnalysisOptions; 30 | -------------------------------------------------------------------------------- /test/components/__snapshots__/RadioButton.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RadioButton component should match exact snapshot when selected is false 1`] = ` 4 | 17 | `; 18 | 19 | exports[`RadioButton component should match exact snapshot when selected is true 1`] = ` 20 | 33 | `; 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".babelrc": "jsonc", 4 | ".eslintrc": "jsonc", 5 | ".prettierrc": "jsonc", 6 | 7 | ".stylelintrc": "json", 8 | 9 | ".dockerignore": "ignore", 10 | ".eslintignore": "ignore", 11 | ".flowconfig": "ignore" 12 | }, 13 | 14 | "javascript.validate.enable": false, 15 | "javascript.format.enable": false, 16 | "typescript.validate.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "flow.useNPMPackagedFlow": true, 20 | "search.exclude": { 21 | ".git": true, 22 | ".eslintcache": true, 23 | "app/dist": true, 24 | "app/main.prod.js": true, 25 | "app/main.prod.js.map": true, 26 | "bower_components": true, 27 | "dll": true, 28 | "flow-typed": true, 29 | "release": true, 30 | "node_modules": true, 31 | "npm-debug.log.*": true, 32 | "test/**/__snapshots__": true, 33 | "yarn.lock": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/components/ErrorSection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | header: string, 6 | errors: Errors, 7 | showFile: boolean 8 | }; 9 | 10 | const preStyle = { 11 | outline: 'auto thin', 12 | textAlign: 'left', 13 | whiteSpace: 'pre-wrap' 14 | }; 15 | 16 | const headerStyle = { 17 | textAlign: 'left' 18 | }; 19 | 20 | export default class ErrorSection extends Component { 21 | render() { 22 | const { header, errors, showFile } = this.props; 23 | return ( 24 |
25 |

{header}

26 |
27 |           {Object.keys(errors).map(item => (
28 |             

29 | {showFile 30 | ? `${item}: ${errors[item].errorMessage}` 31 | : `${errors[item].errorMessage}`} 32 |

33 | ))} 34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/PageContainer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import PageHeader from './PageHeader'; 4 | import PageContent from './PageContent'; 5 | import PageFooter from './PageFooter'; 6 | import styles from './PageContainer.css'; 7 | 8 | type Props = { 9 | children: Array, 10 | goToScreen: number => void, 11 | currentScreen: number, 12 | onStartOver: () => void 13 | }; 14 | 15 | export default class PageContainer extends React.Component { 16 | render() { 17 | const { children, goToScreen, currentScreen, onStartOver } = this.props; 18 | return ( 19 |
20 | 21 | {children[currentScreen]} 22 | 23 |
24 | ); 25 | } 26 | } 27 | 28 | export { PageFooter, PageContent, PageHeader }; 29 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuiltsExist.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Check if the renderer and main bundles are built 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import fs from 'fs'; 6 | 7 | function CheckBuildsExist() { 8 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 9 | const rendererPath = path.join( 10 | __dirname, 11 | '..', 12 | '..', 13 | 'app', 14 | 'dist', 15 | 'renderer.prod.js' 16 | ); 17 | 18 | if (!fs.existsSync(mainPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The main process is not built yet. Build it by running "yarn build-main"' 22 | ) 23 | ); 24 | } 25 | 26 | if (!fs.existsSync(rendererPath)) { 27 | throw new Error( 28 | chalk.whiteBright.bgRed.bold( 29 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 30 | ) 31 | ); 32 | } 33 | } 34 | 35 | CheckBuildsExist(); 36 | -------------------------------------------------------------------------------- /test/utils/writeSummaryOutputUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { allEligibleConvictionsDismissed } from '../../app/utils/writeSummaryOutputUtils'; 2 | 3 | describe('allEligibleConvictionsDismissed', () => { 4 | describe('when baseline eligibility specifies to dismiss all', () => { 5 | it('returns true', () => { 6 | const transformedEligibilityOptions = { 7 | baselineEligibility: { dismiss: ['11357'], reduce: [] } 8 | }; 9 | expect( 10 | allEligibleConvictionsDismissed(transformedEligibilityOptions) 11 | ).toEqual(true); 12 | }); 13 | }); 14 | 15 | describe('when baseline eligibility specifies to reduce at least one code', () => { 16 | it('returns false', () => { 17 | const transformedEligibilityOptions = { 18 | baselineEligibility: { dismiss: [], reduce: ['11357'] } 19 | }; 20 | expect( 21 | allEligibleConvictionsDismissed(transformedEligibilityOptions) 22 | ).toEqual(false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/components/__snapshots__/BaselineEligibilityOption.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaselineEligibilityOption component should match exact snapshot 1`] = ` 4 | 5 | 6 | H&S § undefined 7 | 8 | 9 | 22 | 23 | 24 | 37 | 38 | 39 | `; 40 | -------------------------------------------------------------------------------- /test/components/ContinueButton.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { shallow } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import ContinueButton from '../../app/components/ContinueButton'; 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | describe('ContinueButton component', () => { 9 | it('can be enabled', () => { 10 | const component = shallow( 11 | {}} disabled={false} /> 12 | ); 13 | 14 | expect(component.find('button').props().disabled).toBe(false); 15 | expect(component.find('button').props().className).not.toContain( 16 | 'button--disabled' 17 | ); 18 | }); 19 | 20 | it('can be disabled', () => { 21 | const component = shallow( 22 | {}} disabled={true} /> 23 | ); 24 | 25 | expect(component.find('button').props().disabled).toBe(true); 26 | expect(component.find('button').props().className).toContain( 27 | 'button--disabled' 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /app/main.prod.js 3 | /app/main.prod.js.map 4 | /app/dist/.* 5 | /resources/.* 6 | /node_modules/webpack-cli 7 | /release/.* 8 | /dll/.* 9 | /release/.* 10 | /git/.* 11 | 12 | [include] 13 | 14 | [libs] 15 | 16 | [options] 17 | esproposal.class_static_fields=enable 18 | esproposal.class_instance_fields=enable 19 | esproposal.export_star_as=enable 20 | module.name_mapper.extension='css' -> '/internals/flow/CSSModule.js.flow' 21 | module.name_mapper.extension='styl' -> '/internals/flow/CSSModule.js.flow' 22 | module.name_mapper.extension='scss' -> '/internals/flow/CSSModule.js.flow' 23 | module.name_mapper.extension='png' -> '/internals/flow/WebpackAsset.js.flow' 24 | module.name_mapper.extension='jpg' -> '/internals/flow/WebpackAsset.js.flow' 25 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 26 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | .*.dockerfile -------------------------------------------------------------------------------- /app/components/RadioButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styles from './RadioButton.css'; 4 | 5 | type Props = { 6 | selected: boolean, 7 | value: string, 8 | group: string, 9 | onSelect: (string, string) => void 10 | }; 11 | 12 | export default class RadioButton extends Component { 13 | handleSelect = () => { 14 | const { onSelect, group, value } = this.props; 15 | onSelect(group, value); 16 | }; 17 | 18 | render() { 19 | const { selected, value, group } = this.props; 20 | 21 | return ( 22 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | 53 | gogen 54 | gogen.exe 55 | 56 | -------------------------------------------------------------------------------- /app/components/PageHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import cmrLogo from '../assets/images/cmr_black_logo.png'; 4 | import styles from './PageHeader.css'; 5 | 6 | type Props = { 7 | onStartOver: () => void 8 | }; 9 | 10 | export default class PageHeader extends Component { 11 | onClickStartOver = (event: Event) => { 12 | const { onStartOver } = this.props; 13 | event.preventDefault(); 14 | onStartOver(); 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 |
21 |
22 | 27 | Clear My Record logo 32 | 33 |
34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/DojFileItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import path from 'path'; 4 | import styles from './DojFileItem.css'; 5 | 6 | type Props = { 7 | filePath: string, 8 | onFileRemove: string => void 9 | }; 10 | 11 | export default class DojFileItem extends Component { 12 | formatFileName = () => { 13 | const { filePath } = this.props; 14 | return filePath.split(path.sep).pop(); 15 | }; 16 | 17 | fileRemove = () => { 18 | const { filePath, onFileRemove } = this.props; 19 | onFileRemove(filePath); 20 | }; 21 | 22 | render() { 23 | return ( 24 |
25 |

26 | File imported: {this.formatFileName()} 27 | {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus */} 28 | 33 |

34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # flow-typed 34 | flow-typed/npm/* 35 | !flow-typed/npm/module_vx.x.x.js 36 | 37 | # App packaged 38 | release 39 | app/main.prod.js 40 | app/main.prod.js.map 41 | app/renderer.prod.js 42 | app/renderer.prod.js.map 43 | app/style.css 44 | app/style.css.map 45 | dist 46 | dll 47 | main.js 48 | main.js.map 49 | 50 | .idea 51 | npm-debug.log.* 52 | __snapshots__ 53 | 54 | # Package.json 55 | package.json 56 | .travis.yml 57 | -------------------------------------------------------------------------------- /app/utils/writeSummaryOutputUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import ReactPDF from '@react-pdf/renderer'; 4 | import React from 'react'; 5 | import SummaryReportPdf from '../components/SummaryReportPdf'; 6 | 7 | export function writeSummaryReport( 8 | summaryData: GogenSummaryData, 9 | outputFilePath: string, 10 | dojFilePaths: Array, 11 | formattedEligibilityOptions: BaselineEligibilityConfiguration, 12 | formattedGogenRunTime: string 13 | ) { 14 | ReactPDF.render( 15 | , 23 | path.join(outputFilePath, `Summary_Report_${formattedGogenRunTime}.pdf`) 24 | ); 25 | } 26 | 27 | export function allEligibleConvictionsDismissed( 28 | transformedEligibilityOptions: BaselineEligibilityConfiguration 29 | ) { 30 | return transformedEligibilityOptions.baselineEligibility.reduce.length === 0; 31 | } 32 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { dependencies } from '../package.json'; 8 | 9 | export default { 10 | externals: [...Object.keys(dependencies || {})], 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsx?$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | cacheDirectory: true 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | 27 | output: { 28 | path: path.join(__dirname, '..', 'app'), 29 | // https://github.com/webpack/webpack/issues/1114 30 | libraryTarget: 'commonjs2' 31 | }, 32 | 33 | /** 34 | * Determine the array of extensions that should be used to resolve modules. 35 | */ 36 | resolve: { 37 | extensions: ['.js', '.jsx', '.json'] 38 | }, 39 | 40 | plugins: [ 41 | new webpack.EnvironmentPlugin({ 42 | NODE_ENV: 'production' 43 | }), 44 | 45 | new webpack.NamedModulesPlugin() 46 | ] 47 | }; 48 | -------------------------------------------------------------------------------- /app/constants/californiaCounties.js: -------------------------------------------------------------------------------- 1 | const counties = [ 2 | 'Alameda', 3 | 'Alpine', 4 | 'Amador', 5 | 'Butte', 6 | 'Calaveras', 7 | 'Colusa', 8 | 'Contra Costa', 9 | 'Del Norte', 10 | 'El Dorado', 11 | 'Fresno', 12 | 'Glenn', 13 | 'Humboldt', 14 | 'Imperial', 15 | 'Inyo', 16 | 'Kern', 17 | 'Kings', 18 | 'Lake', 19 | 'Lassen', 20 | 'Los Angeles', 21 | 'Madera', 22 | 'Marin', 23 | 'Mariposa', 24 | 'Mendocino', 25 | 'Merced', 26 | 'Modoc', 27 | 'Mono', 28 | 'Monterey', 29 | 'Napa', 30 | 'Nevada', 31 | 'Orange', 32 | 'Placer', 33 | 'Plumas', 34 | 'Riverside', 35 | 'Sacramento', 36 | 'San Benito', 37 | 'San Bernardino', 38 | 'San Diego', 39 | 'San Francisco', 40 | 'San Joaquin', 41 | 'San Luis Obispo', 42 | 'San Mateo', 43 | 'Santa Barbara', 44 | 'Santa Clara', 45 | 'Santa Cruz', 46 | 'Shasta', 47 | 'Sierra', 48 | 'Siskiyou', 49 | 'Solano', 50 | 'Sonoma', 51 | 'Stanislaus', 52 | 'Sutter', 53 | 'Tehama', 54 | 'Trinity', 55 | 'Tulare', 56 | 'Tuolumne', 57 | 'Ventura', 58 | 'Yolo', 59 | 'Yuba' 60 | ]; 61 | 62 | export default counties; 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Code for America 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app/components/Checkbox.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import styles from './Checkbox.css'; 4 | 5 | type Props = { 6 | checked: boolean, 7 | children: React.Node, 8 | group: string, 9 | labelText: string, 10 | onChange: string => void 11 | }; 12 | 13 | export default class Checkbox extends React.Component { 14 | handleChange = () => { 15 | const { onChange, group } = this.props; 16 | onChange(group); 17 | }; 18 | 19 | render() { 20 | const { checked, group, labelText, children } = this.props; 21 | 22 | return ( 23 |
24 |
25 | {children} 26 |
27 | 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/components/BaselineEligibilityOption.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import RadioButton from './RadioButton'; 4 | 5 | type Props = { 6 | codeSection: string, 7 | codeSectionDescription: string, 8 | baselineEligibilityOptions: BaselineEligibilityOptions, 9 | onEligibilityOptionSelect: (string, string) => void 10 | }; 11 | 12 | export default class BaselineEligibilityOption extends Component { 13 | render() { 14 | const { 15 | codeSection, 16 | codeSectionDescription, 17 | baselineEligibilityOptions, 18 | onEligibilityOptionSelect 19 | } = this.props; 20 | return ( 21 | 22 | {`H&S § ${codeSectionDescription}`} 23 | 24 | 30 | 31 | 32 | 38 | 39 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/components/CountySelectFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import FormCard, { 5 | FormCardContent, 6 | FormCardFooter, 7 | FormCardHeader 8 | } from './FormCard'; 9 | import CountySelect from './CountySelect'; 10 | import ContinueButton from './ContinueButton'; 11 | 12 | type Props = { 13 | selectedCounty: County, 14 | onCountySelect: County => void, 15 | onCountyConfirm: void => void 16 | }; 17 | 18 | export default class CountySelectFormCard extends Component { 19 | componentDidMount() { 20 | window.scrollTo(0, 0); 21 | } 22 | 23 | render() { 24 | const { onCountySelect, onCountyConfirm, selectedCounty } = this.props; 25 | return ( 26 | 27 | 28 | CA County Selection 29 | 30 | 31 | 35 | 36 | 37 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/components/IntroductionFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import IntroductionFormCard from '../../app/components/IntroductionFormCard'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup() { 12 | const onBeginSpy = sandbox.spy(); 13 | const component = shallow(); 14 | return { component, onBeginSpy }; 15 | } 16 | 17 | afterEach(() => { 18 | sandbox.restore(); 19 | }); 20 | 21 | describe('IntroductionFormCard component', () => { 22 | describe('clicking the "Got it!" button', () => { 23 | it('calls the onBegin function', () => { 24 | const { component, onBeginSpy } = setup(); 25 | const beginButton = component.find('#begin'); 26 | beginButton.simulate('click'); 27 | expect(onBeginSpy.called).toBe(true); 28 | expect(onBeginSpy.callCount).toEqual(1); 29 | }); 30 | }); 31 | 32 | it('should match exact snapshot', () => { 33 | const component = ( 34 |
35 | 36 |
37 | ); 38 | 39 | const tree = renderer.create(component).toJSON(); 40 | 41 | expect(tree).toMatchSnapshot(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/components/CountySelect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styles from './CountySelect.css'; 4 | 5 | import counties from '../constants/californiaCounties'; 6 | 7 | type Props = { 8 | onCountySelect: County => void, 9 | selectedCounty: string 10 | }; 11 | 12 | export default class CountySelect extends Component { 13 | handleCountySelect = (e: SyntheticEvent) => { 14 | const { onCountySelect } = this.props; 15 | onCountySelect({ 16 | name: e.currentTarget.selectedOptions[0].text, 17 | code: e.currentTarget.value 18 | }); 19 | }; 20 | 21 | render() { 22 | const { selectedCounty } = this.props; 23 | return ( 24 |
25 |

Choose your county

26 |
27 | 42 |
43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clear My Record 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Checkbox.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Checkbox component should match exact snapshot when NOT checked 1`] = ` 4 |
7 |
10 | This is a checkbox 11 |
12 | 30 |
31 | `; 32 | 33 | exports[`Checkbox component should match exact snapshot when checked 1`] = ` 34 |
37 |
40 | This is a checkbox 41 |
42 | 60 |
61 | `; 62 | -------------------------------------------------------------------------------- /app/components/FormCard.css: -------------------------------------------------------------------------------- 1 | .formCard { 2 | max-width: 960px; 3 | margin-top: 20px; 4 | margin-left: auto !important; 5 | margin-right: auto !important; 6 | } 7 | 8 | .formCardFooter { 9 | margin-top: 0; 10 | } 11 | 12 | .formCardContentFullWidth { 13 | margin-left: -38px !important; 14 | margin-right: -38px !important; 15 | } 16 | 17 | .formCardNoBottomMargin { 18 | margin-bottom: -40px; 19 | } 20 | 21 | .dataTable { 22 | margin-top: -2em; 23 | } 24 | 25 | /* hacky thing to get table in place */ 26 | .dataTableFirst { 27 | margin-top: -2.1em; 28 | margin-bottom: 1.6em; 29 | } 30 | 31 | .dataTableSecond { 32 | margin-bottom: 0; 33 | } 34 | 35 | .dataTableContent { 36 | padding-top: 1.6em !important; 37 | padding-bottom: 1.6em !important; 38 | } 39 | 40 | .dataTable thead { 41 | border-bottom: none !important; 42 | } 43 | 44 | .dataTable th { 45 | border-bottom: 2px solid #f7f5f4 !important; 46 | } 47 | 48 | .dataTable tbody { 49 | border-bottom: 2px solid #f7f5f4 !important; 50 | } 51 | 52 | .dataTable caption { 53 | color: black !important; 54 | font-weight: 600 !important; 55 | border-bottom: 2px solid #f7f5f4 !important; 56 | border-top: 2px solid #eaeae9 !important; 57 | background-color: #f7f5f4 !important; 58 | display: block-start; 59 | text-align: left; 60 | padding-left: 40px; 61 | padding-top: 0.5em; 62 | padding-bottom: 0.5em; 63 | } 64 | 65 | .dataTable td { 66 | line-height: initial !important; 67 | border: none !important; 68 | } 69 | 70 | .dataTable td:first-child { 71 | font-weight: normal !important; 72 | } 73 | -------------------------------------------------------------------------------- /test/components/CountySelect.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import CountySelect from '../../app/components/CountySelect'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup() { 12 | const onCountySelectSpy = sandbox.spy(); 13 | const component = shallow( 14 | 15 | ); 16 | return { 17 | component, 18 | onCountySelectSpy 19 | }; 20 | } 21 | 22 | afterEach(() => { 23 | sandbox.restore(); 24 | }); 25 | 26 | describe('CountySelect component', () => { 27 | it('selecting county should call onCountySelect with county name and value', () => { 28 | const { component, onCountySelectSpy } = setup(); 29 | const countySelect = component.find('#county-select'); 30 | const fakeEvent = { 31 | currentTarget: { 32 | value: 'CONTRA COSTA', 33 | selectedOptions: [{ text: 'Contra Costa' }] 34 | } 35 | }; 36 | countySelect.simulate('change', fakeEvent); 37 | expect(onCountySelectSpy.called).toBe(true); 38 | expect(onCountySelectSpy.args[0][0]).toEqual({ 39 | name: 'Contra Costa', 40 | code: 'CONTRA COSTA' 41 | }); 42 | }); 43 | 44 | it('should match exact snapshot', () => { 45 | const { component } = setup(); 46 | const tree = renderer.create(component).toJSON(); 47 | 48 | expect(tree).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /app/components/DojFileInput.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styles from './DOJFileInput.css'; 4 | 5 | type Props = { 6 | onFileSelect: string => void, 7 | isFilepathEmpty: boolean 8 | }; 9 | 10 | export default class DojFileInput extends Component { 11 | handleFileSelection = (event: SyntheticEvent) => { 12 | const { onFileSelect } = this.props; 13 | const selectedFilesArray = event.currentTarget.files; 14 | Array.from(selectedFilesArray).forEach(file => { 15 | onFileSelect(file.path); 16 | }); 17 | // eslint-disable-next-line no-param-reassign 18 | event.currentTarget.value = ''; 19 | }; 20 | 21 | renderNoFileMessage = () => { 22 | const { isFilepathEmpty } = this.props; 23 | 24 | if (isFilepathEmpty) { 25 | return ( 26 |

27 | No file selected 28 |

29 | ); 30 | } 31 | }; 32 | 33 | render() { 34 | return ( 35 |
36 | 51 | {this.renderNoFileMessage()} 52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/components/DojFileItem.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import DojFileItem from '../../app/components/DojFileItem'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(filePath) { 12 | const onFileRemoveSpy = sandbox.spy(); 13 | const component = shallow( 14 | 15 | ); 16 | return { 17 | component, 18 | onFileRemoveSpy 19 | }; 20 | } 21 | 22 | afterEach(() => { 23 | sandbox.restore(); 24 | }); 25 | 26 | describe('DojFileItem component', () => { 27 | it('should display the file name from path', () => { 28 | const { component } = setup('path/to/file.dat'); 29 | const fileName = component.find('.fileName'); 30 | expect(fileName.text()).toEqual('File imported: file.dat'); 31 | }); 32 | 33 | it('should call onFileRemove with the clicked path when the close icon is clicked', () => { 34 | const { component, onFileRemoveSpy } = setup('path/to/file.dat'); 35 | component.find('.icon-close').simulate('click'); 36 | expect(onFileRemoveSpy.called).toBe(true); 37 | const { args } = onFileRemoveSpy.getCall(0); 38 | expect(args[0]).toEqual('path/to/file.dat'); 39 | }); 40 | 41 | it('should match exact snapshot', () => { 42 | const { component } = setup('path/to/file'); 43 | const tree = renderer.create(component).toJSON(); 44 | 45 | expect(tree).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/components/FormCardHeader.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import FormCardHeader from '../../app/components/FormCardHeader'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(helpText) { 12 | const component = shallow(); 13 | return { component }; 14 | } 15 | 16 | afterEach(() => { 17 | sandbox.restore(); 18 | }); 19 | 20 | describe('FormCardHeader component', () => { 21 | it('should match exact snapshot', () => { 22 | const component = ( 23 |
24 | Hello! This is a title. 25 |
26 | ); 27 | 28 | const tree = renderer.create(component).toJSON(); 29 | 30 | expect(tree).toMatchSnapshot(); 31 | }); 32 | 33 | describe('with optional helper text', () => { 34 | it('displays the help text', () => { 35 | const { component } = setup('my help text'); 36 | const helpDiv = component.find('.text--help'); 37 | 38 | expect(helpDiv.text()).toEqual('my help text'); 39 | }); 40 | 41 | it('should match exact snapshot', () => { 42 | const component = ( 43 |
44 | 45 | Hello! This is a title. 46 | 47 |
48 | ); 49 | 50 | const tree = renderer.create(component).toJSON(); 51 | 52 | expect(tree).toMatchSnapshot(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /app/utils/fileUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs'; 3 | 4 | export function createJsonFile( 5 | jsonData: EligibilityConfiguration, 6 | fileName: string 7 | ) { 8 | const jsonString = JSON.stringify(jsonData); 9 | fs.writeFileSync(fileName, jsonString); 10 | } 11 | 12 | export function getDateTime(dateToConvert: Date) { 13 | const month = dateToConvert.getMonth() + 1; 14 | const day = dateToConvert.getDate(); 15 | const year = dateToConvert.getFullYear(); 16 | let hours = dateToConvert.getHours(); 17 | const minutes = dateToConvert.getMinutes(); 18 | const seconds = dateToConvert.getSeconds(); 19 | let ampm = 'AM'; 20 | 21 | if (hours >= 12) { 22 | if (hours > 12) hours -= 12; 23 | ampm = 'PM'; 24 | } 25 | 26 | return `${twoDigitString(month)}_${twoDigitString(day)}_${twoDigitString( 27 | year 28 | )}_${twoDigitString(hours)}.${twoDigitString(minutes)}.${twoDigitString( 29 | seconds 30 | )}.${ampm}`; 31 | } 32 | 33 | function twoDigitString(int: number) { 34 | if (int < 10) { 35 | return `0${int}`; 36 | } 37 | return int.toString(); 38 | } 39 | 40 | export function getFileSize(pathname: string) { 41 | return fs.statSync(pathname).size; 42 | } 43 | 44 | export function makeDirectory(pathToDirectory: string) { 45 | if (!fs.existsSync(pathToDirectory)) { 46 | fs.mkdirSync(pathToDirectory, { recursive: true }); 47 | } 48 | } 49 | 50 | export function deleteDirectoryRecursive(directoryPath: string) { 51 | if (fs.existsSync(directoryPath)) { 52 | fs.readdirSync(directoryPath).forEach(file => { 53 | const curPath = `${directoryPath}/${file}`; 54 | if (fs.lstatSync(curPath).isDirectory()) { 55 | deleteDirectoryRecursive(curPath); 56 | } else { 57 | fs.unlinkSync(curPath); 58 | } 59 | }); 60 | fs.rmdirSync(directoryPath); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/components/DojFileInput.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import DojFileInput from '../../app/components/DojFileInput'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup() { 12 | const onFileSelectSpy = sandbox.spy(); 13 | const component = shallow(); 14 | return { 15 | component, 16 | onFileSelectSpy 17 | }; 18 | } 19 | 20 | afterEach(() => { 21 | sandbox.restore(); 22 | }); 23 | 24 | describe('DojFileInput component', () => { 25 | it('selecting file should call onFileSelect with file path', () => { 26 | const { component, onFileSelectSpy } = setup(); 27 | const fileInput = component.find('#doj-file-input'); 28 | const fakeEvent = { 29 | currentTarget: { 30 | files: [{ path: 'path/to/file' }] 31 | } 32 | }; 33 | fileInput.simulate('change', fakeEvent); 34 | expect(onFileSelectSpy.called).toBe(true); 35 | const { args } = onFileSelectSpy.getCall(0); 36 | expect(args[0]).toEqual('path/to/file'); 37 | }); 38 | 39 | describe('handleFileSelection', () => { 40 | it('clears the input value', () => { 41 | const { component } = setup(); 42 | const fakeEvent = { 43 | currentTarget: { 44 | value: 'path/to/file', 45 | files: [{ path: 'path/to/file' }] 46 | } 47 | }; 48 | component.instance().handleFileSelection(fakeEvent); 49 | expect(fakeEvent.currentTarget.value).toEqual(''); 50 | }); 51 | }); 52 | 53 | it('should match exact snapshot', () => { 54 | const { component } = setup(); 55 | const tree = renderer.create(component).toJSON(); 56 | 57 | expect(tree).toMatchSnapshot(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/components/NumberSelect.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import NumberSelect from '../../app/components/NumberSelect'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup() { 12 | const onNumberSelectSpy = sandbox.spy(); 13 | const component = shallow( 14 | 20 | ); 21 | return { 22 | component, 23 | onNumberSelectSpy 24 | }; 25 | } 26 | 27 | afterEach(() => { 28 | sandbox.restore(); 29 | }); 30 | 31 | describe('NumberSelect component', () => { 32 | it('selecting a number should call onNumberSelect with numeric value', () => { 33 | const { component, onNumberSelectSpy } = setup(); 34 | const ageSelect = component.find('#age-select'); 35 | const fakeEvent = { 36 | currentTarget: { 37 | value: '3', 38 | selectedOptions: [{ text: '3' }] 39 | } 40 | }; 41 | ageSelect.simulate('change', fakeEvent); 42 | expect(onNumberSelectSpy.called).toBe(true); 43 | expect(onNumberSelectSpy.args[0][1]).toEqual(3); 44 | }); 45 | 46 | it('should populate dropdown with numbers between max and min', () => { 47 | const { component } = setup(); 48 | const ageSelect = component.find('#age-select'); 49 | const ageOptions = ageSelect.find('option').map(opt => { 50 | return opt.props().value; 51 | }); 52 | expect(ageOptions).toEqual([1, 2, 3]); 53 | }); 54 | 55 | it('should match exact snapshot', () => { 56 | const { component } = setup(); 57 | const tree = renderer.create(component).toJSON(); 58 | 59 | expect(tree).toMatchSnapshot(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /app/components/NumberSelect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | labelText: string, 6 | group: string, 7 | minNumber: number, 8 | maxNumber: number, 9 | onNumberSelect: (string, number) => void, 10 | selectedNumber: number 11 | }; 12 | 13 | export default class NumberSelect extends Component { 14 | handleNumberSelect = (e: SyntheticEvent) => { 15 | const { group, onNumberSelect } = this.props; 16 | const selectedValue = parseInt(e.currentTarget.selectedOptions[0].text, 10); 17 | onNumberSelect(group, selectedValue); 18 | }; 19 | 20 | numberRange = (numberThreshold: number, maximumNumber: number) => { 21 | const list = []; 22 | // eslint-disable-next-line no-plusplus 23 | for (let i = numberThreshold; i <= maximumNumber; i++) { 24 | list.push(i); 25 | } 26 | return list; 27 | }; 28 | 29 | render() { 30 | const { 31 | labelText, 32 | group, 33 | minNumber, 34 | maxNumber, 35 | selectedNumber 36 | } = this.props; 37 | return ( 38 |
39 | {/* Below keeps failing linter despite having associated control (and passing screenreader) */} 40 | {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} 41 | {/* eslint-disable-next-line jsx-a11y/label-has-for */} 42 | 45 |
46 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/utils/formatSummaryOutputUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | formatLineCountWithCommas, 3 | formattedProcessingTime, 4 | convertTimestamp 5 | } from '../../app/utils/formatSummaryOutputUtils'; 6 | 7 | describe('formatLineCountWithCommas', () => { 8 | describe('when the input number is greater than 1000', () => { 9 | it('returns the input number with commas added', () => { 10 | expect(formatLineCountWithCommas(1000)).toEqual('1,000'); 11 | expect(formatLineCountWithCommas(10000)).toEqual('10,000'); 12 | }); 13 | }); 14 | describe('when the input number is less than 1000', () => { 15 | it('returns the input number without commas added', () => { 16 | expect(formatLineCountWithCommas(999)).toEqual('999'); 17 | }); 18 | }); 19 | }); 20 | 21 | describe('formattedProcessingTime', () => { 22 | describe('time is 90 seconds or over', () => { 23 | it('should return time in minutes, rounded to tenth', () => { 24 | expect(formattedProcessingTime(91.33458)).toEqual('1.5 minutes'); 25 | }); 26 | }); 27 | 28 | describe('time is under 90 seconds', () => { 29 | it('should return time in seconds, rounded to tenth', () => { 30 | expect(formattedProcessingTime(11.33458)).toEqual('11.3 seconds'); 31 | }); 32 | }); 33 | 34 | describe('time is under 10 seconds and more than 1 second', () => { 35 | it('should return time in seconds, rounded to one decimal place', () => { 36 | expect(formattedProcessingTime(9.10001)).toEqual('9.1 seconds'); 37 | }); 38 | }); 39 | 40 | describe('time is under 1 second', () => { 41 | it('should return time in seconds, rounded to 3 decimal places', () => { 42 | expect(formattedProcessingTime(0.001543)).toEqual('0.002 seconds'); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('convertTimestamp', () => { 48 | it('it should return a human readable date', () => { 49 | const stringDate = convertTimestamp('1979-06-01T00:00:00Z'); 50 | expect(stringDate).toEqual('June 1, 1979'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-dynamic-require: off */ 2 | 3 | /** 4 | * Builds the DLL for development electron renderer process 5 | */ 6 | 7 | import webpack from 'webpack'; 8 | import path from 'path'; 9 | import merge from 'webpack-merge'; 10 | import baseConfig from './webpack.config.base'; 11 | import { dependencies } from '../package.json'; 12 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 13 | 14 | CheckNodeEnv('development'); 15 | 16 | const dist = path.join(__dirname, '..', 'dll'); 17 | 18 | export default merge.smart(baseConfig, { 19 | context: path.join(__dirname, '..'), 20 | 21 | devtool: 'eval', 22 | 23 | mode: 'development', 24 | 25 | target: 'electron-renderer', 26 | 27 | externals: ['fsevents', 'crypto-browserify'], 28 | 29 | /** 30 | * Use `module` from `webpack.config.renderer.dev.js` 31 | */ 32 | module: require('./webpack.config.renderer.dev.babel').default.module, 33 | 34 | entry: { 35 | renderer: Object.keys(dependencies || {}) 36 | }, 37 | 38 | output: { 39 | library: 'renderer', 40 | path: dist, 41 | filename: '[name].dev.dll.js', 42 | libraryTarget: 'var' 43 | }, 44 | 45 | plugins: [ 46 | new webpack.DllPlugin({ 47 | path: path.join(dist, '[name].json'), 48 | name: '[name]' 49 | }), 50 | 51 | /** 52 | * Create global constants which can be configured at compile time. 53 | * 54 | * Useful for allowing different behaviour between development builds and 55 | * release builds 56 | * 57 | * NODE_ENV should be production so that modules do not perform certain 58 | * development checks 59 | */ 60 | new webpack.EnvironmentPlugin({ 61 | NODE_ENV: 'development' 62 | }), 63 | 64 | new webpack.LoaderOptionsPlugin({ 65 | debug: true, 66 | options: { 67 | context: path.join(__dirname, '..', 'app'), 68 | output: { 69 | path: path.join(__dirname, '..', 'dll') 70 | } 71 | } 72 | }) 73 | ] 74 | }); 75 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import merge from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | 13 | CheckNodeEnv('production'); 14 | 15 | export default merge.smart(baseConfig, { 16 | devtool: 'source-map', 17 | 18 | mode: 'production', 19 | 20 | target: 'electron-main', 21 | 22 | entry: './app/main.dev', 23 | 24 | output: { 25 | path: path.join(__dirname, '..'), 26 | filename: './app/main.prod.js' 27 | }, 28 | 29 | optimization: { 30 | minimizer: process.env.E2E_BUILD 31 | ? [] 32 | : [ 33 | new TerserPlugin({ 34 | parallel: true, 35 | sourceMap: true, 36 | cache: true 37 | }) 38 | ] 39 | }, 40 | 41 | plugins: [ 42 | new BundleAnalyzerPlugin({ 43 | analyzerMode: 44 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 45 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 46 | }), 47 | 48 | /** 49 | * Create global constants which can be configured at compile time. 50 | * 51 | * Useful for allowing different behaviour between development builds and 52 | * release builds 53 | * 54 | * NODE_ENV should be production so that modules do not perform certain 55 | * development checks 56 | */ 57 | new webpack.EnvironmentPlugin({ 58 | NODE_ENV: 'production', 59 | DEBUG_PROD: false, 60 | START_MINIMIZED: false 61 | }) 62 | ], 63 | 64 | /** 65 | * Disables webpack processing of __dirname and __filename. 66 | * If you run the bundle in node.js it falls back to these values of node.js. 67 | * https://github.com/webpack/webpack/issues/2010 68 | */ 69 | node: { 70 | __dirname: false, 71 | __filename: false 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /test/e2e/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const path = require('path'); 4 | 5 | const baseOutputDirectory = `${process.env.HOME}/Desktop/Clear_My_Record_output/`; 6 | 7 | export function removeOutputDirectory(dirPath) { 8 | if (fs.existsSync(dirPath)) { 9 | fs.readdirSync(dirPath).forEach(entry => { 10 | const entryPath = path.join(dirPath, entry); 11 | if (fs.lstatSync(entryPath).isDirectory()) { 12 | fs.readdirSync(entryPath).forEach(subEntry => { 13 | const subEntryPath = path.join(entryPath, subEntry); 14 | fs.unlinkSync(subEntryPath); 15 | }); 16 | fs.rmdirSync(entryPath); 17 | } else { 18 | fs.unlinkSync(entryPath); 19 | } 20 | }); 21 | fs.rmdirSync(dirPath); 22 | } 23 | } 24 | 25 | export function getOutputDirectoryPath() { 26 | const creationTimes = []; 27 | const fileNamesByCreationTime = {}; 28 | const files = fs.readdirSync(baseOutputDirectory); 29 | 30 | files.forEach(file => { 31 | if (file.startsWith('CMR_output_')) { 32 | const timeString = fs.statSync(path.join(baseOutputDirectory, file)); 33 | const timeCreated = timeString.mtime.getTime(); 34 | creationTimes.push(timeCreated); 35 | fileNamesByCreationTime[timeCreated] = file; 36 | } 37 | }); 38 | 39 | const fileTimestampsNewestToOldest = creationTimes.sort().reverse(); 40 | const latestTime = fileTimestampsNewestToOldest[0]; 41 | return `${process.env.HOME}/Desktop/Clear_My_Record_output/${fileNamesByCreationTime[latestTime]}`; 42 | } 43 | 44 | export function getMostRecentlyCreatedOutputDirectoryTimeString() { 45 | return getOutputDirectoryPath().split('CMR_output_')[1]; 46 | } 47 | 48 | export function getEligibilityConfigFilePath() { 49 | return path.join( 50 | getOutputDirectoryPath(), 51 | `eligibilityConfig_${getMostRecentlyCreatedOutputDirectoryTimeString()}.json` 52 | ); 53 | } 54 | 55 | export function getSummaryOutputFilePath() { 56 | return path.join( 57 | getOutputDirectoryPath(), 58 | `Summary_Report_${getMostRecentlyCreatedOutputDirectoryTimeString()}.pdf` 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /test/components/BaselineEligibilityOption.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { shallow } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import renderer from 'react-test-renderer'; 5 | import BaselineEligibilityOption from '../../app/components/BaselineEligibilityOption'; 6 | import RadioButton from '../../app/components/RadioButton'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | 10 | function setup() { 11 | const options = { 12 | '11357': 'dismiss', 13 | '11358': 'reduce' 14 | }; 15 | const component1 = shallow( 16 | 20 | ); 21 | 22 | const component2 = shallow( 23 | 27 | ); 28 | return { 29 | component1, 30 | component2 31 | }; 32 | } 33 | 34 | describe('BaselineEligibilityOption component', () => { 35 | it('if dismissed, reduce should not be selected', () => { 36 | const { component1 } = setup(); 37 | expect( 38 | component1.containsAnyMatchingElements([ 39 | 40 | ]) 41 | ).toEqual(true); 42 | 43 | expect( 44 | component1.containsAnyMatchingElements([ 45 | 46 | ]) 47 | ).toEqual(true); 48 | }); 49 | 50 | it('if reduced, dismiss should not be selected', () => { 51 | const { component2 } = setup(); 52 | expect( 53 | component2.containsAnyMatchingElements([ 54 | 55 | ]) 56 | ).toEqual(true); 57 | 58 | expect( 59 | component2.containsAnyMatchingElements([ 60 | 61 | ]) 62 | ).toEqual(true); 63 | }); 64 | 65 | it('should match exact snapshot', () => { 66 | const { component1 } = setup(); 67 | const tree = renderer.create(component1).toJSON(); 68 | 69 | expect(tree).toMatchSnapshot(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/components/ProcessingFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import ProcessingFormCard from '../../app/components/ProcessingFormCard'; 6 | 7 | import * as FileUtils from '../../app/utils/fileUtils'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | const sandbox = sinon.createSandbox(); 11 | 12 | function setup() { 13 | const runScriptSpy = sandbox.spy(); 14 | const startOverSpy = sandbox.spy(); 15 | const resetOutputPathSpy = sandbox.spy(); 16 | 17 | sandbox.stub(FileUtils, 'getFileSize').returns(1000); 18 | 19 | const component = mount( 20 | 25 | ); 26 | 27 | return { 28 | component, 29 | startOverSpy, 30 | resetOutputPathSpy 31 | }; 32 | } 33 | 34 | afterEach(() => { 35 | sandbox.restore(); 36 | }); 37 | 38 | describe('ProcessingFormCard component', () => { 39 | describe('onScriptComplete', () => { 40 | it('should set state when Gogen was successful', () => { 41 | const { component } = setup(); 42 | expect(component.state().gogenComplete).toEqual(false); 43 | component.instance().onScriptComplete(0, 'OK'); 44 | expect(component.state().gogenComplete).toEqual(true); 45 | }); 46 | }); 47 | 48 | describe('calculateFileSizes', () => { 49 | it('returns total size of all provided files', () => { 50 | const { component } = setup(); 51 | expect(component.instance().calculateFileSizes()).toEqual(3000); 52 | }); 53 | }); 54 | 55 | describe('clicking the Start Over button', () => { 56 | it('should call onStartOver and return the user to the home page', () => { 57 | const { component, startOverSpy } = setup(); 58 | const startOverButton = component.find('#start_over').at(0); 59 | startOverButton.simulate('click'); 60 | expect(startOverSpy.called).toBe(true); 61 | expect(startOverSpy.callCount).toEqual(1); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: codeforamerica/bear-ci 6 | 7 | working_directory: ~/bear 8 | 9 | steps: 10 | - checkout 11 | 12 | - run: 13 | name: Fetch gogen binary 14 | command: | 15 | mkdir -p ~/go/bin 16 | curl -Ls https://github.com/codeforamerica/gogen/releases/download/"$(cat ~/bear/.gogen-version)"/gogen_linux -o ~/go/bin/gogen 17 | chmod +x ~/go/bin/gogen 18 | 19 | - restore_cache: 20 | keys: 21 | - v1-dependencies-{{ checksum "package.json" }} 22 | 23 | - run: yarn install 24 | 25 | - save_cache: 26 | paths: 27 | - node_modules 28 | key: v1-dependencies-{{ checksum "package.json" }} 29 | 30 | - run: 31 | name: Run unit tests 32 | command: yarn build && yarn test 33 | 34 | - run: 35 | name: Run end-to-end tests 36 | command: START_MINIMIZED=true yarn build-e2e && yarn test-e2e && yarn test-ci-e2e 37 | 38 | package-and-draft: 39 | docker: 40 | - image: codeforamerica/bear-ci 41 | 42 | working_directory: ~/bear 43 | 44 | steps: 45 | - checkout 46 | 47 | - run: 48 | name: Fetch gogen binary 49 | command: | 50 | curl -Ls https://github.com/codeforamerica/gogen/releases/download/"$(cat ~/bear/.gogen-version)"/gogen_win.exe -o ./gogen.exe 51 | 52 | - restore_cache: 53 | keys: 54 | - v1-dependencies-{{ checksum "package.json" }} 55 | 56 | - run: yarn install 57 | 58 | - save_cache: 59 | paths: 60 | - node_modules 61 | key: v1-dependencies-{{ checksum "package.json" }} 62 | 63 | - run: 64 | name: Build and Publish Draft for Windows 65 | command: | 66 | export GH_TOKEN=$GITHUB_ACCESS_TOKEN 67 | yarn package-publish-win 68 | 69 | workflows: 70 | version: 2 71 | pipeline: 72 | jobs: 73 | - build 74 | - package-and-draft: 75 | requires: 76 | - build 77 | filters: 78 | branches: 79 | only: 80 | - master 81 | -------------------------------------------------------------------------------- /app/components/ProcessingFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import FormCard, { FormCardContent } from './FormCard'; 5 | import ProgressBar from './ProgressBar'; 6 | import StartOverButton from './StartOverButton'; 7 | import { getFileSize } from '../utils/fileUtils'; 8 | 9 | type Props = { 10 | dojFilePaths: Array, 11 | onComplete: (number, string) => void, 12 | runScriptInOptions: ((number, string) => void) => void, 13 | onStartOver: () => void 14 | }; 15 | 16 | type State = { 17 | gogenComplete: boolean, 18 | gogenExitCode: number, 19 | errorText: string 20 | }; 21 | 22 | export default class ProcessingFormCard extends Component { 23 | constructor(props: Props) { 24 | super(props); 25 | this.state = { 26 | gogenComplete: false, 27 | gogenExitCode: -1, 28 | errorText: '' 29 | }; 30 | } 31 | 32 | componentDidMount() { 33 | const { runScriptInOptions } = this.props; 34 | runScriptInOptions(this.onScriptComplete); 35 | window.scrollTo(0, 0); 36 | } 37 | 38 | onScriptComplete = (code: number, errorText: string) => { 39 | this.setState({ gogenComplete: true, gogenExitCode: code, errorText }); 40 | }; 41 | 42 | calculateFileSizes = () => { 43 | const { dojFilePaths } = this.props; 44 | let totalSize = 0; 45 | dojFilePaths.forEach(path => { 46 | totalSize += getFileSize(path); 47 | }); 48 | return totalSize; 49 | }; 50 | 51 | render() { 52 | const { onComplete, onStartOver } = this.props; 53 | const { gogenComplete, gogenExitCode, errorText } = this.state; 54 | return ( 55 | 56 | 57 |
58 |
59 |

Reading and preparing files ...

60 | 67 | 68 |
69 | 70 | 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/ResultsFormCard.css: -------------------------------------------------------------------------------- 1 | .sidePadding { 2 | padding-left: 4em; 3 | padding-right: 4em; 4 | } 5 | 6 | .resultsHeader { 7 | font-weight: lighter !important; 8 | } 9 | 10 | .resultsSubheader { 11 | font-weight: lighter !important; 12 | font-size: 1.9rem; 13 | width: 60%; 14 | margin-top: 2em; 15 | margin-left: auto; 16 | margin-right: auto; 17 | text-align: left; 18 | } 19 | 20 | .subSectionTitle { 21 | font-weight: 400; 22 | } 23 | 24 | .sectionDivider { 25 | margin: 0 -38px auto; 26 | width: 960px; 27 | border-bottom: 2px solid #f7f5f4; 28 | } 29 | 30 | .impactSection { 31 | text-align: center; 32 | margin-bottom: 20px; 33 | } 34 | 35 | .contentsListContainer { 36 | width: 50%; 37 | margin: 0 auto; 38 | } 39 | 40 | .contentsList { 41 | list-style: decimal; 42 | padding-left: 110px; 43 | margin-top: 0; 44 | line-height: 1em; 45 | } 46 | 47 | .contentsTableTitle { 48 | margin-top: 2em; 49 | padding-left: 90px; 50 | text-align: left; 51 | } 52 | 53 | .contentsTable { 54 | border-bottom: none !important; 55 | margin: 0 -38px auto; 56 | width: 960px !important; 57 | } 58 | 59 | .contentsTable th { 60 | padding-right: 15px; 61 | border-bottom: 2px solid #f7f5f4 !important; 62 | } 63 | 64 | .contentsTable td { 65 | padding-right: 15px; 66 | border-bottom: none !important; 67 | } 68 | 69 | .fileColumn { 70 | width: 250px; 71 | } 72 | 73 | .columnsColumn { 74 | width: 185px; 75 | } 76 | 77 | .rowsColumn { 78 | width: 170px; 79 | } 80 | 81 | .supportingColumn { 82 | width: 160px; 83 | } 84 | 85 | .supportingCheck { 86 | padding-right: 35px !important; 87 | } 88 | 89 | .eligibilityColumn { 90 | width: 160px; 91 | } 92 | 93 | .eligibilityCheck { 94 | padding-right: 72px !important; 95 | } 96 | 97 | .completedIcon { 98 | background-color: #009d86; 99 | border-radius: 50%; 100 | display: inline-block; 101 | width: 60px; 102 | height: 60px; 103 | background-image: url('../assets/images/white_check_large.png'); 104 | background-position: center center; 105 | background-repeat: no-repeat; 106 | background-size: 70%; 107 | } 108 | 109 | .tealCheck { 110 | background-image: url('../assets/images/teal_check.png'); 111 | } 112 | 113 | .resultsFooter { 114 | margin-top: 0 !important; 115 | } 116 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | allow_failures: 3 | - os: windows 4 | include: 5 | - os: osx 6 | language: node_js 7 | node_js: 8 | - node 9 | env: 10 | - ELECTRON_CACHE=$HOME/.cache/electron 11 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 12 | 13 | - os: linux 14 | language: node_js 15 | node_js: 16 | - node 17 | addons: 18 | apt: 19 | sources: 20 | - ubuntu-toolchain-r-test 21 | packages: 22 | - gcc-multilib 23 | - g++-8 24 | - g++-multilib 25 | - icnsutils 26 | - graphicsmagick 27 | - xz-utils 28 | - xorriso 29 | - rpm 30 | 31 | - os: windows 32 | language: node_js 33 | node_js: 34 | - node 35 | env: 36 | - ELECTRON_CACHE=$HOME/.cache/electron 37 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 38 | 39 | before_cache: 40 | - rm -rf $HOME/.cache/electron-builder/wine 41 | 42 | cache: 43 | yarn: true 44 | directories: 45 | - node_modules 46 | - $(npm config get prefix)/lib/node_modules 47 | - flow-typed 48 | - $HOME/.cache/electron 49 | - $HOME/.cache/electron-builder 50 | 51 | before_install: 52 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX="g++-8"; fi 53 | 54 | install: 55 | - yarn --ignore-engines 56 | # On Linux, initialize "virtual display". See before_script 57 | - | 58 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 59 | /sbin/start-stop-daemon \ 60 | --start \ 61 | --quiet \ 62 | --pidfile /tmp/custom_xvfb_99.pid \ 63 | --make-pidfile \ 64 | --background \ 65 | --exec /usr/bin/Xvfb \ 66 | -- :99 -ac -screen 0 1280x1024x16 67 | else 68 | : 69 | fi 70 | 71 | before_script: 72 | # On Linux, create a "virtual display". This allows browsers to work properly 73 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 74 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 75 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sleep 3; fi 76 | 77 | script: 78 | - yarn package-ci 79 | - yarn lint 80 | - yarn flow 81 | # HACK: Temporarily ignore `yarn test` on linux 82 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then yarn test; fi 83 | - yarn build-e2e 84 | - yarn test-e2e 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | ## Prerequisites 12 | 13 | 14 | 15 | - [ ] Using yarn 16 | - [ ] Using node 10.x 17 | - [ ] Using an up-to-date [`master` branch](https://github.com/electron-react-boilerplate/electron-react-boilerplate/tree/master) 18 | - [ ] Using latest version of devtools. See [wiki for howto update](https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/DevTools) 19 | - [ ] Link to stacktrace in a Gist (for bugs) 20 | - [ ] For issue in production release, devtools output of `DEBUG_PROD=true yarn build && yarn start` 21 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 22 | 23 | ## Expected Behavior 24 | 25 | 26 | 27 | 28 | ## Current Behavior 29 | 30 | 31 | 32 | 33 | ## Possible Solution 34 | 35 | 36 | 37 | 38 | ## Steps to Reproduce (for bugs) 39 | 40 | 41 | 42 | 43 | 1. 44 | 45 | 2. 46 | 47 | 3. 48 | 49 | 4. 50 | 51 | ## Context 52 | 53 | 54 | 55 | 56 | 57 | ## Your Environment 58 | 59 | 60 | 61 | - Node version : 62 | - Version or Branch used : 63 | - Operating System and version : 64 | - Link to your project : 65 | -------------------------------------------------------------------------------- /app/components/ProgressBar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | fileSizeInBytes: number, 6 | onCompleteCallback: (number, string) => void, 7 | isComplete: boolean, 8 | gogenExitCode: number, 9 | errorText: string 10 | }; 11 | 12 | type State = { 13 | fill: number, 14 | stepSize: number 15 | }; 16 | 17 | export const PROCESSING_RATE = 7000000; 18 | export const MAX_FILL = 100; 19 | export const MIN_PROCESSING_TIME = 5; 20 | export const MAX_STEP_SIZE = MAX_FILL / MIN_PROCESSING_TIME; 21 | 22 | export default class ProgressBar extends Component { 23 | timerID: IntervalID; 24 | 25 | constructor(props: Props) { 26 | super(props); 27 | const { fileSizeInBytes } = this.props; 28 | const stepSize = (PROCESSING_RATE * MAX_FILL) / fileSizeInBytes; 29 | this.state = { 30 | fill: 0, 31 | stepSize: Math.min(MAX_STEP_SIZE, stepSize) 32 | }; 33 | } 34 | 35 | componentDidMount() { 36 | this.timerID = setInterval(() => this.tick(), 1000); 37 | } 38 | 39 | componentWillUnmount() { 40 | clearInterval(this.timerID); 41 | } 42 | 43 | tick() { 44 | const { 45 | isComplete, 46 | gogenExitCode, 47 | onCompleteCallback, 48 | errorText 49 | } = this.props; 50 | const { stepSize, fill } = this.state; 51 | if (gogenExitCode > 0) { 52 | onCompleteCallback(gogenExitCode, errorText); 53 | return; 54 | } 55 | if (isComplete) { 56 | if (fill === MAX_FILL) { 57 | clearInterval(this.timerID); 58 | onCompleteCallback(gogenExitCode, errorText); 59 | return; 60 | } 61 | if (stepSize !== MAX_STEP_SIZE) { 62 | this.setState({ 63 | fill: MAX_FILL 64 | }); 65 | } 66 | } 67 | this.setState(state => ({ 68 | fill: Math.min(MAX_FILL, state.fill + state.stepSize) 69 | })); 70 | } 71 | 72 | render() { 73 | const { fill } = this.state; 74 | const percentage = Math.round(fill); 75 | return ( 76 |
77 |
78 |
82 |
{percentage}%
83 |
84 |
85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | 10 | // babel-preset-react-optimize 11 | require('@babel/plugin-transform-react-constant-elements'), 12 | require('@babel/plugin-transform-react-inline-elements'), 13 | require('babel-plugin-transform-react-remove-prop-types') 14 | ]; 15 | 16 | module.exports = api => { 17 | // see docs about api at https://babeljs.io/docs/en/config-files#apicache 18 | 19 | const development = api.env(developmentEnvironments); 20 | 21 | return { 22 | presets: [ 23 | [ 24 | require('@babel/preset-env'), 25 | { 26 | targets: { electron: require('electron/package.json').version }, 27 | useBuiltIns: 'usage', 28 | corejs: 2 29 | } 30 | ], 31 | [require('@babel/preset-react'), { development }] 32 | ], 33 | plugins: [ 34 | // Stage 0 35 | require('@babel/plugin-transform-flow-strip-types'), 36 | require('@babel/plugin-proposal-function-bind'), 37 | 38 | // Stage 1 39 | require('@babel/plugin-proposal-export-default-from'), 40 | require('@babel/plugin-proposal-logical-assignment-operators'), 41 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 42 | [ 43 | require('@babel/plugin-proposal-pipeline-operator'), 44 | { proposal: 'minimal' } 45 | ], 46 | [ 47 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 48 | { loose: false } 49 | ], 50 | require('@babel/plugin-proposal-do-expressions'), 51 | 52 | // Stage 2 53 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 54 | require('@babel/plugin-proposal-function-sent'), 55 | require('@babel/plugin-proposal-export-namespace-from'), 56 | require('@babel/plugin-proposal-numeric-separator'), 57 | require('@babel/plugin-proposal-throw-expressions'), 58 | 59 | // Stage 3 60 | require('@babel/plugin-syntax-dynamic-import'), 61 | require('@babel/plugin-syntax-import-meta'), 62 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 63 | require('@babel/plugin-proposal-json-strings'), 64 | 65 | ...(development ? developmentPlugins : productionPlugins) 66 | ] 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /test/components/RadioButton.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import RadioButton from '../../app/components/RadioButton'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(selected) { 12 | const onSelectSpy = sandbox.spy(); 13 | const component = shallow( 14 | 20 | ); 21 | return { 22 | component, 23 | onSelectSpy 24 | }; 25 | } 26 | 27 | afterEach(() => { 28 | sandbox.restore(); 29 | }); 30 | 31 | describe('RadioButton component', () => { 32 | it('is checked when selected is true', () => { 33 | const { component } = setup(true); 34 | expect(component.find('input').props().defaultChecked).toEqual(true); 35 | }); 36 | 37 | it('is not checked when selected is false', () => { 38 | const { component } = setup(false); 39 | expect(component.find('input').props().defaultChecked).toEqual(false); 40 | }); 41 | 42 | it('sets the name to the group prop', () => { 43 | const { component } = setup(false); 44 | expect(component.find('input').props().name).toEqual('nihilists'); 45 | }); 46 | 47 | it('constructs the id from the group and value', () => { 48 | const { component } = setup(false); 49 | expect(component.find('input').props().id).toEqual('whatever_nihilists'); 50 | }); 51 | 52 | it('calls onSelect with the group and value when button is clicked', () => { 53 | const { component, onSelectSpy } = setup(false); 54 | component.find('input').simulate('change'); 55 | 56 | expect(onSelectSpy.called).toEqual(true); 57 | const { args } = onSelectSpy.getCall(0); 58 | expect(args[0]).toEqual('nihilists'); 59 | expect(args[1]).toEqual('whatever'); 60 | }); 61 | 62 | it('should match exact snapshot when selected is false', () => { 63 | const { component } = setup(false); 64 | const tree = renderer.create(component).toJSON(); 65 | 66 | expect(tree).toMatchSnapshot(); 67 | }); 68 | 69 | it('should match exact snapshot when selected is true', () => { 70 | const { component } = setup(true); 71 | const tree = renderer.create(component).toJSON(); 72 | 73 | expect(tree).toMatchSnapshot(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/components/CountySelectFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import CountySelectFormCard from '../../app/components/CountySelectFormCard'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(selectedCounty) { 12 | const fakeOnCountySelect = sandbox.spy(); 13 | const fakeOnCountyConfirm = sandbox.spy(); 14 | const component = mount( 15 | 20 | ); 21 | return { 22 | component, 23 | fakeOnCountySelect, 24 | fakeOnCountyConfirm 25 | }; 26 | } 27 | 28 | afterEach(() => { 29 | sandbox.restore(); 30 | }); 31 | 32 | describe('CountySelectFormCard component', () => { 33 | describe('clicking the Continue button when county has been selected', () => { 34 | it('should call the onCountyConfirm function with the next screen number', () => { 35 | const county = { name: 'Alameda', code: 'ALAMEDA' }; 36 | const { component, fakeOnCountyConfirm } = setup(county); 37 | const continueButton = component.find('button').at(0); 38 | continueButton.simulate('click'); 39 | expect(fakeOnCountyConfirm.called).toBe(true); 40 | expect(fakeOnCountyConfirm.callCount).toEqual(1); 41 | expect(component.find('select').props().value).toBe('ALAMEDA'); 42 | }); 43 | }); 44 | 45 | describe('clicking the Continue button when county has NOT been selected', () => { 46 | it('should NOT call the onCountyConfirm function ', () => { 47 | const defaultCounty = { name: '', code: '' }; 48 | const { component, fakeOnCountyConfirm } = setup(defaultCounty); 49 | const continueButton = component.find('button').at(0); 50 | continueButton.simulate('click'); 51 | expect(fakeOnCountyConfirm.called).toBe(false); 52 | expect(component.find('select').props().value).toBe(''); 53 | }); 54 | }); 55 | 56 | it('should match exact snapshot', () => { 57 | const defaultCounty = { name: '', code: '' }; 58 | 59 | const component = ( 60 |
61 | 62 |
63 | ); 64 | 65 | const tree = renderer.create(component).toJSON(); 66 | 67 | expect(tree).toMatchSnapshot(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/components/ResultsFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import ResultsFormCard from '../../app/components/ResultsFormCard'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup() { 12 | const openFolderSpy = sandbox.spy(); 13 | const startOverSpy = sandbox.spy(); 14 | const impactStats = { 15 | noFelony: 2000, 16 | noConvictionLast7: 5000, 17 | noConviction: 1200 18 | }; 19 | const component = mount( 20 | 27 | ); 28 | return { 29 | component, 30 | openFolderSpy, 31 | startOverSpy 32 | }; 33 | } 34 | 35 | afterEach(() => { 36 | sandbox.restore(); 37 | }); 38 | 39 | describe('ResultsFormCard component', () => { 40 | describe('clicking the Open Folder button', () => { 41 | it('should call the openResultsFolder function', () => { 42 | const { component, openFolderSpy } = setup(); 43 | const openFolderButton = component.find('#view_results').at(0); 44 | openFolderButton.simulate('click'); 45 | expect(openFolderSpy.called).toBe(true); 46 | const { args } = openFolderSpy.getCall(0); 47 | expect(args[0]).toEqual('/path/to/output'); 48 | }); 49 | }); 50 | 51 | describe('clicking the Start Over button', () => { 52 | it('should call start over and return the user to the home page', () => { 53 | const { component, startOverSpy } = setup(); 54 | const startOverButton = component.find('#start_over').at(0); 55 | startOverButton.simulate('click'); 56 | expect(startOverSpy.called).toBe(true); 57 | expect(startOverSpy.callCount).toEqual(1); 58 | }); 59 | }); 60 | 61 | it('should match exact snapshot', () => { 62 | const impactStats = { 63 | noFelony: 2000, 64 | noConvictionLast7: 5000, 65 | noConviction: 1200 66 | }; 67 | const component = ( 68 |
69 | 74 |
75 | ); 76 | 77 | const tree = renderer.create(component).toJSON(); 78 | 79 | expect(tree).toMatchSnapshot(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /app/utils/formatSummaryOutputUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // eslint-disable-next-line import/prefer-default-export 3 | 4 | export function formatCountsByCodeSection( 5 | convictionCounts: ConvictionCountByType 6 | ) { 7 | const countPhrases = Object.entries(convictionCounts).map(entry => { 8 | // $FlowFixMe 9 | return `H&S § ${entry[0]}: ${pluralize(entry[1])}`; 10 | }); 11 | return countPhrases.join('; '); 12 | } 13 | 14 | export function formatCountsByAdditionalRelief( 15 | convictionCounts: ConvictionCountByType 16 | ) { 17 | const countPhrases = Object.entries(convictionCounts).map(entry => { 18 | // $FlowFixMe 19 | return `${entry[0]}: ${pluralize(entry[1])}`; 20 | }); 21 | return countPhrases.join('; '); 22 | } 23 | 24 | export function formatDateTime() { 25 | const date = new Date(); 26 | return date.toLocaleString('en-US', { 27 | year: 'numeric', 28 | month: 'long', 29 | day: 'numeric', 30 | hour: 'numeric', 31 | minute: 'numeric' 32 | }); 33 | } 34 | 35 | export function toTitleCase(str: string) { 36 | return str.replace(/\w\S*/g, txt => { 37 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 38 | }); 39 | } 40 | 41 | export function convertTimestamp(timestamp: string) { 42 | const monthNames = { 43 | '01': 'January', 44 | '02': 'February', 45 | '03': 'March', 46 | '04': 'April', 47 | '05': 'May', 48 | '06': 'June', 49 | '07': 'July', 50 | '08': 'August', 51 | '09': 'September', 52 | '10': 'October', 53 | '11': 'November', 54 | '12': 'December' 55 | }; 56 | const dateArray = timestamp.split(' ')[0].split('-'); 57 | const monthNumber = dateArray[1]; 58 | const dayNumber = parseInt(dateArray[2], 10); 59 | return `${monthNames[monthNumber]} ${dayNumber}, ${dateArray[0]}`; 60 | } 61 | 62 | export function formatLineCountWithCommas(number: number) { 63 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 64 | } 65 | 66 | export function formattedProcessingTime(timeInSeconds: number) { 67 | if (timeInSeconds <= 1) { 68 | return `${Math.round(timeInSeconds * 1000) / 1000} seconds`; 69 | } 70 | if (timeInSeconds <= 10 && timeInSeconds > 1) { 71 | return `${Math.round(timeInSeconds * 100) / 100} seconds`; 72 | } 73 | if (timeInSeconds >= 90) { 74 | return `${Math.round(timeInSeconds / 6) / 10} minutes`; 75 | } 76 | return `${Math.round(timeInSeconds * 10) / 10} seconds`; 77 | } 78 | 79 | function pluralize(item: number) { 80 | if (item === 1) { 81 | return `1 conviction`; 82 | } 83 | return `${item} convictions`; 84 | } 85 | -------------------------------------------------------------------------------- /app/components/DojFileSelectFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import DojFileInput from './DojFileInput'; 4 | import ContinueButton from './ContinueButton'; 5 | import DojFileItem from './DojFileItem'; 6 | 7 | import FormCard, { 8 | FormCardContent, 9 | FormCardFooter, 10 | FormCardHeader 11 | } from './FormCard'; 12 | import GoBackButton from './GoBackButton'; 13 | import styles from './DojFileSelectFormCard.css'; 14 | 15 | type Props = { 16 | dojFilePaths: Array, 17 | updateFilePath: string => void, 18 | onFileConfirm: void => void, 19 | onFileRemove: string => void, 20 | onBack: void => void 21 | }; 22 | 23 | export default class DojFileSelectFormCard extends Component { 24 | componentDidMount() { 25 | window.scrollTo(0, 0); 26 | } 27 | 28 | renderCardContent = () => { 29 | const { dojFilePaths, updateFilePath, onFileRemove } = this.props; 30 | 31 | if (this.isEmptyFilePath()) { 32 | return ( 33 | 37 | ); 38 | } 39 | return ( 40 |
41 | {dojFilePaths.map(path => { 42 | return ( 43 | 49 | ); 50 | })} 51 | 55 |
56 | ); 57 | }; 58 | 59 | isEmptyFilePath = () => { 60 | const { dojFilePaths } = this.props; 61 | return dojFilePaths.length === 0; 62 | }; 63 | 64 | render() { 65 | const { onBack, onFileConfirm } = this.props; 66 | 67 | return ( 68 | 69 | 70 | Import Prop 64 bulk conviction data files 71 | 72 | 73 |

Choose .dat files to import

74 |
{this.renderCardContent()}
75 |
76 | 77 |
78 | 82 | 83 |
84 |
85 |
86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /flow-typed/domainTypes.js: -------------------------------------------------------------------------------- 1 | declare type ApplicationState = { 2 | gogenPath: string, 3 | formattedGogenRunTime: string, 4 | currentScreen: number, 5 | previousScreenInFlow: number, 6 | county: County, 7 | dojFilePaths: Array, 8 | baselineEligibilityOptions: BaselineEligibilityOptions, 9 | additionalReliefOptions: AdditionalReliefOptions, 10 | impactStatistics: ImpactStatistics, 11 | outputPathPrefix: string, 12 | outputFilePath: string, 13 | errorText: string 14 | }; 15 | 16 | declare type County = { 17 | name: string, 18 | code: string 19 | }; 20 | 21 | declare type BaselineEligibilityOptions = { [string]: string }; 22 | 23 | declare type AdditionalReliefOptions = { 24 | dismissYearsSinceConvictionThreshold: boolean, 25 | yearsSinceConvictionThreshold: number, 26 | dismissYearsCrimeFreeThreshold: boolean, 27 | yearsCrimeFreeThreshold: number, 28 | subjectHasOnlyProp64Charges: boolean 29 | }; 30 | 31 | declare type AdditionalReliefValue = number | boolean; 32 | 33 | declare type BaselineEligibilityConfiguration = { 34 | baselineEligibility: { 35 | dismiss: Array, 36 | reduce: Array 37 | } 38 | }; 39 | 40 | declare type EligibilityConfiguration = { 41 | baselineEligibility: { 42 | dismiss: Array, 43 | reduce: Array 44 | }, 45 | additionalRelief: AdditionalReliefOptions 46 | }; 47 | 48 | declare type ImpactStatistics = { 49 | noFelony: number, 50 | noConvictionLast7: number, 51 | noConviction: number 52 | }; 53 | 54 | declare type GogenImpactStatistics = { 55 | CountSubjectsNoConviction: number, 56 | CountSubjectsNoConvictionLast7Years: number, 57 | CountSubjectsNoFelony: number 58 | }; 59 | 60 | declare type ConvictionCountByType = { [key: string]: number }; 61 | 62 | declare type GogenSummaryData = { 63 | county: string, 64 | earliestConviction: string, 65 | lineCount: number, 66 | processingTimeInSeconds: number, 67 | reliefWithCurrentEligibilityChoices: GogenImpactStatistics, 68 | reliefWithDismissAllProp64: GogenImpactStatistics, 69 | prop64ConvictionsCountInCountyByCodeSection: ConvictionCountByType, 70 | subjectsWithProp64ConvictionCountInCounty: number, 71 | prop64FelonyConvictionsCountInCounty: number, 72 | prop64NonFelonyConvictionsCountInCounty: number, 73 | subjectsWithSomeReliefCount: number, 74 | convictionDismissalCountByCodeSection: ConvictionCountByType, 75 | convictionReductionCountByCodeSection: ConvictionCountByType, 76 | convictionDismissalCountByAdditionalRelief: ConvictionCountByType 77 | }; 78 | 79 | declare type ErrorData = { 80 | errorType: string, 81 | errorMessage: string 82 | }; 83 | 84 | declare type Errors = { 85 | [key: string]: ErrorData 86 | }; 87 | -------------------------------------------------------------------------------- /test/components/Checkbox.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import Checkbox from '../../app/components/Checkbox'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(checked) { 12 | const onChangeSpy = sandbox.spy(); 13 | const component = shallow( 14 | 20 | This is a checkbox 21 | 22 | ); 23 | return { 24 | component, 25 | onChangeSpy 26 | }; 27 | } 28 | 29 | afterEach(() => { 30 | sandbox.restore(); 31 | }); 32 | 33 | describe('Checkbox component', () => { 34 | it('wraps the input in a label with proper class', () => { 35 | const { component } = setup(true); 36 | const label = component.find('label'); 37 | expect(label.props().className).toContain('checkbox'); 38 | expect(label.find('input').length).toBe(1); 39 | }); 40 | 41 | it('renders a checkbox with value=true', () => { 42 | const { component } = setup(true); 43 | expect(component.find('input').props().value).toEqual(true); 44 | }); 45 | 46 | it('is checked when selected is true', () => { 47 | const { component } = setup(true); 48 | expect(component.find('input').props().defaultChecked).toEqual(true); 49 | }); 50 | 51 | it('is not checked when selected is false', () => { 52 | const { component } = setup(false); 53 | expect(component.find('input').props().defaultChecked).toEqual(false); 54 | }); 55 | 56 | it('sets the name to the group prop', () => { 57 | const { component } = setup(false); 58 | expect(component.find('input').props().name).toEqual('nihilists'); 59 | }); 60 | 61 | it('constructs the id from the group and value', () => { 62 | const { component } = setup(false); 63 | expect(component.find('input').props().id).toEqual('true_nihilists'); 64 | }); 65 | 66 | it('calls onChange with the group and value when checkbox is clicked', () => { 67 | const { component, onChangeSpy } = setup(false); 68 | component.find('input').simulate('change'); 69 | 70 | expect(onChangeSpy.called).toEqual(true); 71 | const { args } = onChangeSpy.getCall(0); 72 | expect(args[0]).toEqual('nihilists'); 73 | }); 74 | 75 | it('should match exact snapshot when checked', () => { 76 | const { component } = setup(true); 77 | const tree = renderer.create(component).toJSON(); 78 | 79 | expect(tree).toMatchSnapshot(); 80 | }); 81 | 82 | it('should match exact snapshot when NOT checked', () => { 83 | const { component } = setup(false); 84 | const tree = renderer.create(component).toJSON(); 85 | 86 | expect(tree).toMatchSnapshot(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /app/components/SummaryReportPdfStyles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { StyleSheet } from '@react-pdf/renderer'; 3 | // import robotoBold from '../assets/fonts/Roboto-Bold.ttf'; 4 | // import robotoRegular from '../assets/fonts/Roboto-Regular.ttf'; 5 | // 6 | // Font.register({ 7 | // family: 'Roboto-Bold', 8 | // src: robotoBold 9 | // }); 10 | // 11 | // Font.register({ 12 | // family: 'Roboto', 13 | // src: robotoRegular 14 | // }); 15 | 16 | const baseTextStyles = { 17 | fontSize: 12, 18 | lineHeight: 1.5 19 | // fontFamily: 'Roboto' 20 | }; 21 | 22 | const baseHxStyles = { 23 | color: '#008060', 24 | fontWeight: 'bold' 25 | }; 26 | 27 | const baseHorizontalRuleStyles = { 28 | borderBottom: 1, 29 | borderColor: '#008060', 30 | marginHorizontal: 130, 31 | marginTop: 12 32 | }; 33 | 34 | const styles = StyleSheet.create({ 35 | page: { 36 | paddingBottom: 20, 37 | paddingTop: 30 38 | }, 39 | logoImage: { 40 | position: 'absolute', 41 | top: 0, 42 | left: 35, 43 | width: 200 44 | }, 45 | bulletImage: { 46 | paddingRight: 10, 47 | height: 6, 48 | marginTop: 4 49 | }, 50 | headerText: { 51 | width: 300, 52 | position: 'absolute', 53 | right: 35, 54 | bottom: 10 55 | }, 56 | tinyText: { 57 | fontSize: 8, 58 | marginVertical: 3, 59 | textAlign: 'right' 60 | }, 61 | header: { 62 | borderBottom: 7, 63 | borderColor: '#008060', 64 | marginTop: -20, 65 | marginBottom: 20, 66 | position: 'relative', 67 | height: 100 68 | }, 69 | body: { 70 | paddingTop: 15, 71 | paddingHorizontal: 35, 72 | paddingRight: 65 73 | }, 74 | h1: { 75 | marginBottom: 10, 76 | fontSize: 24 77 | // fontFamily: 'Roboto-Bold' 78 | }, 79 | h2: { 80 | ...baseHxStyles, 81 | marginTop: 18, 82 | marginBottom: 12, 83 | fontSize: 14 84 | }, 85 | h3: { 86 | ...baseHxStyles, 87 | marginTop: 16, 88 | marginBottom: 10, 89 | fontSize: 12 90 | }, 91 | h3Condensed: { 92 | ...baseHxStyles, 93 | fontSize: 12, 94 | marginTop: 8, 95 | marginBottom: 10 96 | }, 97 | text: { 98 | ...baseTextStyles 99 | }, 100 | textWithBottomPadding: { 101 | ...baseTextStyles, 102 | marginBottom: 5 103 | }, 104 | indentedText: { 105 | ...baseTextStyles, 106 | marginLeft: 16, 107 | marginBottom: 10 108 | }, 109 | boldText: { 110 | ...baseTextStyles 111 | // fontFamily: 'Roboto-Bold' 112 | }, 113 | listItem: { 114 | ...baseTextStyles, 115 | display: 'flex', 116 | flexDirection: 'row', 117 | marginBottom: 8, 118 | paddingLeft: 15 119 | }, 120 | horizontalRule: { 121 | ...baseHorizontalRuleStyles 122 | }, 123 | horizontalRuleWithTopPadding: { 124 | ...baseHorizontalRuleStyles, 125 | marginTop: 16 126 | } 127 | }); 128 | 129 | export default styles; 130 | -------------------------------------------------------------------------------- /app/components/ErrorFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import FormCard, { 5 | FormCardContent, 6 | FormCardFooter, 7 | FormCardHeader 8 | } from './FormCard'; 9 | 10 | import StartOverButton from './StartOverButton'; 11 | import ErrorSection from './ErrorSection'; 12 | 13 | type Props = { 14 | onStartOver: () => void, 15 | errorText: string 16 | }; 17 | 18 | export default class ErrorFormCard extends Component { 19 | componentDidMount() { 20 | window.scrollTo(0, 0); 21 | } 22 | 23 | extractErrors = (errorData: Errors, parsing: boolean) => { 24 | let keys; 25 | if (parsing) { 26 | keys = Object.keys(errorData).filter( 27 | key => errorData[key].errorType === 'PARSING' 28 | ); 29 | } else { 30 | keys = Object.keys(errorData).filter( 31 | key => errorData[key].errorType !== 'PARSING' 32 | ); 33 | } 34 | return keys.reduce( 35 | (res, key) => Object.assign(res, { [key]: errorData[key] }), 36 | {} 37 | ); 38 | }; 39 | 40 | renderErrorSection = ( 41 | errorData: Errors, 42 | parsing: boolean, 43 | header: string 44 | ) => { 45 | const errors = this.extractErrors(errorData, parsing); 46 | 47 | if (Object.keys(errors).length !== 0) { 48 | return ( 49 | 50 | ); 51 | } 52 | return null; 53 | }; 54 | 55 | render() { 56 | const { errorText, onStartOver } = this.props; 57 | const errorData = JSON.parse(errorText); 58 | 59 | return ( 60 | 61 | 62 |
63 |
64 |

Error

65 |
66 |
67 | Sorry, we had trouble with your request. 68 |
69 |
70 |
71 | If you continue to run into problems, contact us at 72 | clearmyrecord@codeforamerica.org and share the following error 73 | messages by copying and pasting them into an email. 74 |
75 |
76 | 77 | 78 |
79 |
80 | {this.renderErrorSection( 81 | errorData, 82 | true, 83 | 'We were not able to read the following files. Please download the original DOJ files and try again.' 84 | )} 85 | {this.renderErrorSection( 86 | errorData, 87 | false, 88 | 'We encountered the following errors:' 89 | )} 90 |
91 | 92 |
93 |
94 | 95 | 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/components/ErrorFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import ErrorFormCard from '../../app/components/ErrorFormCard'; 6 | import ErrorSection from '../../app/components/ErrorSection'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | const sandbox = sinon.createSandbox(); 10 | 11 | function setup(errorData) { 12 | const startOverSpy = sandbox.spy(); 13 | 14 | const component = mount( 15 | 19 | ); 20 | return { 21 | component, 22 | startOverSpy 23 | }; 24 | } 25 | 26 | afterEach(() => { 27 | sandbox.restore(); 28 | }); 29 | 30 | describe('ErrorFormCard component', () => { 31 | describe('Displays errors', () => { 32 | it('should show Parsing header for parsing errors', () => { 33 | const { component } = setup({ 34 | file_1: { 35 | errorType: 'PARSING', 36 | errorMessage: 'record on line 2: wrong number of fields' 37 | } 38 | }); 39 | expect( 40 | component.containsAnyMatchingElements([ 41 | 42 | ]) 43 | ).toEqual(true); 44 | 45 | expect( 46 | component.containsAnyMatchingElements([ 47 | 48 | ]) 49 | ).toEqual(false); 50 | }); 51 | 52 | it('should show Non Parsing header for non-parsing errors', () => { 53 | const { component } = setup({ 54 | file_1: { 55 | errorType: 'OTHER', 56 | errorMessage: 'Cannot open file' 57 | } 58 | }); 59 | expect( 60 | component.containsAnyMatchingElements([ 61 | 62 | ]) 63 | ).toEqual(false); 64 | 65 | expect( 66 | component.containsAnyMatchingElements([ 67 | 68 | ]) 69 | ).toEqual(true); 70 | }); 71 | 72 | it('should show both Parsing & Non Parsing titles for mixed errors', () => { 73 | const { component } = setup({ 74 | file_1: { 75 | errorType: 'OTHER', 76 | errorMessage: 'Cannot open file' 77 | }, 78 | file_2: { 79 | errorType: 'PARSING', 80 | errorMessage: 'record on line 2: wrong number of fields' 81 | } 82 | }); 83 | expect( 84 | component.containsAnyMatchingElements([ 85 | 86 | ]) 87 | ).toEqual(true); 88 | 89 | expect( 90 | component.containsAnyMatchingElements([ 91 | 92 | ]) 93 | ).toEqual(true); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/components/__snapshots__/IntroductionFormCard.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`IntroductionFormCard component should match exact snapshot 1`] = ` 4 |
5 |
8 |
11 |
14 |

17 | Using Clear My Record will expedite the process of determining which Prop 64 convictions are eligible for relief under H&S § 11361.9. 18 |

19 |

20 | Here's what you need to do: 21 |

22 |
25 |
28 |
31 |

34 | Import bulk conviction data 35 |

36 |

39 | Choose your county and import the bulk Prop 64 conviction data file received from the DOJ. 40 |

41 |
42 |
45 |
48 |

51 | Select eligibility requirements 52 |

53 |

56 | Determine which convictions to reduce or dismiss. 57 |

58 |
59 |
62 |
65 |

68 | Review files 69 |

70 |

73 | Open and review files which now include Prop 64 eligibility determinations. 74 |

75 |
76 |
77 |
80 | If you run into problems or have questions, visit our 81 | 82 | 87 | FAQ 88 | 89 | 90 | or contact us at clearmyrecord@codeforamerica.org 91 |
92 |
95 | 102 |
103 |
104 |
105 |
106 |
107 | `; 108 | -------------------------------------------------------------------------------- /app/components/IntroductionFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import FormCard, { FormCardContent } from './FormCard'; 4 | import nonLinearScreenNumbers from '../constants/nonLinearScreenNumbers'; 5 | import styles from './IntroductionFormCard.css'; 6 | 7 | type Props = { 8 | onBegin: () => void, 9 | goToScreen: number => void 10 | }; 11 | 12 | export default class CountySelectFormCard extends Component { 13 | componentDidMount() { 14 | window.scrollTo(0, 0); 15 | } 16 | 17 | goToFaq = () => { 18 | const { goToScreen } = this.props; 19 | goToScreen(nonLinearScreenNumbers.faq); 20 | }; 21 | 22 | render() { 23 | const { onBegin } = this.props; 24 | return ( 25 | 26 | 27 |
28 |

29 | Using Clear My Record will expedite the process of determining 30 | which Prop 64 convictions are eligible for relief under H&S § 31 | 11361.9. 32 |

33 |

Here's what you need to do:

34 |
35 |
36 |
37 |

38 | Import bulk conviction data 39 |

40 |

41 | Choose your county and import the bulk Prop 64 conviction data 42 | file received from the DOJ. 43 |

44 |
45 |
46 |
47 |

48 | Select eligibility requirements 49 |

50 |

51 | Determine which convictions to reduce or dismiss. 52 |

53 |
54 |
55 |
56 |

Review files

57 |

58 | Open and review files which now include Prop 64 eligibility 59 | determinations. 60 |

61 |
62 |
63 |
64 | If you run into problems or have questions, visit our{' '} 65 | 66 | FAQ 67 | {' '} 68 | or contact us at clearmyrecord@codeforamerica.org 69 |
70 |
71 | 79 |
80 |
81 | 82 | 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/main.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 2 | /** 3 | * This module executes inside of electron's main process. You can start 4 | * electron renderer process from here and communicate with the other processes 5 | * through IPC. 6 | * 7 | * When running `yarn build` or `yarn build-main`, this file is compiled to 8 | * `./app/main.prod.js` using webpack. This gives us some performance wins. 9 | * 10 | * @flow 11 | */ 12 | import { app, shell, BrowserWindow } from 'electron'; 13 | import { autoUpdater } from 'electron-updater'; 14 | import log from 'electron-log'; 15 | 16 | export default class AppUpdater { 17 | constructor() { 18 | log.transports.file.level = 'info'; 19 | autoUpdater.logger = log; 20 | autoUpdater.checkForUpdatesAndNotify(); 21 | } 22 | } 23 | 24 | let mainWindow = null; 25 | 26 | if (process.env.NODE_ENV === 'production') { 27 | const sourceMapSupport = require('source-map-support'); 28 | sourceMapSupport.install(); 29 | } 30 | 31 | if ( 32 | process.env.NODE_ENV === 'development' || 33 | process.env.DEBUG_PROD === 'true' 34 | ) { 35 | require('electron-debug')(); 36 | } 37 | 38 | const installExtensions = async () => { 39 | const installer = require('electron-devtools-installer'); 40 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 41 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; 42 | 43 | return Promise.all( 44 | extensions.map(name => installer.default(installer[name], forceDownload)) 45 | ).catch(console.log); 46 | }; 47 | 48 | /** 49 | * Add event listeners... 50 | */ 51 | 52 | app.on('window-all-closed', () => { 53 | // Respect the OSX convention of having the application in memory even 54 | // after all windows have been closed 55 | if (process.platform !== 'darwin') { 56 | app.quit(); 57 | } 58 | }); 59 | 60 | app.on('ready', async () => { 61 | if ( 62 | process.env.NODE_ENV === 'development' || 63 | process.env.DEBUG_PROD === 'true' 64 | ) { 65 | await installExtensions(); 66 | } 67 | 68 | process.env.IS_PACKAGED = !!app.isPackaged; 69 | process.env.PLATFORM = process.platform; 70 | 71 | mainWindow = new BrowserWindow({ 72 | show: false, 73 | width: 1024, 74 | height: 728, 75 | webPreferences: { 76 | nodeIntegration: true 77 | } 78 | }); 79 | mainWindow.maximize(); 80 | mainWindow.loadURL(`file://${__dirname}/app.html`); 81 | 82 | if (process.env.NODE_ENV === 'development') { 83 | mainWindow.webContents.openDevTools(); 84 | } 85 | 86 | // @TODO: Use 'ready-to-show' event 87 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event 88 | mainWindow.webContents.on('did-finish-load', () => { 89 | if (!mainWindow) { 90 | throw new Error('"mainWindow" is not defined'); 91 | } 92 | if (process.env.START_MINIMIZED) { 93 | mainWindow.minimize(); 94 | } else { 95 | mainWindow.show(); 96 | mainWindow.focus(); 97 | } 98 | }); 99 | 100 | mainWindow.webContents.on('new-window', (event, url) => { 101 | event.preventDefault(); 102 | shell.openExternal(url).catch(console.log); 103 | }); 104 | 105 | mainWindow.on('closed', () => { 106 | mainWindow = null; 107 | }); 108 | 109 | mainWindow.removeMenu(); 110 | 111 | // Remove this if your app does not use auto updates 112 | // eslint-disable-next-line 113 | new AppUpdater(); 114 | }); 115 | -------------------------------------------------------------------------------- /app/components/PageFooter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { version } from '../../package.json'; 4 | import nonLinearScreenNumbers from '../constants/nonLinearScreenNumbers'; 5 | import styles from './PageFooter.css'; 6 | import cmrLogo from '../assets/images/cmr_black_logo.png'; 7 | 8 | type Props = { 9 | goToScreen: number => void, 10 | onStartOver: () => void 11 | }; 12 | 13 | export default class PageFooter extends Component { 14 | goToTermsOfService = () => { 15 | const { goToScreen } = this.props; 16 | goToScreen(nonLinearScreenNumbers.termsOfService); 17 | }; 18 | 19 | goToFaq = () => { 20 | const { goToScreen } = this.props; 21 | goToScreen(nonLinearScreenNumbers.faq); 22 | }; 23 | 24 | onClickStartOver = (event: Event) => { 25 | const { onStartOver } = this.props; 26 | event.preventDefault(); 27 | onStartOver(); 28 | }; 29 | 30 | render() { 31 | return ( 32 | 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/ci-e2e/ciHomeIntegration.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import sleep from '../../app/utils/testHelpers'; 3 | import { 4 | getEligibilityConfigFilePath, 5 | getOutputDirectoryPath, 6 | removeOutputDirectory 7 | } from '../e2e/helpers'; 8 | 9 | const { Application } = require('spectron'); 10 | const electronPath = require('electron'); // Require Electron from the binaries included in node_modules. 11 | const path = require('path'); 12 | 13 | let outputDirectory; 14 | 15 | describe('The happy path with additional relief', () => { 16 | let app; 17 | let pageTitle; 18 | 19 | beforeEach(() => { 20 | app = new Application({ 21 | path: electronPath, 22 | args: [path.join(__dirname, '../..')] 23 | }); 24 | return app.start(); 25 | }); 26 | 27 | afterEach(() => { 28 | removeOutputDirectory(outputDirectory); 29 | if (app && app.isRunning()) { 30 | return app.stop(); 31 | } 32 | }); 33 | 34 | it('can complete the full flow and generate correct eligibility config', async () => { 35 | jest.setTimeout(30000); 36 | 37 | await app.client.click('#begin'); 38 | 39 | pageTitle = await app.client.getText('.form-card__title'); 40 | expect(pageTitle).toEqual('CA County Selection'); 41 | 42 | const countySelect = app.client.$('#county-select'); 43 | await countySelect.selectByVisibleText('Sacramento'); 44 | await app.client.click('#continue'); 45 | 46 | pageTitle = await app.client.getText('.form-card__title'); 47 | expect(pageTitle).toEqual('Import Prop 64 bulk conviction data files'); 48 | 49 | await app.client.chooseFile('#doj-file-input', './test/fixtures/file.dat'); 50 | await app.client.click('#continue'); 51 | 52 | pageTitle = await app.client.getText('.form-card__title'); 53 | expect(pageTitle).toContain('Baseline eligibility'); 54 | 55 | await app.client.click('#reduce_11360'); 56 | await app.client.click('#continue'); 57 | 58 | pageTitle = await app.client.getText('.form-card__title'); 59 | expect(pageTitle).toContain('Additional relief'); 60 | 61 | const yearSelect = app.client.$('#yearsSinceConvictionThreshold-select'); 62 | await yearSelect.selectByVisibleText('3'); 63 | 64 | await app.client.click('#true_subjectHasOnlyProp64Charges'); 65 | await app.client.click('#continue'); 66 | 67 | outputDirectory = getOutputDirectoryPath(); 68 | 69 | const eligibilityConfigFilePath = getEligibilityConfigFilePath(); 70 | const eligibilityConfigFileContents = fs.readFileSync( 71 | eligibilityConfigFilePath, 72 | 'utf8' 73 | ); 74 | const eligibilityConfig = JSON.parse(eligibilityConfigFileContents); 75 | 76 | expect(eligibilityConfig).toEqual({ 77 | baselineEligibility: { 78 | dismiss: ['11357', '11358', '11359'], 79 | reduce: ['11360'] 80 | }, 81 | additionalRelief: { 82 | subjectUnder21AtConviction: false, 83 | dismissOlderThanAgeThreshold: false, 84 | subjectAgeThreshold: 0, 85 | dismissYearsSinceConvictionThreshold: true, 86 | yearsSinceConvictionThreshold: 3, 87 | dismissYearsCrimeFreeThreshold: true, 88 | yearsCrimeFreeThreshold: 5, 89 | subjectHasOnlyProp64Charges: false, 90 | subjectIsDeceased: false 91 | } 92 | }); 93 | 94 | const processingCardContent = await app.client.getText( 95 | '.form-card__content h3' 96 | ); 97 | expect(processingCardContent).toContain('Reading and preparing files ...'); 98 | 99 | await sleep(6); 100 | const resultsFormCardContent = await app.client.getText( 101 | '.form-card__title' 102 | ); 103 | expect(resultsFormCardContent).toContain('Your files are ready!'); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/components/DojFileSelectFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import DojFileSelectFormCard from '../../app/components/DojFileSelectFormCard'; 7 | import ContinueButton from '../../app/components/ContinueButton'; 8 | import DojFileItem from '../../app/components/DojFileItem'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | const sandbox = sinon.createSandbox(); 12 | 13 | function setup(dojFilePaths) { 14 | const fakeUpdateFilePath = sandbox.spy(); 15 | const onFileConfirmSpy = sandbox.spy(); 16 | const onBackSpy = sandbox.spy(); 17 | const component = mount( 18 | 24 | ); 25 | return { 26 | component, 27 | onFileConfirmSpy, 28 | onBackSpy 29 | }; 30 | } 31 | 32 | afterEach(() => { 33 | sandbox.restore(); 34 | }); 35 | 36 | describe('DojFileSelectFormCard component', () => { 37 | describe('the Continue button', () => { 38 | it('should appear as disabled if the file path is empty', () => { 39 | const { component } = setup([]); 40 | expect(component.find('ContinueButton').props().disabled).toEqual(true); 41 | }); 42 | 43 | it('should appear if the file path is not empty', () => { 44 | const { component } = setup(['path/to/file']); 45 | expect( 46 | component.containsAnyMatchingElements([]) 47 | ).toEqual(true); 48 | }); 49 | }); 50 | 51 | describe('the file name', () => { 52 | it('should not appear if the file path is empty', () => { 53 | const { component } = setup([]); 54 | expect(component.containsAnyMatchingElements([])).toEqual( 55 | false 56 | ); 57 | }); 58 | 59 | it('should appear if the file path is not empty', () => { 60 | const { component } = setup(['path/to/file']); 61 | expect(component.containsAnyMatchingElements([])).toEqual( 62 | true 63 | ); 64 | }); 65 | }); 66 | 67 | describe('clicking the continue button', () => { 68 | it('should call onFileConfirm with the next screen number', () => { 69 | const { component, onFileConfirmSpy } = setup(['path/to/file']); 70 | component.find('#continue').simulate('click'); 71 | expect(onFileConfirmSpy.called).toBe(true); 72 | expect(onFileConfirmSpy.callCount).toEqual(1); 73 | }); 74 | }); 75 | 76 | describe('clicking the go back button', () => { 77 | it('should call onFileConfirm with the previous screen number', () => { 78 | const { component, onBackSpy } = setup(['path/to/file']); 79 | component.find('#goback').simulate('click'); 80 | expect(onBackSpy.called).toBe(true); 81 | expect(onBackSpy.callCount).toEqual(1); 82 | }); 83 | }); 84 | 85 | it('should match exact snapshot when file has not been selected', () => { 86 | const component = ( 87 |
88 | 89 |
90 | ); 91 | 92 | const tree = renderer.create(component).toJSON(); 93 | 94 | expect(tree).toMatchSnapshot(); 95 | }); 96 | 97 | it('should match exact snapshot when file has been selected', () => { 98 | const component = ( 99 |
100 | 101 |
102 | ); 103 | 104 | const tree = renderer.create(component).toJSON(); 105 | 106 | expect(tree).toMatchSnapshot(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/components/EligibilityOptionsFormCard.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import EligibilityOptionsFormCard from '../../app/components/EligibilityOptionsFormCard'; 7 | import BaselineEligibilityOption from '../../app/components/BaselineEligibilityOption'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | const sandbox = sinon.createSandbox(); 11 | 12 | function setup(isAllDismiss) { 13 | const options = { 14 | '11357(a)': 'reduce', 15 | '11357(b)': 'dismiss', 16 | '11357(c)': 'reduce', 17 | '11357(d)': 'reduce', 18 | '11358': 'dismiss', 19 | '11359': 'reduce', 20 | '11360': 'dismiss' 21 | }; 22 | 23 | const onOptionsConfirmSpy = sandbox.spy(); 24 | const onBackSpy = sandbox.spy(); 25 | const onUpdateDateSpy = sandbox.spy(); 26 | const component = mount( 27 | 34 | ); 35 | return { 36 | options, 37 | component, 38 | onOptionsConfirmSpy, 39 | onBackSpy, 40 | onUpdateDateSpy 41 | }; 42 | } 43 | afterEach(() => { 44 | sandbox.restore(); 45 | }); 46 | 47 | describe('EligibilityOptionsFormCard component', () => { 48 | it('should render each option with the correct selection (dismiss or reduce)', () => { 49 | const { component } = setup(false); 50 | expect( 51 | component.containsAnyMatchingElements([ 52 | 53 | ]) 54 | ).toEqual(true); 55 | expect( 56 | component.containsAnyMatchingElements([ 57 | 58 | ]) 59 | ).toEqual(true); 60 | expect( 61 | component.containsAnyMatchingElements([ 62 | 63 | ]) 64 | ).toEqual(true); 65 | expect( 66 | component.containsAnyMatchingElements([ 67 | 68 | ]) 69 | ).toEqual(true); 70 | }); 71 | 72 | describe('clicking the continue button', () => { 73 | it('should call onOptionsConfirm once', () => { 74 | const { component, onOptionsConfirmSpy } = setup(false); 75 | component.find('#continue').simulate('click'); 76 | expect(onOptionsConfirmSpy.called).toBe(true); 77 | expect(onOptionsConfirmSpy.callCount).toEqual(1); 78 | }); 79 | 80 | describe('updating date', () => { 81 | it('should call updateDate once if all charges dismissed', () => { 82 | const { component, onUpdateDateSpy } = setup(true); 83 | component.find('#continue').simulate('click'); 84 | expect(onUpdateDateSpy.called).toBe(true); 85 | expect(onUpdateDateSpy.callCount).toEqual(1); 86 | }); 87 | 88 | it('should not call updateDate if isAllDismiss is false', () => { 89 | const { component, onUpdateDateSpy } = setup(false); 90 | component.find('#continue').simulate('click'); 91 | expect(onUpdateDateSpy.called).toBe(false); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('clicking the go back button', () => { 97 | it('should call onBack once', () => { 98 | const { component, onBackSpy } = setup(false); 99 | component.find('#goback').simulate('click'); 100 | expect(onBackSpy.called).toBe(true); 101 | expect(onBackSpy.callCount).toEqual(1); 102 | }); 103 | }); 104 | 105 | it('should match exact snapshot', () => { 106 | const options = { '11357(a)': 'dismiss' }; 107 | const component = ( 108 |
109 | 113 |
114 | ); 115 | 116 | const tree = renderer.create(component).toJSON(); 117 | 118 | expect(tree).toMatchSnapshot(); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /app/components/EligibilityOptionsFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import FormCard, { 5 | FormCardContent, 6 | FormCardFooter, 7 | FormCardHeader 8 | } from './FormCard'; 9 | import styles from './FormCard.css'; 10 | import BaselineEligibilityOption from './BaselineEligibilityOption'; 11 | import ContinueButton from './ContinueButton'; 12 | import GoBackButton from './GoBackButton'; 13 | 14 | type Props = { 15 | baselineEligibilityOptions: BaselineEligibilityOptions, 16 | onEligibilityOptionSelect: (string, string) => void, 17 | onOptionsConfirm: void => void, 18 | updateDate: void => void, 19 | onBack: void => void, 20 | isAllDismiss: boolean 21 | }; 22 | 23 | export default class EligibilityOptionsFormCard extends Component { 24 | componentDidMount() { 25 | window.scrollTo(0, 0); 26 | } 27 | 28 | onContinue = () => { 29 | const { onOptionsConfirm, updateDate, isAllDismiss } = this.props; 30 | 31 | if (isAllDismiss) { 32 | updateDate(); 33 | } 34 | onOptionsConfirm(); 35 | }; 36 | 37 | render() { 38 | const { 39 | baselineEligibilityOptions, 40 | onEligibilityOptionSelect, 41 | onBack 42 | } = this.props; 43 | return ( 44 | 45 | 46 | Baseline eligibility 47 | 48 | 51 |
52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 |
Misdemeanors and Infractions
57 | All misdemeanors and infractions will be marked as eligible 58 | for dismissal. 59 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {[ 76 | { 77 | codeSection: '11357', 78 | description: '11357 felonies including all subsections' 79 | }, 80 | { 81 | codeSection: '11358', 82 | description: '11358 felonies including all subsections' 83 | }, 84 | { 85 | codeSection: '11359', 86 | description: '11359 felonies including all subsections' 87 | }, 88 | { 89 | codeSection: '11360', 90 | description: '11360 felonies including all subsections' 91 | } 92 | ].map(option => { 93 | return ( 94 | 101 | ); 102 | })} 103 | 104 |
Felonies
Type of convictionsDismissReduce
105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/components/AdditionalReliefFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import FormCard, { 5 | FormCardContent, 6 | FormCardFooter, 7 | FormCardHeader 8 | } from './FormCard'; 9 | import ContinueButton from './ContinueButton'; 10 | import GoBackButton from './GoBackButton'; 11 | import Checkbox from './Checkbox'; 12 | import NumberSelect from './NumberSelect'; 13 | 14 | type Props = { 15 | additionalReliefOptions: AdditionalReliefOptions, 16 | onReliefOptionSelect: (string, AdditionalReliefValue) => void, 17 | onOptionsConfirm: void => void, 18 | updateDate: void => void, 19 | onBack: void => void 20 | }; 21 | 22 | export default class AdditionalReliefFormCard extends Component { 23 | componentDidMount() { 24 | window.scrollTo(0, 0); 25 | } 26 | 27 | onContinue = () => { 28 | const { onOptionsConfirm, updateDate } = this.props; 29 | onOptionsConfirm(); 30 | updateDate(); 31 | }; 32 | 33 | handleToggleChecked = (group: string) => { 34 | const { onReliefOptionSelect, additionalReliefOptions } = this.props; 35 | 36 | const toggledValue = !additionalReliefOptions[group]; 37 | onReliefOptionSelect(group, toggledValue); 38 | }; 39 | 40 | handleNumberSelect = (group: string, selectedNumber: number) => { 41 | const { onReliefOptionSelect } = this.props; 42 | onReliefOptionSelect(group, selectedNumber); 43 | }; 44 | 45 | render() { 46 | const { additionalReliefOptions, onBack } = this.props; 47 | return ( 48 | 49 | 50 | Additional relief 51 | 52 | 53 | 61 | Dismiss convictions that occurred more than X years ago: 62 | 72 | 73 | 79 | Dismiss convictions if the individual has had no convictions in the 80 | past X years: 81 | 89 | 90 | 96 | Dismiss all H&S § 11357, H&S § 11358, H&S § 11359, or H&S § 11360 97 | convictions if those are the only convictions on an 98 | individual's record. 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/utils/gogenUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-unused-expressions */ 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { 6 | createJsonFile, 7 | makeDirectory, 8 | deleteDirectoryRecursive 9 | } from './fileUtils'; 10 | import { writeSummaryReport } from './writeSummaryOutputUtils'; 11 | 12 | function transformBaselineEligibilityOptions( 13 | eligibilityOptions: BaselineEligibilityOptions 14 | ) { 15 | const jsonObject = { baselineEligibility: { dismiss: [], reduce: [] } }; 16 | Object.keys(eligibilityOptions) 17 | .sort() 18 | .forEach(codeSection => { 19 | eligibilityOptions[codeSection] === 'dismiss' 20 | ? jsonObject.baselineEligibility.dismiss.push(codeSection) 21 | : jsonObject.baselineEligibility.reduce.push(codeSection); 22 | }); 23 | return jsonObject; 24 | } 25 | 26 | function transformOptionalReliefValues( 27 | additionalReliefOptions: AdditionalReliefOptions 28 | ) { 29 | const transformedOptions = { 30 | yearsSinceConvictionThreshold: 0, 31 | yearsCrimeFreeThreshold: 0 32 | }; 33 | 34 | if (additionalReliefOptions.dismissYearsSinceConvictionThreshold) { 35 | transformedOptions.yearsSinceConvictionThreshold = 36 | additionalReliefOptions.yearsSinceConvictionThreshold; 37 | } 38 | 39 | if (additionalReliefOptions.dismissYearsCrimeFreeThreshold) { 40 | transformedOptions.yearsCrimeFreeThreshold = 41 | additionalReliefOptions.yearsCrimeFreeThreshold; 42 | } 43 | 44 | return transformedOptions; 45 | } 46 | 47 | function readGogenErrors(outputFilePath: string, fileNameSuffix: string) { 48 | const pathToErrors = path.join(outputFilePath, `gogen_${fileNameSuffix}.err`); 49 | return fs.readFileSync(pathToErrors, 'utf8'); 50 | } 51 | 52 | function removeResultsDirectories( 53 | dojFilePaths: Array, 54 | outputFilePath: string, 55 | fileNameSuffix: string 56 | ) { 57 | dojFilePaths.forEach((_, index) => { 58 | const resultDirectory = path.join( 59 | outputFilePath, 60 | `DOJ_Input_File_${index + 1}_Results_${fileNameSuffix}` 61 | ); 62 | deleteDirectoryRecursive(resultDirectory); 63 | }); 64 | } 65 | 66 | // eslint-disable-next-line import/prefer-default-export 67 | export function runScript( 68 | state: ApplicationState, 69 | spawnChildProcess: function, 70 | onGogenComplete: function, 71 | updateImpactStatistics: function, 72 | preserveEligibilityConfig: boolean 73 | ) { 74 | const { 75 | gogenPath, 76 | formattedGogenRunTime, 77 | county, 78 | dojFilePaths, 79 | baselineEligibilityOptions, 80 | additionalReliefOptions, 81 | outputFilePath 82 | } = state; 83 | makeDirectory(outputFilePath); 84 | const JsonFileName = `eligibilityConfig_${formattedGogenRunTime}.json`; 85 | const pathToEligibilityOptions = path.join(outputFilePath, JsonFileName); 86 | const formattedEligibilityOptions = transformBaselineEligibilityOptions( 87 | baselineEligibilityOptions 88 | ); 89 | 90 | const formattedAdditionalReliefOptions = { 91 | ...additionalReliefOptions, 92 | ...transformOptionalReliefValues(additionalReliefOptions) 93 | }; 94 | 95 | const eligibilityLogicConfig = { 96 | ...formattedEligibilityOptions, 97 | additionalRelief: formattedAdditionalReliefOptions 98 | }; 99 | 100 | createJsonFile(eligibilityLogicConfig, pathToEligibilityOptions); 101 | const countyCode = county.code; 102 | 103 | const fileNameSuffix = formattedGogenRunTime; 104 | 105 | const goProcess = spawnChildProcess( 106 | gogenPath, 107 | [ 108 | `run`, 109 | `--file-name-suffix=${fileNameSuffix}`, 110 | `--input-doj=${dojFilePaths.join(',')}`, 111 | `--outputs=${outputFilePath}`, 112 | `--county=${countyCode}`, 113 | `--eligibility-options=${pathToEligibilityOptions}`, 114 | `--compute-at=2020-07-01` 115 | ], 116 | { stdio: 'ignore' } 117 | ); 118 | 119 | goProcess.on('exit', code => { 120 | let errorText = ''; 121 | if (code !== 0) { 122 | errorText = readGogenErrors(outputFilePath, fileNameSuffix); 123 | removeResultsDirectories(dojFilePaths, outputFilePath, fileNameSuffix); 124 | } else { 125 | const summaryData = parseGogenOutput(outputFilePath, fileNameSuffix); 126 | updateImpactStatistics(summaryData.reliefWithCurrentEligibilityChoices); 127 | writeSummaryReport( 128 | summaryData, 129 | outputFilePath, 130 | dojFilePaths, 131 | formattedEligibilityOptions, 132 | formattedGogenRunTime 133 | ); 134 | if (!preserveEligibilityConfig) { 135 | fs.unlinkSync(pathToEligibilityOptions); 136 | } 137 | } 138 | onGogenComplete(code, errorText); 139 | }); 140 | } 141 | 142 | function parseGogenOutput(outputFilePath: string, fileNameSuffix: string) { 143 | const pathToGogenOutput = path.join( 144 | outputFilePath, 145 | `gogen_${fileNameSuffix}.json` 146 | ); 147 | const gogenOutputData = fs.readFileSync(pathToGogenOutput, 'utf8'); 148 | fs.unlinkSync(pathToGogenOutput); 149 | return JSON.parse(gogenOutputData); 150 | } 151 | -------------------------------------------------------------------------------- /test/components/__snapshots__/DojFileSelectFormCard.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DojFileSelectFormCard component should match exact snapshot when file has been selected 1`] = ` 4 |
5 |
8 |
11 |

14 | Import Prop 64 bulk conviction data files 15 |

16 |
19 | Import the original .dat files received from the DOJ. 20 |
21 |
22 |
25 |

28 | Choose .dat files to import 29 |

30 |
33 |
34 |
37 |

40 | File imported: 41 | file 42 | 47 |

48 |
49 |
50 | 65 |
66 |
67 |
68 |
69 |
72 |
75 | 86 | 96 |
97 |
98 |
99 |
100 | `; 101 | 102 | exports[`DojFileSelectFormCard component should match exact snapshot when file has not been selected 1`] = ` 103 |
104 |
107 |
110 |

113 | Import Prop 64 bulk conviction data files 114 |

115 |
118 | Import the original .dat files received from the DOJ. 119 |
120 |
121 |
124 |

127 | Choose .dat files to import 128 |

129 |
132 |
133 | 148 |

152 | No file selected 153 |

154 |
155 |
156 |
157 |
160 |
163 | 174 | 184 |
185 |
186 |
187 |
188 | `; 189 | -------------------------------------------------------------------------------- /test/components/ProgressBar.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import renderer from 'react-test-renderer'; 6 | import ProgressBar, { 7 | PROCESSING_RATE, 8 | MAX_STEP_SIZE, 9 | MAX_FILL 10 | } from '../../app/components/ProgressBar'; 11 | import sleep from '../../app/utils/testHelpers'; 12 | 13 | Enzyme.configure({ adapter: new Adapter() }); 14 | const sandbox = sinon.createSandbox(); 15 | 16 | function setup(fileSize = 1000) { 17 | const onCompleteCallbackSpy = sandbox.spy(); 18 | const component = shallow( 19 | 24 | ); 25 | return { 26 | component, 27 | onCompleteCallbackSpy 28 | }; 29 | } 30 | 31 | afterEach(() => { 32 | sandbox.restore(); 33 | }); 34 | 35 | describe('ProgressBar component', () => { 36 | describe('setup', () => { 37 | it('defaults the stepSize to the maximum if the computed stepSize would be > maximum', () => { 38 | const { component } = setup(PROCESSING_RATE / 100); 39 | expect(component.instance().state.stepSize).toEqual(MAX_STEP_SIZE); 40 | }); 41 | 42 | it('uses the computed stepSize if the stepSize is <= maximum', () => { 43 | const { component } = setup(PROCESSING_RATE * 100); 44 | expect(component.instance().state.stepSize).toEqual(1); 45 | }); 46 | 47 | it('initializes fill to zero', async () => { 48 | const { component } = setup(); 49 | expect(component.instance().state.fill).toEqual(0); 50 | }); 51 | }); 52 | 53 | describe('tick', () => { 54 | describe('when the gogen process is complete', () => { 55 | describe('when the progress bar is full', () => { 56 | it('calls the gogen complete callback', () => { 57 | const { component, onCompleteCallbackSpy } = setup(); 58 | component.setProps({ isComplete: true }); 59 | component.setState({ fill: MAX_FILL }); 60 | 61 | const instance = component.instance(); 62 | instance.tick(); 63 | 64 | expect(onCompleteCallbackSpy.called).toBe(true); 65 | }); 66 | }); 67 | 68 | describe('when the bar is not full and the file takes LESS than the minimum processing time', () => { 69 | it('increases fill by the stepSize amount', () => { 70 | const { component } = setup(); 71 | component.setProps({ isComplete: true }); 72 | 73 | const instance = component.instance(); 74 | instance.tick(); 75 | expect(instance.state.fill).toEqual(20); 76 | instance.tick(); 77 | expect(instance.state.fill).toEqual(40); 78 | }); 79 | }); 80 | 81 | describe('when the bar is not full and the file takes MORE than the minimum processing time', () => { 82 | it('sets the bar to full', () => { 83 | const { component } = setup(PROCESSING_RATE * 100); 84 | component.setProps({ isComplete: true }); 85 | 86 | const instance = component.instance(); 87 | instance.tick(); 88 | expect(instance.state.fill).toEqual(MAX_FILL); 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('lifecycle', () => { 95 | it('advances fill with the passage of time', async () => { 96 | const { component } = setup(); 97 | expect(component.instance().state.fill).toEqual(0); 98 | await sleep(3); 99 | expect(component.instance().state.fill > 0).toBe(true); 100 | }); 101 | }); 102 | 103 | describe('termination', () => { 104 | describe('when fill is 100 but isComplete not set', () => { 105 | it('does not call the onCompleteCallback method', () => { 106 | const { component, onCompleteCallbackSpy } = setup(); 107 | const instance = component.instance(); 108 | 109 | for (let i = 0; i < 10; i += 1) { 110 | instance.tick(); 111 | } 112 | expect(onCompleteCallbackSpy.called).toBe(false); 113 | }); 114 | }); 115 | 116 | describe('when fill is not 100 and isComplete is set', () => { 117 | it('does not call the onCompleteCallback method', () => { 118 | const { component, onCompleteCallbackSpy } = setup(); 119 | const instance = component.instance(); 120 | 121 | for (let i = 0; i < 3; i += 1) { 122 | instance.tick(); 123 | } 124 | component.setProps({ isComplete: true }); 125 | expect(onCompleteCallbackSpy.called).toBe(false); 126 | }); 127 | }); 128 | 129 | describe('when fill is 100 and isComplete is set', () => { 130 | it('calls the onCompleteCallback method', () => { 131 | const { component, onCompleteCallbackSpy } = setup(); 132 | const instance = component.instance(); 133 | 134 | component.setProps({ isComplete: true }); 135 | instance.setState({ fill: 100 }); 136 | instance.tick(); 137 | expect(onCompleteCallbackSpy.called).toBe(true); 138 | }); 139 | }); 140 | }); 141 | 142 | it('should match exact snapshot', () => { 143 | const { component } = setup(); 144 | const tree = renderer.create(component).toJSON(); 145 | 146 | expect(tree).toMatchSnapshot(); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /app/components/ResultsFormCard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import FormCard, { 4 | FormCardContent, 5 | FormCardFooter, 6 | FormCardHeader 7 | } from './FormCard'; 8 | // @flow 9 | import StartOverButton from './StartOverButton'; 10 | import ImpactItem from './ImpactItem'; 11 | import styles from './ResultsFormCard.css'; 12 | 13 | type Props = { 14 | county: County, 15 | impactStatistics: ImpactStatistics, 16 | outputFolder: string, 17 | openFolder: string => void, 18 | onStartOver: () => void 19 | }; 20 | 21 | export default class ResultsFormCard extends Component { 22 | componentDidMount() { 23 | window.scrollTo(0, 0); 24 | } 25 | 26 | openResultsFolder = () => { 27 | const { openFolder, outputFolder } = this.props; 28 | openFolder(outputFolder); 29 | }; 30 | 31 | render() { 32 | const { county, onStartOver, impactStatistics } = this.props; 33 | return ( 34 | 35 | 36 |
37 |
38 |

39 | Your files are ready! 40 |

41 |

42 | {`We have generated results for ${county.name} County.`} Look for 43 | a folder on your desktop labeled 44 | "Clear_My_Record_output". Within it will be a 45 | timestamped folder that will have all of your results files. 46 |

47 | 55 |
56 | 57 | 58 |
59 |

60 | Based on your eligibility choices: 61 |

62 | 66 | 70 | 74 |
75 |
76 |

77 | What's included in the folder: 78 |

79 |
80 |
    81 |
  1. Summary Report
  2. 82 |
  3. Prop 64 Conviction Results
  4. 83 |
  5. All Results Condensed
  6. 84 |
  7. All Results
  8. 85 |
86 |
87 | 88 | 89 | 90 | 91 | 94 | 97 | 100 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 124 | 127 | 128 | 129 | 130 | 131 | 132 | 135 | 138 | 139 | 140 |
File Name 92 | Columns from original DOJ file 93 | 95 | Rows from original DOJ file 96 | 98 | Supporting Clear My Record columns 99 | 101 | Eligibility determination 102 |
Prop 64 ConvictionsAllOnly Prop 64 convictions 111 |
112 |
114 |
115 |
All Results CondensedCondensed for analysisAll 122 |
123 |
125 |
126 |
All ResultsAllAll 133 |
134 |
136 |
137 |
141 | 142 | 143 |
144 | 145 |
146 |
147 | 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 8 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import merge from 'webpack-merge'; 11 | import TerserPlugin from 'terser-webpack-plugin'; 12 | import baseConfig from './webpack.config.base'; 13 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 14 | 15 | CheckNodeEnv('production'); 16 | export default merge.smart(baseConfig, { 17 | devtool: 'source-map', 18 | 19 | mode: 'production', 20 | 21 | target: 'electron-renderer', 22 | 23 | entry: path.join(__dirname, '..', 'app/index'), 24 | 25 | output: { 26 | path: path.join(__dirname, '..', 'app/dist'), 27 | publicPath: './dist/', 28 | filename: 'renderer.prod.js' 29 | }, 30 | 31 | module: { 32 | rules: [ 33 | // Extract all .global.css to style.css as is 34 | { 35 | test: /\.global\.css$/, 36 | use: [ 37 | { 38 | loader: MiniCssExtractPlugin.loader, 39 | options: { 40 | publicPath: './' 41 | } 42 | }, 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | sourceMap: true 47 | } 48 | } 49 | ] 50 | }, 51 | // Pipe other styles through css modules and append to style.css 52 | { 53 | test: /^((?!\.global).)*\.css$/, 54 | use: [ 55 | { 56 | loader: MiniCssExtractPlugin.loader 57 | }, 58 | { 59 | loader: 'css-loader', 60 | options: { 61 | modules: true, 62 | localIdentName: '[name]__[local]__[hash:base64:5]', 63 | sourceMap: true 64 | } 65 | } 66 | ] 67 | }, 68 | // Add SASS support - compile all .global.scss files and pipe it to style.css 69 | { 70 | test: /\.global\.(scss|sass)$/, 71 | use: [ 72 | { 73 | loader: MiniCssExtractPlugin.loader 74 | }, 75 | { 76 | loader: 'css-loader', 77 | options: { 78 | sourceMap: true, 79 | importLoaders: 1 80 | } 81 | }, 82 | { 83 | loader: 'sass-loader', 84 | options: { 85 | sourceMap: true 86 | } 87 | } 88 | ] 89 | }, 90 | // Add SASS support - compile all other .scss files and pipe it to style.css 91 | { 92 | test: /^((?!\.global).)*\.(scss|sass)$/, 93 | use: [ 94 | { 95 | loader: MiniCssExtractPlugin.loader 96 | }, 97 | { 98 | loader: 'css-loader', 99 | options: { 100 | modules: true, 101 | importLoaders: 1, 102 | localIdentName: '[name]__[local]__[hash:base64:5]', 103 | sourceMap: true 104 | } 105 | }, 106 | { 107 | loader: 'sass-loader', 108 | options: { 109 | sourceMap: true 110 | } 111 | } 112 | ] 113 | }, 114 | // WOFF Font 115 | { 116 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 117 | use: { 118 | loader: 'url-loader', 119 | options: { 120 | limit: 10000, 121 | mimetype: 'application/font-woff' 122 | } 123 | } 124 | }, 125 | // WOFF2 Font 126 | { 127 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 128 | use: { 129 | loader: 'url-loader', 130 | options: { 131 | limit: 10000, 132 | mimetype: 'application/font-woff' 133 | } 134 | } 135 | }, 136 | // TTF Font 137 | { 138 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 139 | use: { 140 | loader: 'url-loader', 141 | options: { 142 | limit: 10000, 143 | mimetype: 'application/octet-stream' 144 | } 145 | } 146 | }, 147 | // EOT Font 148 | { 149 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 150 | use: 'file-loader' 151 | }, 152 | // SVG Font 153 | { 154 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 155 | use: { 156 | loader: 'url-loader', 157 | options: { 158 | limit: 10000, 159 | mimetype: 'image/svg+xml' 160 | } 161 | } 162 | }, 163 | // Common Image Formats 164 | { 165 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 166 | use: 'url-loader' 167 | } 168 | ] 169 | }, 170 | 171 | optimization: { 172 | minimizer: process.env.E2E_BUILD 173 | ? [] 174 | : [ 175 | new TerserPlugin({ 176 | parallel: true, 177 | sourceMap: true, 178 | cache: true 179 | }), 180 | new OptimizeCSSAssetsPlugin({ 181 | cssProcessorOptions: { 182 | map: { 183 | inline: false, 184 | annotation: true 185 | } 186 | } 187 | }) 188 | ] 189 | }, 190 | 191 | plugins: [ 192 | /** 193 | * Create global constants which can be configured at compile time. 194 | * 195 | * Useful for allowing different behaviour between development builds and 196 | * release builds 197 | * 198 | * NODE_ENV should be production so that modules do not perform certain 199 | * development checks 200 | */ 201 | new webpack.EnvironmentPlugin({ 202 | NODE_ENV: 'production', 203 | PRESERVE_ELIGIBILITY_CONFIG: process.env.E2E_BUILD 204 | }), 205 | 206 | new MiniCssExtractPlugin({ 207 | filename: 'style.css' 208 | }), 209 | 210 | new BundleAnalyzerPlugin({ 211 | analyzerMode: 212 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 213 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 214 | }) 215 | ] 216 | }); 217 | -------------------------------------------------------------------------------- /test/components/__snapshots__/CountySelect.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CountySelect component should match exact snapshot 1`] = ` 4 |
5 |

8 | Choose your county 9 |

10 |
13 | 315 |
316 |
317 | `; 318 | --------------------------------------------------------------------------------