Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/react/issue-wizard/DetailsStep.test.tsx b/static_src/react/issue-wizard/DetailsStep.test.tsx
new file mode 100644
index 0000000..eaef0e7
--- /dev/null
+++ b/static_src/react/issue-wizard/DetailsStep.test.tsx
@@ -0,0 +1,34 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {render, cleanup} from '@testing-library/react';
+import {assert} from 'chai';
+
+import DetailsStep from './DetailsStep.tsx';
+
+describe('DetailsStep', () => {
+ afterEach(cleanup);
+
+ it('renders', async () => {
+ const {container} = render(<DetailsStep />);
+
+ // this is checking for the first question
+ const input = container.querySelector('input');
+ assert.isNotNull(input)
+
+ // this is checking for the rest
+ const count = document.querySelectorAll('textarea').length;
+ assert.equal(count, 3)
+ });
+
+ it('renders category in title', async () => {
+ const {container} = render(<DetailsStep category='UI'/>);
+
+ // this is checking the title contains our category
+ const title = container.querySelector('h2');
+ assert.include(title?.innerText, 'Details for problems with UI');
+ });
+
+});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/DetailsStep.tsx b/static_src/react/issue-wizard/DetailsStep.tsx
new file mode 100644
index 0000000..1a69cc1
--- /dev/null
+++ b/static_src/react/issue-wizard/DetailsStep.tsx
@@ -0,0 +1,65 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {createStyles, createTheme} from '@material-ui/core/styles';
+import {makeStyles} from '@material-ui/styles';
+import TextField from '@material-ui/core/TextField';
+import {red, grey} from '@material-ui/core/colors';
+
+/**
+ * The detail step is the second step on the dot
+ * stepper. This react component provides the users with
+ * specific questions about their bug to be filled out.
+ */
+const theme: Theme = createTheme();
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ width: '100%',
+ },
+ },
+ head: {
+ marginTop: '25px',
+ },
+ red: {
+ color: red[600],
+ },
+ grey: {
+ color: grey[600],
+ },
+ }), {defaultTheme: theme}
+);
+
+export default function DetailsStep({textValues, setTextValues, category}:
+ {textValues: Object, setTextValues: Function, category: string}): React.ReactElement {
+ const classes = useStyles();
+
+ const handleChange = (valueName: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
+ const textInput = e.target.value;
+ setTextValues({...textValues, [valueName]: textInput});
+ };
+
+ return (
+ <>
+ <h2 className={classes.grey}>Details for problems with {category}</h2>
+ <form className={classes.root} noValidate autoComplete="off">
+ <h3 className={classes.head}>Please enter a one line summary <span className={classes.red}>*</span></h3>
+ <TextField id="outlined-basic-1" variant="outlined" onChange={handleChange('oneLineSummary')}/>
+
+ <h3 className={classes.head}>Steps to reproduce problem <span className={classes.red}>*</span></h3>
+ <TextField multiline rows={4} id="outlined-basic-2" variant="outlined" onChange={handleChange('stepsToReproduce')}/>
+
+ <h3 className={classes.head}>Please describe the problem <span className={classes.red}>*</span></h3>
+ <TextField multiline rows={3} id="outlined-basic-3" variant="outlined" onChange={handleChange('describeProblem')}/>
+
+ <h3 className={classes.head}>Additional Comments</h3>
+ <TextField multiline rows={3} id="outlined-basic-4" variant="outlined" onChange={handleChange('additionalComments')}/>
+ </form>
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/DotMobileStepper.test.tsx b/static_src/react/issue-wizard/DotMobileStepper.test.tsx
new file mode 100644
index 0000000..5203110
--- /dev/null
+++ b/static_src/react/issue-wizard/DotMobileStepper.test.tsx
@@ -0,0 +1,59 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {render, screen, cleanup} from '@testing-library/react';
+import {assert} from 'chai';
+
+import DotMobileStepper from './DotMobileStepper.tsx';
+
+describe('DotMobileStepper', () => {
+ let container: HTMLElement;
+
+ afterEach(cleanup);
+
+ it('renders', () => {
+ container = render(<DotMobileStepper activeStep={0} nextEnabled={true}/>).container;
+
+ // this is checking the buttons for the stepper rendered
+ const count = document.querySelectorAll('button').length;
+ assert.equal(count, 2)
+ });
+
+ it('back button disabled on first step', () => {
+ render(<DotMobileStepper activeStep={0} nextEnabled={true}/>).container;
+
+ // Finds a button on the page with "back" as text using React testing library.
+ const backButton = screen.getByRole('button', {name: /backButton/i}) as HTMLButtonElement;
+
+ // Back button is disabled on the first step.
+ assert.isTrue(backButton.disabled);
+ });
+
+ it('both buttons enabled on second step', () => {
+ render(<DotMobileStepper activeStep={1} nextEnabled={true}/>).container;
+
+ // Finds a button on the page with "back" as text using React testing library.
+ const backButton = screen.getByRole('button', {name: /backButton/i}) as HTMLButtonElement;
+
+ // Finds a button on the page with "next" as text using React testing library.
+ const nextButton = screen.getByRole('button', {name: /nextButton/i}) as HTMLButtonElement;
+
+ // Back button is not disabled on the second step.
+ assert.isFalse(backButton.disabled);
+
+ // Next button is not disabled on the second step.
+ assert.isFalse(nextButton.disabled);
+ });
+
+ it('next button disabled on last step', () => {
+ render(<DotMobileStepper activeStep={2}/>).container;
+
+ // Finds a button on the page with "next" as text using React testing library.
+ const nextButton = screen.getByRole('button', {name: /nextButton/i}) as HTMLButtonElement;
+
+ // Next button is disabled on the second step.
+ assert.isTrue(nextButton.disabled);
+ });
+});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/DotMobileStepper.tsx b/static_src/react/issue-wizard/DotMobileStepper.tsx
new file mode 100644
index 0000000..9870f03
--- /dev/null
+++ b/static_src/react/issue-wizard/DotMobileStepper.tsx
@@ -0,0 +1,72 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {createTheme} from '@material-ui/core/styles';
+import {makeStyles} from '@material-ui/styles';
+import MobileStepper from '@material-ui/core/MobileStepper';
+import Button from '@material-ui/core/Button';
+import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
+import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
+
+const theme: Theme = createTheme();
+
+const useStyles = makeStyles({
+ root: {
+ width: '100%',
+ flexGrow: 1,
+ },
+}, {defaultTheme: theme});
+
+/**
+ * `<DotMobileStepper />`
+ *
+ * React component for rendering the linear dot stepper of the issue wizard.
+ *
+ * @return ReactElement.
+ */
+export default function DotsMobileStepper({nextEnabled, activeStep, setActiveStep} : {nextEnabled: boolean, activeStep: number, setActiveStep: Function}) : React.ReactElement {
+ const classes = useStyles();
+
+ const handleNext = () => {
+ setActiveStep((prevActiveStep: number) => prevActiveStep + 1);
+ };
+
+ const handleBack = () => {
+ setActiveStep((prevActiveStep: number) => prevActiveStep - 1);
+ };
+
+ let label;
+ let icon;
+
+ if (activeStep === 2){
+ label = 'Submit';
+ icon = '';
+ } else {
+ label = 'Next';
+ icon = <KeyboardArrowRight />;
+ }
+ return (
+ <MobileStepper
+ id="mobile-stepper"
+ variant="dots"
+ steps={3}
+ position="static"
+ activeStep={activeStep}
+ className={classes.root}
+ nextButton={
+ <Button aria-label="nextButton" size="medium" onClick={handleNext} disabled={activeStep === 2 || !nextEnabled}>
+ {label}
+ {icon}
+ </Button>
+ }
+ backButton={
+ <Button aria-label="backButton" size="medium" onClick={handleBack} disabled={activeStep === 0}>
+ <KeyboardArrowLeft />
+ Back
+ </Button>
+ }
+ />
+ );
+}
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/LandingStep.tsx b/static_src/react/issue-wizard/LandingStep.tsx
new file mode 100644
index 0000000..efe6491
--- /dev/null
+++ b/static_src/react/issue-wizard/LandingStep.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import {makeStyles, withStyles} from '@material-ui/styles';
+import {blue, yellow, red, grey} from '@material-ui/core/colors';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
+import Checkbox, {CheckboxProps} from '@material-ui/core/Checkbox';
+import SelectMenu from './SelectMenu.tsx';
+import RadioDescription from './RadioDescription.tsx';
+
+const CustomCheckbox = withStyles({
+ root: {
+ color: blue[400],
+ '&$checked': {
+ color: blue[600],
+ },
+ },
+ checked: {},
+})((props: CheckboxProps) => <Checkbox color="default" {...props} />);
+
+const useStyles = makeStyles({
+ pad: {
+ margin: '10px, 20px',
+ display: 'inline-block',
+ },
+ flex: {
+ display: 'flex',
+ },
+ inlineBlock: {
+ display: 'inline-block',
+ },
+ warningBox: {
+ minHeight: '10vh',
+ borderStyle: 'solid',
+ borderWidth: '2px',
+ borderColor: yellow[800],
+ borderRadius: '8px',
+ background: yellow[50],
+ padding: '0px 20px 1em',
+ margin: '30px 0px'
+ },
+ warningHeader: {
+ color: yellow[800],
+ fontSize: '16px',
+ fontWeight: '500',
+ },
+ star:{
+ color: red[700],
+ marginRight: '8px',
+ fontSize: '16px',
+ display: 'inline-block',
+ },
+ header: {
+ color: grey[900],
+ fontSize: '28px',
+ marginTop: '6vh',
+ },
+ subheader: {
+ color: grey[700],
+ fontSize: '18px',
+ lineHeight: '32px',
+ },
+ red: {
+ color: red[600],
+ },
+});
+
+export default function LandingStep({checkExisting, setCheckExisting, userType, setUserType, category, setCategory}:
+ {checkExisting: boolean, setCheckExisting: Function, userType: string, setUserType: Function, category: string, setCategory: Function}) {
+ const classes = useStyles();
+
+ const handleCheckChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ setCheckExisting(event.target.checked);
+ };
+
+ return (
+ <>
+ <p className={classes.header}>Report an issue with Chromium</p>
+ <p className={classes.subheader}>
+ We want you to enter the best possible issue report so that the project team members
+ can act on it effectively. The following steps will help route your issue to the correct
+ people.
+ </p>
+ <p className={classes.subheader}>
+ Please select your following role: <span className={classes.red}>*</span>
+ </p>
+ <RadioDescription value={userType} setValue={setUserType}/>
+ <div className={classes.subheader}>
+ Which of the following best describes the issue that you are reporting? <span className={classes.red}>*</span>
+ </div>
+ <SelectMenu option={category} setOption={setCategory}/>
+ <div className={classes.warningBox}>
+ <p className={classes.warningHeader}> Avoid duplicate issue reports:</p>
+ <div>
+ <div className={classes.star}>*</div>
+ <FormControlLabel className={classes.pad}
+ control={
+ <CustomCheckbox
+ checked={checkExisting}
+ onChange={handleCheckChange}
+ name="warningCheck"
+ />
+ }
+ label="By checking this box, I'm acknowledging that I have searched for existing issues that already report this problem."
+ />
+ </div>
+ </div>
+ </>
+ );
+}
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription.test.tsx b/static_src/react/issue-wizard/RadioDescription.test.tsx
new file mode 100644
index 0000000..ff65eae
--- /dev/null
+++ b/static_src/react/issue-wizard/RadioDescription.test.tsx
@@ -0,0 +1,54 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {render, screen, cleanup} from '@testing-library/react';
+import userEvent from '@testing-library/user-event'
+import {assert} from 'chai';
+import sinon from 'sinon';
+
+import RadioDescription from './RadioDescription.tsx';
+
+describe('RadioDescription', () => {
+ afterEach(cleanup);
+
+ it('renders', () => {
+ render(<RadioDescription />);
+ // look for blue radios
+ const radioOne = screen.getByRole('radio', {name: /Web Developer/i});
+ assert.isNotNull(radioOne)
+
+ const radioTwo = screen.getByRole('radio', {name: /End User/i});
+ assert.isNotNull(radioTwo)
+
+ const radioThree = screen.getByRole('radio', {name: /Chromium Contributor/i});
+ assert.isNotNull(radioThree)
+ });
+
+ it('checks selected radio value', () => {
+ // We're passing in the "Web Developer" value here manually
+ // to tell our code that that radio button is selected.
+ render(<RadioDescription value={'Web Developer'} />);
+
+ const checkedRadio = screen.getByRole('radio', {name: /Web Developer/i});
+ assert.isTrue(checkedRadio.checked);
+
+ // Extra check to make sure we haven't checked every single radio button.
+ const uncheckedRadio = screen.getByRole('radio', {name: /End User/i});
+ assert.isFalse(uncheckedRadio.checked);
+ });
+
+ it('sets radio value when clicked', () => {
+ // Using the sinon.js testing library to create a function for testing.
+ const setValue = sinon.stub();
+
+ render(<RadioDescription setValue={setValue} />);
+
+ const radio = screen.getByRole('radio', {name: /Web Developer/i});
+ userEvent.click(radio);
+
+ // Asserts that "Web Developer" was passed into our "setValue" function.
+ sinon.assert.calledWith(setValue, 'Web Developer');
+ });
+});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription.tsx b/static_src/react/issue-wizard/RadioDescription.tsx
new file mode 100644
index 0000000..ad78c78
--- /dev/null
+++ b/static_src/react/issue-wizard/RadioDescription.tsx
@@ -0,0 +1,117 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {makeStyles, withStyles} from '@material-ui/styles';
+import {blue, grey} from '@material-ui/core/colors';
+import Radio, {RadioProps} from '@material-ui/core/Radio';
+
+const userGroups = Object.freeze({
+ END_USER: 'End User',
+ WEB_DEVELOPER: 'Web Developer',
+ CONTRIBUTOR: 'Chromium Contributor',
+});
+
+const BlueRadio = withStyles({
+ root: {
+ color: blue[400],
+ '&$checked': {
+ color: blue[600],
+ },
+ },
+ checked: {},
+})((props: RadioProps) => <Radio color="default" {...props} />);
+
+const useStyles = makeStyles({
+ flex: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ },
+ container: {
+ width: '320px',
+ height: '150px',
+ position: 'relative',
+ display: 'inline-block',
+ },
+ text: {
+ position: 'absolute',
+ display: 'inline-block',
+ left: '55px',
+ },
+ title: {
+ marginTop: '7px',
+ fontSize: '20px',
+ color: grey[900],
+ },
+ subheader: {
+ fontSize: '16px',
+ color: grey[800],
+ },
+ line: {
+ position: 'absolute',
+ bottom: 0,
+ width: '300px',
+ left: '20px',
+ }
+});
+
+/**
+ * `<RadioDescription />`
+ *
+ * React component for radio buttons and their descriptions
+ * on the landing step of the Issue Wizard.
+ *
+ * @return ReactElement.
+ */
+export default function RadioDescription({value, setValue} : {value: string, setValue: Function}): React.ReactElement {
+ const classes = useStyles();
+
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ setValue(event.target.value);
+ };
+
+ return (
+ <div className={classes.flex}>
+ <div className={classes.container}>
+ <BlueRadio
+ checked={value === userGroups.END_USER}
+ onChange={handleChange}
+ value={userGroups.END_USER}
+ inputProps={{ 'aria-label': userGroups.END_USER}}
+ />
+ <div className={classes.text}>
+ <p className={classes.title}>{userGroups.END_USER}</p>
+ <p className={classes.subheader}>I am a user trying to do something on a website.</p>
+ </div>
+ <hr color={grey[200]} className={classes.line}/>
+ </div>
+ <div className={classes.container}>
+ <BlueRadio
+ checked={value === userGroups.WEB_DEVELOPER}
+ onChange={handleChange}
+ value={userGroups.WEB_DEVELOPER}
+ inputProps={{ 'aria-label': userGroups.WEB_DEVELOPER }}
+ />
+ <div className={classes.text}>
+ <p className={classes.title}>{userGroups.WEB_DEVELOPER}</p>
+ <p className={classes.subheader}>I am a web developer trying to build something.</p>
+ </div>
+ <hr color={grey[200]} className={classes.line}/>
+ </div>
+ <div className={classes.container}>
+ <BlueRadio
+ checked={value === userGroups.CONTRIBUTOR}
+ onChange={handleChange}
+ value={userGroups.CONTRIBUTOR}
+ inputProps={{ 'aria-label': userGroups.CONTRIBUTOR }}
+ />
+ <div className={classes.text}>
+ <p className={classes.title}>{userGroups.CONTRIBUTOR}</p>
+ <p className={classes.subheader}>I know about a problem in specific tests or code.</p>
+ </div>
+ <hr color={grey[200]} className={classes.line}/>
+ </div>
+ </div>
+ );
+ }
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/SelectMenu.test.tsx b/static_src/react/issue-wizard/SelectMenu.test.tsx
new file mode 100644
index 0000000..13efef6
--- /dev/null
+++ b/static_src/react/issue-wizard/SelectMenu.test.tsx
@@ -0,0 +1,38 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {render} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import {screen} from '@testing-library/dom';
+import {assert} from 'chai';
+
+import SelectMenu from './SelectMenu.tsx';
+
+describe('SelectMenu', () => {
+ let container: React.RenderResult;
+
+ beforeEach(() => {
+ container = render(<SelectMenu />).container;
+ });
+
+ it('renders', () => {
+ const form = container.querySelector('form');
+ assert.isNotNull(form)
+ });
+
+ it('renders options on click', () => {
+ const input = document.getElementById('outlined-select-category');
+ if (!input) {
+ throw new Error('Input is undefined');
+ }
+
+ userEvent.click(input)
+
+ // 14 is the current number of options in the select menu
+ const count = screen.getAllByTestId('select-menu-item').length;
+
+ assert.equal(count, 14);
+ });
+});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/SelectMenu.tsx b/static_src/react/issue-wizard/SelectMenu.tsx
new file mode 100644
index 0000000..3b0b96d
--- /dev/null
+++ b/static_src/react/issue-wizard/SelectMenu.tsx
@@ -0,0 +1,133 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import React from 'react';
+import {createTheme, Theme} from '@material-ui/core/styles';
+import {makeStyles} from '@material-ui/styles';
+import MenuItem from '@material-ui/core/MenuItem';
+import TextField from '@material-ui/core/TextField';
+
+const CATEGORIES = [
+ {
+ value: 'UI',
+ label: 'UI',
+ },
+ {
+ value: 'Accessibility',
+ label: 'Accessibility',
+ },
+ {
+ value: 'Network/Downloading',
+ label: 'Network/Downloading',
+ },
+ {
+ value: 'Audio/Video',
+ label: 'Audio/Video',
+ },
+ {
+ value: 'Content',
+ label: 'Content',
+ },
+ {
+ value: 'Apps',
+ label: 'Apps',
+ },
+ {
+ value: 'Extensions/Themes',
+ label: 'Extensions/Themes',
+ },
+ {
+ value: 'Webstore',
+ label: 'Webstore',
+ },
+ {
+ value: 'Sync',
+ label: 'Sync',
+ },
+ {
+ value: 'Enterprise',
+ label: 'Enterprise',
+ },
+ {
+ value: 'Installation',
+ label: 'Installation',
+ },
+ {
+ value: 'Crashes',
+ label: 'Crashes',
+ },
+ {
+ value: 'Security',
+ label: 'Security',
+ },
+ {
+ value: 'Other',
+ label: 'Other',
+ },
+];
+
+const theme: Theme = createTheme();
+
+const useStyles = makeStyles((theme: Theme) => ({
+ container: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ maxWidth: '65%',
+ },
+ textField: {
+ marginLeft: theme.spacing(1),
+ marginRight: theme.spacing(1),
+ },
+ menu: {
+ width: '100%',
+ minWidth: '300px',
+ },
+}), {defaultTheme: theme});
+
+/**
+ * Select menu component that is located on the landing step if the
+ * Issue Wizard. The menu is used for the user to indicate the category
+ * of their bug when filing an issue.
+ *
+ * @return ReactElement.
+ */
+export default function SelectMenu({option, setOption}: {option: string, setOption: Function}) {
+ const classes = useStyles();
+ const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+ setOption(event.target.value as string);
+ };
+
+ return (
+ <form className={classes.container} noValidate autoComplete="off">
+ <TextField
+ id="outlined-select-category"
+ select
+ label=''
+ className={classes.textField}
+ value={option}
+ onChange={handleChange}
+ InputLabelProps={{shrink: false}}
+ SelectProps={{
+ MenuProps: {
+ className: classes.menu,
+ },
+ }}
+ margin="normal"
+ variant="outlined"
+ fullWidth={true}
+ >
+ {CATEGORIES.map(option => (
+ <MenuItem
+ className={classes.menu}
+ key={option.value}
+ value={option.value}
+ data-testid="select-menu-item"
+ >
+ {option.label}
+ </MenuItem>
+ ))}
+ </TextField>
+ </form>
+ );
+}
\ No newline at end of file