Merge branch 'main' into avm99963-monorail
Merged commit 4137ed7879acadbf891e8c471108acb874dae886.
GitOrigin-RevId: b6100ffc5b1da355a35f37b13fcaaf746ee8b307
diff --git a/static_src/react/IssueWizard.css b/static_src/react/IssueWizard.css
index 46b1ff2..a19059b 100644
--- a/static_src/react/IssueWizard.css
+++ b/static_src/react/IssueWizard.css
@@ -1,6 +1,7 @@
.container {
margin-left: 50px;
max-width: 70vw;
+ min-width: 300px;
width: 100%;
font-family: 'Poppins', serif;
}
@@ -15,4 +16,10 @@
.poppins {
font-family: 'Poppins', serif;
-}
\ No newline at end of file
+}
+
+.feedback {
+ text-align: right;
+ padding-top: 1.5em;
+ color: #888;
+}
diff --git a/static_src/react/IssueWizard.test.tsx b/static_src/react/IssueWizard.test.tsx
index 07016ce..7584ff6 100644
--- a/static_src/react/IssueWizard.test.tsx
+++ b/static_src/react/IssueWizard.test.tsx
@@ -10,7 +10,7 @@
describe('IssueWizard', () => {
it('renders', async () => {
- render(<IssueWizard />);
+ render(<IssueWizard loginUrl="login" userDisplayName="user"/>);
const stepper = document.getElementById("mobile-stepper")
diff --git a/static_src/react/IssueWizard.tsx b/static_src/react/IssueWizard.tsx
index de5e8fb..ecb6664 100644
--- a/static_src/react/IssueWizard.tsx
+++ b/static_src/react/IssueWizard.tsx
@@ -6,62 +6,162 @@
import * as React from 'react'
import ReactDOM from 'react-dom';
import styles from './IssueWizard.css';
-import DotMobileStepper from './issue-wizard/DotMobileStepper.tsx';
import LandingStep from './issue-wizard/LandingStep.tsx';
import DetailsStep from './issue-wizard/DetailsStep.tsx'
+import {IssueWizardPersona} from './issue-wizard/IssueWizardTypes.tsx';
+import CustomQuestionsStep from './issue-wizard/CustomQuestionsStep.tsx';
+import {getOs, getChromeVersion, buildIssueDescription} from './issue-wizard/IssueWizardUtils.tsx'
+import Header from './issue-wizard/Header.tsx'
+
+import {GetQuestionsByCategory, buildIssueLabels, getCompValByCategory, getLabelsByCategory} from './issue-wizard/IssueWizardUtils.tsx';
+import {ISSUE_WIZARD_QUESTIONS, ISSUE_REPRODUCE_PLACEHOLDER, OS_CHANNEL_LIST} from './issue-wizard/IssueWizardConfig.ts';
+import {prpcClient} from 'prpc-client-instance.js';
+import {expandDescriptions} from './issue-wizard/IssueWizardDescriptionsUtils.tsx';
+import SubmitSuccessStep from './issue-wizard/SubmitSuccessStep.tsx';
+import {IssueWizardFeedback} from './issue-wizard/IssueWizardFeedback.tsx';
/**
* Base component for the issue filing wizard, wrapper for other components.
* @return Issue wizard JSX.
*/
-export function IssueWizard(): ReactElement {
- const [checkExisting, setCheckExisting] = React.useState(false);
- const [userType, setUserType] = React.useState('End User');
+ type Props = {
+ loginUrl: string,
+ userDisplayName: string,
+}
+export function IssueWizard(props: Props): ReactElement {
+ const {loginUrl, userDisplayName} = props;
+ React.useEffect(() => {
+ if(!userDisplayName) {
+ window.location.href = loginUrl;
+ }
+ },[loginUrl, userDisplayName]);
+
+ const [userPersona, setUserPersona] = React.useState(IssueWizardPersona.EndUser);
const [activeStep, setActiveStep] = React.useState(0);
const [category, setCategory] = React.useState('');
+ const [newIssueID, setnewIssueID] = React.useState('');
+ const [isRegression, setIsRegression] = React.useState(false);
const [textValues, setTextValues] = React.useState(
{
oneLineSummary: '',
- stepsToReproduce: '',
+ stepsToReproduce: ISSUE_REPRODUCE_PLACEHOLDER,
describeProblem: '',
- additionalComments: ''
+ chromeVersion: getChromeVersion(),
+ osName: getOs(),
+ channel: OS_CHANNEL_LIST[0].name,
});
+ const [enableFeedback, setEnableFeedback] = React.useState(false);
+ const questionByCategory = GetQuestionsByCategory(ISSUE_WIZARD_QUESTIONS);
- let nextEnabled;
+ const moveStep = (step: number) => {
+ window.scrollTo(0, 0);
+ setActiveStep(step);
+ }
+
+ const reset = () => {
+ setTextValues({
+ oneLineSummary: '',
+ stepsToReproduce: ISSUE_REPRODUCE_PLACEHOLDER,
+ describeProblem: '',
+ chromeVersion: getChromeVersion(),
+ osName: getOs(),
+ channel: OS_CHANNEL_LIST[0].name,
+ });
+ setIsRegression(false);
+ }
+
+ const updateCategory = (category: string) => {
+ setCategory(category);
+ reset();
+ }
+
let page;
- if (activeStep === 0){
+ if (activeStep === 0) {
page = <LandingStep
- checkExisting={checkExisting}
- setCheckExisting={setCheckExisting}
- userType={userType}
- setUserType={setUserType}
+ userPersona={userPersona}
+ setUserPersona={setUserPersona}
category={category}
- setCategory={setCategory}
+ setCategory={updateCategory}
+ setActiveStep={moveStep}
/>;
- nextEnabled = checkExisting && userType && (category != '');
- } else if (activeStep === 1){
- page = <DetailsStep textValues={textValues} setTextValues={setTextValues} category={category}/>;
- nextEnabled = (textValues.oneLineSummary.trim() !== '') &&
- (textValues.stepsToReproduce.trim() !== '') &&
- (textValues.describeProblem.trim() !== '');
+ } else if (activeStep === 1) {
+ page = <DetailsStep
+ textValues={textValues}
+ setTextValues={setTextValues}
+ category={category}
+ setActiveStep={moveStep}
+ setIsRegression={setIsRegression}
+ />;
+ } else if (activeStep === 2) {
+ const compValByCategory = getCompValByCategory(ISSUE_WIZARD_QUESTIONS);
+ const labelsByCategory = getLabelsByCategory(ISSUE_WIZARD_QUESTIONS);
+
+ const onSubmitIssue = (comments: string, customQuestionsAnswers: Array<string>, attachments: Array<any>,onSuccess: Function, onFailure: Function) => {
+ const summary = textValues.oneLineSummary;
+ const component = compValByCategory.get(category);
+ const description = buildIssueDescription(
+ textValues.stepsToReproduce,
+ textValues.describeProblem,
+ comments, textValues.osName,
+ textValues.chromeVersion,
+ textValues.channel);
+ const labels = buildIssueLabels(category, textValues.osName, textValues.chromeVersion, labelsByCategory.get(category));
+
+ const {expandDescription, expandLabels, compVal} =
+ expandDescriptions(category, customQuestionsAnswers, isRegression, description, labels, component);
+
+ const componentsArray = [];
+ if (compVal.length > 0) {
+ componentsArray.push({
+ component: 'projects/chromium/componentDefs/' + compVal
+ })
+ }
+
+ const response = prpcClient.call('monorail.v3.Issues', 'MakeIssue', {
+ parent: 'projects/chromium',
+ issue: {
+ summary,
+ status: {
+ status: 'Untriaged',
+ },
+ components: componentsArray,
+ labels: expandLabels,
+ },
+ description: expandDescription,
+ uploads: attachments,
+ });
+ response.then(onSuccess, onFailure);
+ }
+ page =
+ <CustomQuestionsStep
+ setActiveStep={moveStep}
+ questions={questionByCategory.get(category)}
+ onSubmit={onSubmitIssue}
+ setnewIssueID={setnewIssueID}
+ />;
+ } else if (activeStep === 3) {
+ page = <SubmitSuccessStep issueID={newIssueID}/>;
}
return (
<>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins"></link>
<div className={styles.container}>
+ <Header />
{page}
- <DotMobileStepper nextEnabled={nextEnabled} activeStep={activeStep} setActiveStep={setActiveStep}/>
+ <div className={styles.feedback} onClick={()=>{setEnableFeedback(true);}}>Report a problem with this wizard</div>
</div>
+ <IssueWizardFeedback enable={enableFeedback} setEnable={setEnableFeedback}/>
</>
);
}
/**
* Renders the issue filing wizard page.
- * @param mount HTMLElement that the React component should be
- * added to.
+ * @param mount HTMLElement that the React component should be added to.
+ * @param loginUrl redirect to login page
+ * @param userDisplayName login user
*/
-export function renderWizard(mount: HTMLElement): void {
- ReactDOM.render(<IssueWizard />, mount);
+export function renderWizard(mount: HTMLElement, loginUrl: string, userDisplayName: string): void {
+ ReactDOM.render(<IssueWizard loginUrl={loginUrl} userDisplayName={userDisplayName}/>, mount);
}
diff --git a/static_src/react/ReactAutocomplete.test.tsx b/static_src/react/ReactAutocomplete.test.tsx
index a1e7c62..3253892 100644
--- a/static_src/react/ReactAutocomplete.test.tsx
+++ b/static_src/react/ReactAutocomplete.test.tsx
@@ -19,7 +19,7 @@
fireEvent.keyDown(input, {key: 'Enter', code: 'Enter'});
};
-xdescribe('ReactAutocomplete', () => {
+describe.skip('ReactAutocomplete', () => {
it('renders', async () => {
const {container} = render(<ReactAutocomplete label="cool" options={[]} />);
@@ -141,6 +141,27 @@
assert.strictEqual(input?.value, 'foobar');
});
+ it('filterOptions matching prefix first', async () => {
+ const options = [`a_test`, `test`];
+
+ const {container} = render(<ReactAutocomplete
+ label="cool"
+ options={options}
+ />);
+
+ const input = container.querySelector('input');
+ assert.isNotNull(input);
+ if (!input) return;
+
+ userEvent.type(input, 'tes');
+
+ const results = document.querySelectorAll('.autocomplete-option');
+
+ fireEvent.keyDown(input, {key: 'Enter', code: 'Enter'});
+
+ assert.strictEqual(input?.value, 'test');
+ });
+
it('onChange callback is called', async () => {
const onChangeStub = sinon.stub();
diff --git a/static_src/react/ReactAutocomplete.tsx b/static_src/react/ReactAutocomplete.tsx
index 27fdc32..60284a8 100644
--- a/static_src/react/ReactAutocomplete.tsx
+++ b/static_src/react/ReactAutocomplete.tsx
@@ -86,12 +86,20 @@
if (!inputValue.length) {
return [];
}
+ const prefixMatch = (option: T) => {
+ const label = getOptionLabel(option);
+ return label.substring(0, inputValue.length).toLowerCase() === inputValue.toLowerCase();
+ }
+ const prefixMatchOptions = options.filter(prefixMatch);
+
+ const prefixMatchOptionsSet = new Set(prefixMatchOptions);
const regex = _matchRegex(inputValue);
const predicate = (option: T) => {
- return getOptionLabel(option).match(regex) ||
- getOptionDescription(option).match(regex);
+ return !prefixMatchOptionsSet.has(option) && (getOptionLabel(option).match(regex) ||
+ getOptionDescription(option).match(regex));
}
- return options.filter(predicate).slice(0, MAX_AUTOCOMPLETE_OPTIONS);
+ const matchOptions = options.filter(predicate);
+ return [...prefixMatchOptions, ...matchOptions].slice(0, MAX_AUTOCOMPLETE_OPTIONS);
}
}
diff --git a/static_src/react/issue-wizard/AttachmentUploader.css b/static_src/react/issue-wizard/AttachmentUploader.css
new file mode 100644
index 0000000..1a4883e
--- /dev/null
+++ b/static_src/react/issue-wizard/AttachmentUploader.css
@@ -0,0 +1,52 @@
+.materialIcons {
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 20px;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-feature-settings: 'liga';
+ -webkit-font-smoothing: 'antialiased';
+
+}
+
+.button {
+ margin-right: 8px;
+ padding: 0.1em 4px;
+ display: inline-flex;
+ width: auto;
+ cursor: pointer;
+ border: var(--chops-normal-border);
+ margin-left: 0;
+}
+
+.controls:focus-within > label{
+ border: 2px solid #1976d2;
+}
+
+.controls {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ width: 100%;
+ font-size: 12px;
+}
+
+.inputUpload {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ position: absolute;
+ top: -9999;
+ left: -9999;
+}
+
+.error {
+ color: red;
+}
diff --git a/static_src/react/issue-wizard/AttachmentUploader.tsx b/static_src/react/issue-wizard/AttachmentUploader.tsx
new file mode 100644
index 0000000..a207ef5
--- /dev/null
+++ b/static_src/react/issue-wizard/AttachmentUploader.tsx
@@ -0,0 +1,87 @@
+// 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 Button from '@material-ui/core/Button';
+import styles from './AttachmentUploader.css';
+
+type Props = {
+ files: Array<File>,
+ setFiles: Function,
+ setSubmitEnable: Function,
+}
+
+const isSameFile = (a: File, b: File) => {
+ // NOTE: This function could return a false positive if two files have the
+ // exact same name, lastModified time, size, and type but different
+ // content. This is extremely unlikely, however.
+ return a.name === b.name && a.lastModified === b.lastModified &&
+ a.size === b.size && a.type === b.type;
+}
+
+const getTotalSize = (files: Array<File>) => {
+ let size = 0;
+ files.forEach((f) => {
+ size += f.size;
+ });
+ return size;
+}
+
+const MAX_SIZE = 10 * 1000 * 1000;
+export default function AttachmentUploader(props: Props): React.ReactElement {
+
+ const {files, setFiles, setSubmitEnable} = props;
+ const [isOverSize, setIsOverSize] = React.useState(false);
+
+ const onSelectFile = (event: {currentTarget: any;}) => {
+ const input = event.currentTarget;
+ if (!input.files || input.files.length === 0) {
+ return;
+ }
+
+ const newFiles = [...input.files].filter((f1) => {
+ const fileExist = files.some((f2) => isSameFile(f1, f2));
+ return !fileExist;
+ })
+
+ const expendFiles = [...files].concat(newFiles);
+ const filesSize = getTotalSize(expendFiles);
+ setIsOverSize(filesSize > MAX_SIZE);
+ setSubmitEnable(filesSize <= MAX_SIZE);
+ setFiles(expendFiles);
+ }
+
+ const onRemoveFile = (index: number) => () => {
+ let remainingFiles = [...files];
+ remainingFiles.splice(index, 1);
+ const filesSize = getTotalSize(remainingFiles);
+ setIsOverSize(filesSize > MAX_SIZE);
+ setSubmitEnable(filesSize <= MAX_SIZE);
+ setFiles(remainingFiles);
+ }
+ return (
+ <>
+ <div className={styles.controls}>
+ <input className={styles.inputUpload} id="file-uploader" type="file" multiple onChange={onSelectFile}/>
+ <label className={styles.button} for="file-uploader">
+ <i className={styles.materialIcons} role="presentation">attach_file</i>Add attachments
+ </label>
+ You can include multiple attachments (Max: 10.0 MB per issue)
+ </div>
+ {files.length === 0 ? null :
+ (<ul>
+ {
+ files?.map((f, i) => (
+ <li>
+ {f.name}
+ <Button onClick={onRemoveFile(i)}> X</Button>
+ </li>
+ ))
+ }
+ </ul>)
+ }
+ {isOverSize ? <div className={styles.error}>Warning: Attachments are too big !</div> : null}
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/ConfirmBackModal.tsx b/static_src/react/issue-wizard/ConfirmBackModal.tsx
new file mode 100644
index 0000000..0d07b83
--- /dev/null
+++ b/static_src/react/issue-wizard/ConfirmBackModal.tsx
@@ -0,0 +1,51 @@
+// Copyright 2022 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 * as React from 'react';
+import {makeStyles} from '@material-ui/styles';
+import Dialog from '@material-ui/core/Dialog';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogContentText from '@material-ui/core/DialogContentText';
+import DialogActions from '@material-ui/core/DialogActions';
+import Button from '@material-ui/core/Button';
+
+const userStyles = makeStyles({
+ actionsButtons: {
+ paddingTop: '0',
+ },
+ primaryButton: {
+ backgroundColor: 'rgb(25, 118, 210)',
+ color: 'white',
+ }
+});
+
+type Props = {
+ enable: boolean,
+ setEnable: Function,
+ confirmBack: Function,
+}
+
+export function ConfirmBackModal(props: Props): React.ReactElement {
+ const {enable, setEnable, confirmBack} = props;
+ const classes = userStyles();
+
+ return (
+ <Dialog open={enable}>
+ <DialogTitle>Warning!</DialogTitle>
+ <DialogContent>
+ <DialogContentText>
+ Changes you made on this page won't be saved.
+ </DialogContentText>
+ </DialogContent>
+ <DialogActions className={classes.actionsButtons}>
+ <Button onClick={() => setEnable(false)}>Cancel</Button>
+ <Button onClick={() => {
+ confirmBack();
+ setEnable(false);
+ }} className={classes.primaryButton}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+ )
+}
diff --git a/static_src/react/issue-wizard/CustomQuestions/CustomQuestionInput.tsx b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionInput.tsx
new file mode 100644
index 0000000..aa7fdd0
--- /dev/null
+++ b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionInput.tsx
@@ -0,0 +1,48 @@
+// 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 OutlinedInput from "@material-ui/core/OutlinedInput";
+import {makeStyles} from '@material-ui/styles';
+
+const userStyles = makeStyles({
+ head: {
+ marginTop: '1.5rem',
+ fontSize: '1rem'
+ },
+ inputArea: {
+ width: '100%',
+ },
+});
+
+type Props = {
+ question: string,
+ updateAnswers: Function,
+}
+
+export default function CustomQuestionInput(props: Props): React.ReactElement {
+
+ const classes = userStyles();
+
+ const {question, updateAnswers} = props;
+ const [answer, setAnswer] = React.useState('');
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setAnswer(e.target.value);
+ updateAnswers(e.target.value);
+ };
+ const getInnerHtml = ()=> {
+ return {__html: question};
+ }
+ return (
+ <>
+ <h3 dangerouslySetInnerHTML={getInnerHtml()} className={classes.head}/>
+ <OutlinedInput
+ value={answer}
+ onChange={handleChange}
+ className={classes.inputArea}
+ inputProps={{ maxLength: 1000 }}
+ />
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/CustomQuestions/CustomQuestionSelector.tsx b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionSelector.tsx
new file mode 100644
index 0000000..bb855a3
--- /dev/null
+++ b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionSelector.tsx
@@ -0,0 +1,105 @@
+// 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} from '@material-ui/styles';
+import SelectMenu from '../SelectMenu.tsx';
+import {CustomQuestion, CustomQuestionType} from '../IssueWizardTypes.tsx';
+import CustomQuestionInput from './CustomQuestionInput.tsx';
+import CustomQuestionTextarea from './CustomQuestionTextarea.tsx';
+import {GetSelectMenuOptions} from '../IssueWizardUtils.tsx';
+
+const userStyles = makeStyles({
+ head: {
+ marginTop: '1.5rem',
+ fontSize: '1rem'
+ },
+ inputArea: {
+ width: '100%',
+ },
+ tip: {
+ margin: '0.5rem 0',
+ },
+});
+
+type Props = {
+ question: string,
+ tip?: string,
+ options: string[],
+ subQuestions: CustomQuestion[] | null,
+ updateAnswers: Function,
+}
+
+export default function CustomQuestionSelector(props: Props): React.ReactElement {
+
+ const classes = userStyles();
+
+ const {question, updateAnswers, options, subQuestions, tip} = props;
+ const [selectedOption, setSelectedOption] = React.useState(options[0]);
+
+ const [subQuestion, setSubQuestion] = React.useState(subQuestions? subQuestions[0] : null);
+
+ React.useEffect(() => {
+ updateAnswers(options[0]);
+ },[]);
+
+ const handleOptionChange = (option: string) => {
+ setSelectedOption(option);
+ updateAnswers(option);
+ const index = options.indexOf(option);
+ if (subQuestions !== null) {
+ setSubQuestion(subQuestions[index]);
+ }
+ };
+
+ const updateSubQuestionAnswer = (answer:string) => {
+ const updatedAnswer = selectedOption + ' ' + answer;
+ updateAnswers(updatedAnswer);
+ }
+ const optionList = GetSelectMenuOptions(options);
+
+ let renderSubQuestion = null;
+
+ if (subQuestion != null) {
+ switch(subQuestion.type) {
+ case CustomQuestionType.Input:
+ renderSubQuestion =
+ <CustomQuestionInput
+ question={subQuestion.question}
+ updateAnswers={updateSubQuestionAnswer}
+ />
+ break;
+ case CustomQuestionType.Text:
+ renderSubQuestion =
+ <CustomQuestionTextarea
+ question={subQuestion.question}
+ tip={subQuestion.tip}
+ updateAnswers={updateSubQuestionAnswer}
+ />;
+ break;
+ default:
+ break;
+ }
+ }
+
+ const getQuestionInnerHtml = () => {
+ return {__html: question};
+ }
+
+ const getTipInnerHtml = () => {
+ return {__html: tip};
+ }
+ return (
+ <>
+ <h3 dangerouslySetInnerHTML={getQuestionInnerHtml()} className={classes.head}/>
+ {tip? <div dangerouslySetInnerHTML={getTipInnerHtml()} className={classes.tip}/> : null}
+ <SelectMenu
+ optionsList={optionList}
+ selectedOption={selectedOption}
+ setOption={handleOptionChange}
+ />
+ {renderSubQuestion}
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/CustomQuestions/CustomQuestionTextarea.tsx b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionTextarea.tsx
new file mode 100644
index 0000000..fdbdf1f
--- /dev/null
+++ b/static_src/react/issue-wizard/CustomQuestions/CustomQuestionTextarea.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 OutlinedInput from "@material-ui/core/OutlinedInput";
+import {makeStyles} from '@material-ui/styles';
+
+const userStyles = makeStyles({
+ head: {
+ marginTop: '1.5rem',
+ fontSize: '1rem'
+ },
+ inputArea: {
+ width: '100%',
+ },
+ tip: {
+ margin: '0.5rem 0'
+ },
+});
+
+type Props = {
+ question: string,
+ tip?: string,
+ updateAnswers: Function,
+}
+
+export default function CustomQuestionTextarea(props: Props): React.ReactElement {
+
+ const classes = userStyles();
+
+ const {question, updateAnswers, tip} = props;
+ const [answer, setAnswer] = React.useState('');
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setAnswer(e.target.value);
+ updateAnswers(e.target.value);
+ };
+
+ const getQuestionInnerHtml = ()=> {
+ return {__html: question};
+ }
+
+ const getTipInnerHtml = ()=> {
+ return {__html: tip};
+ }
+ return (
+ <>
+ <h3 dangerouslySetInnerHTML={getQuestionInnerHtml()} className={classes.head}/>
+ {tip? <div dangerouslySetInnerHTML={getTipInnerHtml()} className={classes.tip}/> : null}
+ <OutlinedInput
+ multiline={true}
+ rows={3}
+ value={answer}
+ onChange={handleChange}
+ className={classes.inputArea}
+ />
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/CustomQuestionsStep.tsx b/static_src/react/issue-wizard/CustomQuestionsStep.tsx
new file mode 100644
index 0000000..9e1ef72
--- /dev/null
+++ b/static_src/react/issue-wizard/CustomQuestionsStep.tsx
@@ -0,0 +1,186 @@
+// 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} from '@material-ui/styles';
+import {grey} from '@material-ui/core/colors';
+import DotMobileStepper from './DotMobileStepper.tsx';
+import {CustomQuestion, CustomQuestionType} from './IssueWizardTypes.tsx';
+import CustomQuestionInput from './CustomQuestions/CustomQuestionInput.tsx';
+import CustomQuestionTextarea from './CustomQuestions/CustomQuestionTextarea.tsx';
+import CustomQuestionSelector from './CustomQuestions/CustomQuestionSelector.tsx';
+import Alert from '@material-ui/core/Alert';
+import AttachmentUploader from './AttachmentUploader.tsx';
+import Modal from '@material-ui/core/Modal';
+import Box from '@material-ui/core/Box';
+import {LABELS_PREFIX} from './IssueWizardConfig.ts';
+
+const userStyles = makeStyles({
+ greyText: {
+ color: grey[600],
+ },
+ root: {
+ width: '100%',
+ },
+ modalBox: {
+ position: 'absolute',
+ top: '40%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ width: 400,
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ padding: '10px',
+ },
+ modalTitle: {
+ fontSize: '20px',
+ margin: '5px 0px',
+ },
+ modalContext: {
+ fontSize: '15px',
+ },
+});
+
+type Props = {
+ setActiveStep: Function,
+ questions: CustomQuestion[],
+ onSubmit: Function,
+ setnewIssueID: Function,
+};
+
+export default function CustomQuestionsStep(props: Props): React.ReactElement {
+
+ const {setActiveStep, questions, onSubmit, setnewIssueID} = props;
+ const classes = userStyles();
+
+ const customQuestions = new Array();
+
+ const [additionalComments, setAdditionalComments] = React.useState('');
+ const [attachments, setAttachments] = React.useState([]);
+ const [answers, setAnswers] = React.useState(Array(questions.length).fill(''));
+ const [hasError, setHasError] = React.useState(false);
+ const [submitEnable, setSubmitEnable] = React.useState(true);
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+ const updateAnswer = (answer: string, index: number) => {
+ const updatedAnswers = answers;
+ const answerPrefix = questions[index].answerPrefix !== LABELS_PREFIX ?
+ '<b>' + questions[index].answerPrefix + '</b> ' : LABELS_PREFIX;
+ updatedAnswers[index] = answerPrefix + answer;
+ setAnswers(updatedAnswers);
+ }
+
+ questions.forEach((q, i) => {
+ switch(q.type) {
+ case CustomQuestionType.Input:
+ customQuestions.push(
+ <CustomQuestionInput
+ question={q.question}
+ updateAnswers={(answer: string) => updateAnswer(answer, i)}
+ />
+ );
+ return;
+ case CustomQuestionType.Text:
+ customQuestions.push(
+ <CustomQuestionTextarea
+ question={q.question}
+ tip={q.tip}
+ updateAnswers={(answer: string) => updateAnswer(answer, i)}
+ />
+ );
+ return;
+ case CustomQuestionType.Select:
+ customQuestions.push(
+ <CustomQuestionSelector
+ question={q.question}
+ tip={q.tip}
+ options={q.options}
+ subQuestions={q.subQuestions}
+ updateAnswers={(answer: string) => updateAnswer(answer, i)}
+ />
+ );
+ return;
+ default:
+ return;
+ }
+ });
+
+ const loadFiles = () => {
+ if (!attachments || attachments.length === 0) {
+ return Promise.resolve([]);
+ }
+ const loads = attachments.map(loadLocalFile);
+ return Promise.all(loads);
+ }
+
+ const loadLocalFile = (f: File) => {
+ return new Promise((resolve, reject) => {
+ const r = new FileReader();
+ r.onloadend = () => {
+ resolve({filename: f.name, content: btoa(r.result)});
+ };
+ r.onerror = () => {
+ reject(r.error);
+ };
+
+ r.readAsBinaryString(f);
+ });
+ }
+
+ const onSuccess = (response: Issue) => {
+ //redirect to issue
+ setIsSubmitting(false);
+ const issueId = response.name.split('/')[3];
+ setnewIssueID(issueId);
+ setActiveStep(3);
+ };
+
+ const onFailure = () => {
+ setIsSubmitting(false);
+ setHasError(true);
+ }
+
+ const onMakeIssue = () => {
+ setHasError(false);
+ setIsSubmitting(true);
+ try {
+ const uploads = loadFiles();
+ uploads.then((files) => {
+ // TODO: add attachments to request
+ onSubmit(additionalComments, answers, files, onSuccess, onFailure);
+ }, onFailure)
+ } catch (e) {
+ onFailure();
+ }
+ }
+
+ return (
+ <>
+ <h2 className={classes.greyText}>Extra Information about the Issue</h2>
+ {hasError
+ ? <Alert severity="error" onClose={() => {setHasError(false)}}>Something went wrong, please try again later.</Alert>
+ : null
+ }
+ <div className={classes.root}>
+ {customQuestions}
+
+ <CustomQuestionTextarea
+ question="Additional comments"
+ updateAnswers={(answer: string) => setAdditionalComments(answer)}
+ />
+
+ <h3>Upload any relevant screenshots</h3>
+ <AttachmentUploader files={attachments} setFiles={setAttachments} setSubmitEnable={setSubmitEnable}/>
+
+ </div>
+ <DotMobileStepper nextEnabled={submitEnable} activeStep={2} setActiveStep={setActiveStep} onSubmit={onMakeIssue}/>
+ <Modal open={isSubmitting} >
+ <Box className={classes.modalBox}>
+ <p className={classes.modalTitle}>Thanks for contributing to Chromium!</p>
+ <p>Stay put, we're filing your issue!</p>
+ </Box>
+ </Modal>
+ </>
+ );
+}
diff --git a/static_src/react/issue-wizard/DetailsStep.test.tsx b/static_src/react/issue-wizard/DetailsStep.test.tsx
index eaef0e7..e53c3a9 100644
--- a/static_src/react/issue-wizard/DetailsStep.test.tsx
+++ b/static_src/react/issue-wizard/DetailsStep.test.tsx
@@ -12,7 +12,12 @@
afterEach(cleanup);
it('renders', async () => {
- const {container} = render(<DetailsStep />);
+ const textFiled = {
+ oneLineSummary: '',
+ stepsToReproduce: '',
+ describeProblem: '',
+ };
+ const {container} = render(<DetailsStep textValues={textFiled} setIsRegression={() => {}}/>);
// this is checking for the first question
const input = container.querySelector('input');
@@ -20,15 +25,21 @@
// this is checking for the rest
const count = document.querySelectorAll('textarea').length;
- assert.equal(count, 3)
+ assert.equal(count, 4)
});
it('renders category in title', async () => {
- const {container} = render(<DetailsStep category='UI'/>);
+ const textFiled = {
+ oneLineSummary: '',
+ stepsToReproduce: '',
+ describeProblem: '',
+ };
+
+ const {container} = render(<DetailsStep category='UI' textValues={textFiled} setIsRegression={() => {}}/>);
// 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
index 1a69cc1..ae968c1 100644
--- a/static_src/react/issue-wizard/DetailsStep.tsx
+++ b/static_src/react/issue-wizard/DetailsStep.tsx
@@ -5,8 +5,14 @@
import React from 'react';
import {createStyles, createTheme} from '@material-ui/core/styles';
import {makeStyles} from '@material-ui/styles';
+import { TextareaAutosize } from '@material-ui/core';
import TextField from '@material-ui/core/TextField';
import {red, grey} from '@material-ui/core/colors';
+import DotMobileStepper from './DotMobileStepper.tsx';
+import SelectMenu from './SelectMenu.tsx';
+import {OS_LIST, ISSUE_WIZARD_QUESTIONS, ISSUE_REPRODUCE_PLACEHOLDER, OS_CHANNEL_LIST} from './IssueWizardConfig.ts'
+import {getTipByCategory} from './IssueWizardUtils.tsx';
+import CustomQuestionSelector from './CustomQuestions/CustomQuestionSelector.tsx';
/**
* The detail step is the second step on the dot
@@ -19,47 +25,120 @@
createStyles({
root: {
'& > *': {
- margin: theme.spacing(1),
width: '100%',
},
},
head: {
- marginTop: '25px',
+ marginTop: '1.5rem',
+ fontSize: '1rem'
},
red: {
color: red[600],
},
- grey: {
- color: grey[600],
+ pageHeader: {
+ color: grey[600],
+ fontSize: '1.5rem',
+ margin: '1rem 0',
},
+ inlineStyle: {
+ display: 'inline-flex',
+ alignItems: 'center',
+ marginTop: '1.5rem',
+ },
+ inlineTitle: {
+ marginRight: '10px',
+ fontSize: '1rem',
+ }
}), {defaultTheme: theme}
);
-export default function DetailsStep({textValues, setTextValues, category}:
- {textValues: Object, setTextValues: Function, category: string}): React.ReactElement {
+type Props = {
+ textValues: Object,
+ setTextValues: Function,
+ category: string,
+ setActiveStep: Function,
+ osName: string,
+ setOsName: Function,
+ setIsRegression: Function,
+};
+
+export default function DetailsStep(props: Props): React.ReactElement {
const classes = useStyles();
+ const {
+ textValues,
+ setTextValues,
+ category,
+ setActiveStep,
+ setIsRegression
+ } = props;
+
const handleChange = (valueName: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const textInput = e.target.value;
setTextValues({...textValues, [valueName]: textInput});
};
+ const selectOs = (os: string) => {
+ setTextValues({...textValues, 'osName': os});
+ }
+
+ const selectChannel = (channel: string) => {
+ setTextValues({...textValues, 'channel': channel});
+ }
+
+ const tipByCategory = getTipByCategory(ISSUE_WIZARD_QUESTIONS);
+
+ const nextEnabled =
+ (textValues.oneLineSummary.trim() !== '') &&
+ (textValues.stepsToReproduce.trim() !== ISSUE_REPRODUCE_PLACEHOLDER) &&
+ (textValues.stepsToReproduce.trim() !== '') &&
+ (textValues.describeProblem.trim() !== '');
+
+ const getTipInnerHtml = () => {
+ return {__html: tipByCategory.get(category)};
+ }
return (
<>
- <h2 className={classes.grey}>Details for problems with {category}</h2>
+ <h2 className={classes.pageHeader}>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')}/>
+ <div dangerouslySetInnerHTML={getTipInnerHtml()}/>
- <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 confirm that the following version information is correct. <span className={classes.red}>*</span></h3>
+ <div className={classes.inlineStyle}>
+ <h3 className={classes.inlineTitle}>Operating System:</h3>
+ <SelectMenu optionsList={OS_LIST} selectedOption={textValues.osName} setOption={selectOs} />
+ <h3 className={classes.inlineTitle}>Channel:</h3>
+ <SelectMenu optionsList={OS_CHANNEL_LIST} selectedOption={textValues.channel} setOption={selectChannel} />
+ </div>
+ <div className={classes.inlineStyle}>
+ <h3 className={classes.inlineTitle}>Chrome version: </h3>
+ <TextField variant="outlined" onChange={handleChange('chromeVersion')} value={textValues.chromeVersion}/>
+ </div>
- <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}>Please enter a one line summary (100 character limit) <span className={classes.red}>*</span></h3>
+ <TextField id="outlined-basic-1" variant="outlined" inputProps={{maxLength: 100}} onChange={handleChange('oneLineSummary')} value={textValues.oneLineSummary}/>
- <h3 className={classes.head}>Additional Comments</h3>
- <TextField multiline rows={3} id="outlined-basic-4" variant="outlined" onChange={handleChange('additionalComments')}/>
+ <h3 className={classes.head}>Steps to reproduce problem (5000 character limit) <span className={classes.red}>*</span></h3>
+ <TextareaAutosize minRows={4} id="outlined-basic-2" maxLength={5000} onChange={handleChange('stepsToReproduce')} value={textValues.stepsToReproduce}/>
+
+ <h3 className={classes.head}>Please describe the problem (5000 character limit)<span className={classes.red}>*</span></h3>
+ <TextareaAutosize minRows={3} id="outlined-basic-3" maxLength={5000} onChange={handleChange('describeProblem')} value={textValues.describeProblem}/>
+
+ <CustomQuestionSelector
+ question="Did this work before?"
+ options={["Not applicable or don't know", "Yes - This is a regression", "No - I think it never worked"]}
+ subQuestions={null}
+ updateAnswers={(answer: string) => {
+ if (answer === "Yes - This is a regression") {
+ setIsRegression(true);
+ } else {
+ setIsRegression(false);
+ }
+ }}
+ />
</form>
+ <DotMobileStepper nextEnabled={nextEnabled} activeStep={1} setActiveStep={setActiveStep}/>
</>
);
}
diff --git a/static_src/react/issue-wizard/DotMobileStepper.test.tsx b/static_src/react/issue-wizard/DotMobileStepper.test.tsx
index 5203110..b7c9aa4 100644
--- a/static_src/react/issue-wizard/DotMobileStepper.test.tsx
+++ b/static_src/react/issue-wizard/DotMobileStepper.test.tsx
@@ -18,17 +18,17 @@
// this is checking the buttons for the stepper rendered
const count = document.querySelectorAll('button').length;
- assert.equal(count, 2)
+ assert.equal(count, 1)
});
- it('back button disabled on first step', () => {
+ it('back button not avlialbe 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;
+ const backButton = document.querySelector('[aria-label="backButton"]');
- // Back button is disabled on the first step.
- assert.isTrue(backButton.disabled);
+ // Back button is not avliable on the first step.
+ assert.notExists(backButton);
});
it('both buttons enabled on second step', () => {
@@ -46,14 +46,4 @@
// 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
index 9870f03..9aa3fa8 100644
--- a/static_src/react/issue-wizard/DotMobileStepper.tsx
+++ b/static_src/react/issue-wizard/DotMobileStepper.tsx
@@ -2,13 +2,15 @@
// 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 React, {useEffect} 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 Box from '@material-ui/core/Box';
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
+import {ConfirmBackModal} from './ConfirmBackModal.tsx';
const theme: Theme = createTheme();
@@ -16,9 +18,19 @@
root: {
width: '100%',
flexGrow: 1,
+ padding: '8px 0px',
+ },
+ back: {
+ padding: '6px 0px',
},
}, {defaultTheme: theme});
+type Props = {
+ nextEnabled: boolean,
+ activeStep: number,
+ setActiveStep: Function,
+ onSubmit?: Function,
+}
/**
* `<DotMobileStepper />`
*
@@ -26,47 +38,82 @@
*
* @return ReactElement.
*/
-export default function DotsMobileStepper({nextEnabled, activeStep, setActiveStep} : {nextEnabled: boolean, activeStep: number, setActiveStep: Function}) : React.ReactElement {
+export default function DotsMobileStepper(props: Props) : React.ReactElement {
+
+ const {nextEnabled, activeStep, setActiveStep, onSubmit} = props;
const classes = useStyles();
+ const [showConfirmModal, setShowConfirmModal] = React.useState(false);
+
const handleNext = () => {
- setActiveStep((prevActiveStep: number) => prevActiveStep + 1);
+ setActiveStep(activeStep + 1);
};
const handleBack = () => {
- setActiveStep((prevActiveStep: number) => prevActiveStep - 1);
+ if (activeStep === 2) {
+ setShowConfirmModal(true);
+ } else {
+ setActiveStep(activeStep - 1);
+ }
};
- let label;
- let icon;
-
- if (activeStep === 2){
- label = 'Submit';
- icon = '';
- } else {
- label = 'Next';
- icon = <KeyboardArrowRight />;
+ const onSubmitIssue = () => {
+ if (onSubmit) {
+ onSubmit();
+ }
}
+
+ const onBrowserBackButtonEvent = (e: Event) => {
+ e.preventDefault();
+ if (activeStep === 0) {
+ window.history.back();
+ } else {
+ setActiveStep(activeStep-1);
+ }
+ }
+
+ useEffect(() => {
+ window.history.pushState(null, '', window.location.pathname);
+ window.addEventListener('popstate', onBrowserBackButtonEvent);
+ return () => {
+ window.removeEventListener('popstate', onBrowserBackButtonEvent);
+ };
+ }, [activeStep]);
+
+ let nextButton;
+ if (activeStep === 2){
+ nextButton = (<Button aria-label="nextButton" size="medium" onClick={onSubmitIssue} disabled={!nextEnabled}>{'Submit'}</Button>);
+ } else {
+ nextButton =
+ (<Button aria-label="nextButton" size="medium" onClick={handleNext} disabled={!nextEnabled}>
+ {'Next'}
+ <KeyboardArrowRight />
+ </Button>);
+ }
+
+ const backButton = activeStep === 0 ? <Box></Box> :
+ (<Button aria-label="backButton" size="medium" onClick={handleBack} disabled={activeStep === 0} className={classes.back}>
+ <KeyboardArrowLeft />
+ Back
+ </Button>);
+
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>
- }
- />
+ <>
+ <MobileStepper
+ id="mobile-stepper"
+ variant="dots"
+ steps={3}
+ position="static"
+ activeStep={activeStep}
+ className={classes.root}
+ nextButton={nextButton}
+ backButton={backButton}
+ />
+ <ConfirmBackModal
+ enable={showConfirmModal}
+ setEnable={setShowConfirmModal}
+ confirmBack={()=>{setActiveStep(activeStep-1);}}
+ />
+ </>
);
-}
\ No newline at end of file
+}
diff --git a/static_src/react/issue-wizard/Header.tsx b/static_src/react/issue-wizard/Header.tsx
new file mode 100644
index 0000000..e8dfdd9
--- /dev/null
+++ b/static_src/react/issue-wizard/Header.tsx
@@ -0,0 +1,26 @@
+// 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 AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import Typography from '@material-ui/core/Typography'
+
+
+
+export default function Header() {
+ return (
+ <>
+ <AppBar sx={{bgcolor: "white"}}>
+ <Toolbar>
+ <img src='/static/images/chromium.svg' width='=40' height='40'/>
+ <Typography variant="h5" component="div" color="black"> Bugs</Typography>
+ </Toolbar>
+ </AppBar>
+ <Toolbar />
+ </>
+ );
+
+
+}
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/IssueWizardConfig.ts b/static_src/react/issue-wizard/IssueWizardConfig.ts
new file mode 100644
index 0000000..b7f1c17
--- /dev/null
+++ b/static_src/react/issue-wizard/IssueWizardConfig.ts
@@ -0,0 +1,470 @@
+// Copyright 2022 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.
+
+// TODO: create a `monorail/frontend/config/` folder to store all the feature config file
+import {IssueCategory, IssueWizardPersona, CustomQuestionType} from "./IssueWizardTypes.tsx";
+
+// Customer Question convert to related labels
+export const LABELS_PREFIX = 'LABELS: ';
+
+export const ISSUE_WIZARD_QUESTIONS: IssueCategory[] = [
+ {
+ name: 'UI',
+ description: 'Problems with the user interface (e.g. tabs, context menus, etc...)',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-UI',
+ customQuestions: [],
+ },
+ {
+ name: 'Network / Downloading',
+ description: 'Problems with accessing remote content',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Internals-Network',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "What specific URL can reproduce the problem?",
+ answerPrefix: "Example URL: ",
+ },
+ ],
+ },
+ {
+ name: 'Audio / Video',
+ description: 'Problems playing back sound or movies',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Internals-Media',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "What specific URL can reproduce the problem?",
+ answerPrefix: "Example URL: ",
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Does this feature work correctly in other browsers?",
+ answerPrefix: "Does this work in other browsers?\n",
+ options: ["Not sure - I don't know", "Yes - This is just a Chromium problem", "No - I can reproduce the problem in another browser"],
+ subQuestions: [
+ null,
+ null,
+ {
+ type:CustomQuestionType.Input,
+ question: "Which other browsers (including versions) also have the problem?",
+ }],
+ },
+ {
+ type: CustomQuestionType.Text,
+ question: "Please open chrome://gpu in a new Chrome tab and paste the report here.",
+ answerPrefix: "Contents of chrome://gpu: \n",
+ }
+ ],
+ },
+ {
+ name: 'Content',
+ description: "Problems with webpages not working correctly",
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: '',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "What specific URL has a problem?",
+ answerPrefix: "Example URL: ",
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Does the problem occur on multiple sites?",
+ answerPrefix: LABELS_PREFIX,
+ options: ["Not sure - I don't know", "Yes - Please describe below", "No - Just that one URL"],
+ subQuestions: [null,null,null],
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Is it a problem with a plugin?",
+ answerPrefix: "Is it a problem with a plugin? ",
+ options: ["Not sure - I don't know", "Yes - Those darn plugins", "No - It's the browser itself"],
+ subQuestions: [
+ null,
+ {
+ type:CustomQuestionType.Input,
+ question: "Which plugin?",
+ },
+ null],
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Does this feature work correctly in other browsers?",
+ answerPrefix: "Does this work in other browsers? ",
+ options: ["Not sure - I don't know", "Yes - This is just a Chromium problem", "No - I can reproduce the problem in another browser"],
+ subQuestions: [
+ null,
+ null,
+ {
+ type:CustomQuestionType.Input,
+ question: "Which other browsers (including versions) also have the problem?",
+ }],
+ },
+ ],
+ },
+ {
+ name: 'Apps',
+ description: 'Issues with Webstore apps',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Platform-Apps',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "What is the link to that software in <a href='https://chrome.google.com/webstore' target='_blank'>the Chrome Webstore </a>?",
+ answerPrefix: "Webstore page: ",
+ }
+ ],
+ },
+ {
+ name: 'Extensions / Themes',
+ description: 'Issues with Webstore extensions and themes',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Platform-Extensions',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Select,
+ question: "What kind of software had the problem?",
+ answerPrefix: LABELS_PREFIX,
+ options: ["Chrome Extension - Adds new browser functionality", "Chrome Theme - Makes Chrome look awesome"],
+ subQuestions: [
+ null,
+ {
+ type:CustomQuestionType.Input,
+ question: "Do you know the latest version where it worked?",
+ },
+ null],
+ },
+ {
+ type: CustomQuestionType.Input,
+ question: "What is the link to that software in <a href='https://chrome.google.com/webstore' target='_blank'>the Chrome Webstore</a>?",
+ answerPrefix: "WebStore page: ",
+ },
+ ],
+ },
+ {
+ name: 'Webstore',
+ description: 'Problems with the Chrome Webstore itself',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Webstore',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "What is the URL of the Chrome WesStore page that had the problem?",
+ answerPrefix: "Webstore page: ",
+ },
+ ],
+ },
+ {
+ name: 'Sync',
+ description: 'Problems syncing data to a Google account',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Services-Sync',
+ customQuestions: [],
+ },
+ {
+ name: 'Enterprise',
+ description: 'Policy configuration and deployment issues',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Enterprise',
+ customQuestions: [],
+ },
+ {
+ name: 'Installation',
+ description: 'Problem installing Chrome',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: 'Cr-Internals-Installer',
+ customQuestions: [],
+ },
+ {
+ name: 'Crashes',
+ description: 'The browser closes abruptly or I see "Aw, Snap!" pages',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ tip: 'Please read the instructions on <a href="https://sites.google.com/a/chromium.org/dev/for-testers/bug-reporting-guidelines/reporting-crash-bug" target="_blank">reporting a crash issue</a>',
+ component: '',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Input,
+ question: "Do you have a Report ID from chrome://crashes?",
+ answerPrefix: "Crashed report ID: ",
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "How severe is the crash?",
+ options: ["Just one tab", "Just one plugin", "The whole browser"],
+ answerPrefix: "How much crashed? ",
+ subQuestions: null,
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Is it a problem with a plugin?",
+ answerPrefix: "Is it a problem with a plugin? ",
+ options: ["Not sure - I don't know", "Yes - Those darn plugins", "No - It's the browser itself"],
+ subQuestions: [
+ null,
+ {
+ type:CustomQuestionType.Input,
+ question: "Which plugin?",
+ },
+ null],
+ },
+ ],
+ labels: ['Stability-Crash'],
+ },
+ {
+ name: 'Security',
+ description: 'Problems with the browser security',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ tip: 'Please follow the instructions for <a href="https://www.chromium.org/Home/chromium-security/reporting-security-bugs" target="_blank">reporting security issues</a>.',
+ component: '',
+ customQuestions: [],
+ labels: ['Restrict-View-SecurityTeam'],
+ },
+ {
+ name: 'Other',
+ description: 'Something not listed here',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ component: '',
+ customQuestions: [
+ {
+ type: CustomQuestionType.Select,
+ question: "Please select a label to classify your issue:",
+ answerPrefix: LABELS_PREFIX,
+ options: [
+ "Not sure - I don't know",
+ "Type-Feature - Request for new or improved features",
+ "Type-Bug-Regression - Used to work, now broken",
+ "Type-Bug - Software not working correctly",
+ "Cr-UI-I18N - Issue in translating UI to other languages"
+ ],
+ subQuestions: null,
+ },
+ ],
+ },
+ {
+ name: 'API',
+ description: 'Problems with a browser API',
+ persona: IssueWizardPersona.Developer,
+ enabled: true,
+ component: '',
+ customQuestions: [
+ {
+ type:CustomQuestionType.Select,
+ question:"Which <a href='https://bugs.chromium.org/p/chromium/adminComponents' target='_blank'>component</a> does this fall under?",
+ answerPrefix: LABELS_PREFIX,
+ options: [
+ "Not sure - I don't know",
+ "Blink>Animation",
+ "Blink>BackgroundSync",
+ "Blink>Bindings",
+ "Blink>Bluetooth",
+ "Blink>Canvas",
+ "Blink>Compositing",
+ "Blink>CSS",
+ "Blink>DataTransfer",
+ "Blink>DOM",
+ "Blink>Editing",
+ "Blink>FileAPI",
+ "Blink>Focus",
+ "Blink>Fonts",
+ "Blink>Forms",
+ "Blink>Fullscreen",
+ "Blink>GamepadAPI",
+ "Blink>GetUserMedia",
+ "Blink>HitTesting",
+ "Blink>HTML",
+ "Blink>Image",
+ "Blink>Input",
+ "Blink>Internals",
+ "Blink>Javascript",
+ "Blink>Layout",
+ "Blink>Loader",
+ "Blink>Location",
+ "Blink>Media",
+ "Blink>MediaStream",
+ "Blink>MemoryAllocator",
+ "Blink>Messaging",
+ "Blink>Network",
+ "Blink>Paint",
+ "Blink>Payments",
+ "Blink>PerformanceAPIs",
+ "Blink>PermissionsAPI",
+ "Blink>PresentationAPI",
+ "Blink>PushAPI",
+ "Blink>SavePage",
+ "Blink>Scheduling",
+ "Blink>Scroll",
+ "Blink>SecurityFeature",
+ "Blink>ServiceWorker",
+ "Blink>Speech",
+ "Blink>Storage",
+ "Blink>SVG",
+ "Blink>TextAutosize",
+ "Blink>TextEncoding",
+ "Blink>TextSelection",
+ "Blink>USB",
+ "Blink>Vibration",
+ "Blink>ViewSource",
+ "Blink>WebAudio",
+ "Blink>WebComponents",
+ "Blink>WebCrypto",
+ "Blink>WebFonts",
+ "Blink>WebGL",
+ "Blink>WebGPU",
+ "Blink>WebMIDI",
+ "Blink>WebRTC",
+ "Blink>WebShare",
+ "Blink>WebVR",
+ "Blink>WindowDialog",
+ "Blink>Workers",
+ "Blink>XML",
+ ],
+ subQuestions: null,
+ },
+ {
+ type: CustomQuestionType.Select,
+ question: "Does this feature work correctly in other browsers?",
+ answerPrefix: "Does this work in other browsers? ",
+ tip: "Tip: Use <a href='https://www.browserstack.com/' target='_blank'>browserstack.com</a> to compare behavior on different browser versions.",
+ options: ["Not sure - I don't know", "Yes - This is just a Chrome problem", "No - I can reproduce the problem in another browser"],
+ subQuestions: [
+ null,
+ null,
+ {
+ type:CustomQuestionType.Text,
+ question: "Details of interop issue",
+ tip: "Please describe what the behavior is on other browsers and link to any <a href='https://browser-issue-tracker-search.appspot.com/' target='_blank'>existing bugs.</a>",
+ }
+ ],
+ },
+ ]
+ },
+ {
+ name: 'JavaScript',
+ description: 'Problems with the JavaScript interpreter',
+ persona: IssueWizardPersona.Developer,
+ enabled: true,
+ component: 'Cr-Blink',
+ customQuestions: [],
+ },
+ {
+ name: 'Developer Tools',
+ description: 'Problems with the Developer tool chain/inspector',
+ persona: IssueWizardPersona.Developer,
+ enabled: true,
+ component: 'Cr-Platform-DevTools',
+ customQuestions: [],
+ },
+];
+
+export const OS_LIST = [
+ {
+ name: 'Android',
+ description: '',
+ },
+ {
+ name: 'Chrome OS',
+ description: '',
+ },
+ {
+ name: 'iOS',
+ description: '',
+ },
+ {
+ name: 'Linux',
+ description: '',
+ },
+ {
+ name: 'Mac OS',
+ description: '',
+ },
+ {
+ name: 'Windows',
+ description: '',
+ },
+ {
+ name: 'Unknown/Other',
+ description: '',
+ },
+]
+
+// possible user os channel
+export const OS_CHANNEL_LIST = [
+ {
+ name: 'Not sure',
+ description: '',
+ },
+ {
+ name: 'Stable',
+ description: '',
+ },
+ {
+ name: 'Beta',
+ description: '',
+ },
+ {
+ name: 'Dev',
+ description: '',
+ },
+ {
+ name: 'Canary',
+ description: '',
+ },
+]
+
+export const BROWSER_LIST = [
+ {
+ name: 'Apple Safari',
+ description: '',
+ },
+ {
+ name: 'Google Chrome or Chromium',
+ description: '',
+ },
+ {
+ name: 'Mozilla Firefox',
+ description: '',
+ },
+ {
+ name: 'Microsoft Edge (Chromium)',
+ description: '',
+ },
+ {
+ name: 'Microsoft Edge (Legacy)',
+ description: '',
+ },
+ {
+ name: 'Microsoft Internet Explorer',
+ description: '',
+ },
+ {
+ name: 'Opera',
+ description: '',
+ },
+ {
+ name: 'Samsung Internet',
+ description: '',
+ },
+ {
+ name: 'Unknown / Other',
+ description: '',
+ },
+]
+
+export const ISSUE_REPRODUCE_PLACEHOLDER = '1.\n2.\n3.';
diff --git a/static_src/react/issue-wizard/IssueWizardDescriptionsUtils.tsx b/static_src/react/issue-wizard/IssueWizardDescriptionsUtils.tsx
new file mode 100644
index 0000000..e32d2d5
--- /dev/null
+++ b/static_src/react/issue-wizard/IssueWizardDescriptionsUtils.tsx
@@ -0,0 +1,85 @@
+// Copyright 2022 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 {LABELS_PREFIX} from "./IssueWizardConfig.ts";
+
+// Chromium project component prefix
+const CR_PREFIX = 'Cr-';
+
+// customized function for add additoinal data base on different categories.
+export function expandDescriptions(
+ category: string,
+ customQuestionsAnswers: Array<string>,
+ isRegression: boolean,
+ description: string,
+ labels: Array<any>,
+ component?: string,
+ ): {expandDescription:string, expandLabels:Array<any>, compVal:string} {
+ let expandDescription = "";
+ let expandLabels = labels;
+ let compVal = component || '';
+ let typeLabel = isRegression ? 'Type-Bug-Regression' : 'Type-Bug';
+
+ customQuestionsAnswers.forEach((ans) => {
+ if (ans.startsWith(LABELS_PREFIX)) {
+ const currentAnswer = ans.substring(LABELS_PREFIX.length);
+ switch (category) {
+ case 'Content':
+ if (currentAnswer.split(' - ')[0] === 'Yes') {
+ compVal = 'Cr-Blink';
+ typeLabel = 'Type-Bug';
+ } else {
+ compVal = '';
+ typeLabel = 'Type-Compat';
+ }
+ break;
+ case 'Extensions / Themes':
+ if (currentAnswer.split(' - ')[0] === 'Chrome Extension') {
+ compVal = 'Cr-Platform-Extensions';
+ } else {
+ compVal = 'Cr-UI-Browser-Themes';
+ }
+ break;
+ case 'Security':
+ if (typeLabel === '') {
+ typeLabel = 'Type-Bug-Security';
+ }
+ case 'Other':
+ typeLabel = "Type-Bug";
+ const issueType = currentAnswer.split(' - ')[0];
+ if (issueType !== 'Not sure'){
+ typeLabel = issueType;
+ }
+ if (issueType === 'Cr-UI-I18N') {
+ compVal = 'Cr-UI-I18N';
+ }
+ break;
+ case 'API':
+ compVal = currentAnswer;
+ if (compVal === "Not sure - I don't know") {
+ compVal = '';
+ }
+ break;
+ }
+ } else {
+ expandDescription = expandDescription + ans + "\n\n";
+ }
+ });
+
+ expandDescription = expandDescription + description;
+
+ if (typeLabel.length > 0) {
+ expandLabels.push({
+ label: typeLabel
+ });
+ }
+
+ if (compVal.length > 0) {
+ if (compVal.startsWith(CR_PREFIX)) {
+ compVal = compVal.substring(CR_PREFIX.length);
+ compVal = compVal.replace(/-/g, '>');
+ }
+ }
+ return {expandDescription, expandLabels, compVal};
+ }
diff --git a/static_src/react/issue-wizard/IssueWizardFeedback.tsx b/static_src/react/issue-wizard/IssueWizardFeedback.tsx
new file mode 100644
index 0000000..0dff09b
--- /dev/null
+++ b/static_src/react/issue-wizard/IssueWizardFeedback.tsx
@@ -0,0 +1,98 @@
+// Copyright 2019 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 * as React from 'react';
+import {makeStyles} from '@material-ui/styles';
+import Dialog from '@material-ui/core/Dialog';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogContentText from '@material-ui/core/DialogContentText';
+import DialogActions from '@material-ui/core/DialogActions';
+import Button from '@material-ui/core/Button';
+import Input from '@material-ui/core/Input';
+
+const userStyles = makeStyles({
+ title: {
+ backgroundColor: 'rgb(84, 110, 122)',
+ color: 'white',
+ font: '300 20px / 24px Roboto, RobotoDraft, Helvetica, Arial, sans-serif'
+ },
+ inputArea: {
+ padding: '10px',
+ },
+ content: {
+ backgroundColor: 'rgb(250, 250, 250)',
+ padding: '12px 16px',
+ },
+ contentText: {
+ fontSize: '12px',
+ },
+ actionsButton: {
+ backgroundColor: 'rgb(250, 250, 250)',
+ borderTop: '1px solid rgb(224, 224, 224)',
+ }
+});
+
+type Props = {
+ enable: boolean,
+ setEnable: Function,
+}
+
+export function IssueWizardFeedback(props: Props): React.ReactElement {
+ React.useEffect(() => {
+ const script = document.createElement("script");
+ script.src = 'https://support.google.com/inapp/api.js';
+ script.async = true;
+ document.body.appendChild(script);
+ }, []);
+
+ const classes = userStyles();
+ const {enable, setEnable} = props;
+ const [feedback, setFeedback] = React.useState('');
+
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const textInput = e.target.value;
+ setFeedback(textInput);
+ };
+
+ const issueWizardFeedbackSend = () => {
+ window.userfeedback.api.startFeedback({
+ 'productId': '5208992', // Required.
+ 'bucket': 'IssueWizard', // Optional.
+ 'report': {
+ 'description': feedback
+ }
+ });
+ setEnable(false);
+ }
+
+ return (
+ <Dialog open={enable}>
+ <DialogTitle className={classes.title}>Send Feedback</DialogTitle>
+ <Input
+ placeholder="Have Feedback? We'd love to hear it, but please don't share sensitive informations. Have questions? Try help or support."
+ disableUnderline={true}
+ multiline={true}
+ rows={3}
+ className={classes.inputArea}
+ inputProps={{maxLength: 5000}}
+ onChange={handleInputChange}
+ />
+ <DialogContent className={classes.content}>
+ <DialogContentText className={classes.contentText}>
+ Some account and system information may be sent to Google. We will use it to fix problems and improve our services, subject to our
+ <a href="https://myaccount.google.com/privacypolicy?hl=en&authuser=0" target="_blank"> Privacy Policy </a>
+ and <a href="https://www.google.com/intl/en/policies/terms?authuser=0" target="_blank"> Terms of Service </a>
+ . We may email you for more information or updates.
+ Go to <a href="https://support.google.com/legal/answer/3110420?hl=en&authuser=0" target="_blank"> Legal Help </a>
+ to ask for content changes for legal reasons.
+ </DialogContentText>
+ </DialogContent>
+ <DialogActions className={classes.actionsButton}>
+ <Button onClick={()=>{setEnable(false);}}>Cancel</Button>
+ <Button onClick={issueWizardFeedbackSend}>Send</Button>
+ </DialogActions>
+ </Dialog>
+ );
+}
diff --git a/static_src/react/issue-wizard/IssueWizardTypes.tsx b/static_src/react/issue-wizard/IssueWizardTypes.tsx
new file mode 100644
index 0000000..3f43bce
--- /dev/null
+++ b/static_src/react/issue-wizard/IssueWizardTypes.tsx
@@ -0,0 +1,57 @@
+// Copyright 2022 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.
+
+// this const is used on issue wizard lading page for render user role options
+export enum IssueWizardPersona {
+ EndUser = "EndUser",
+ Developer = "Developer",
+ Contributor = "Contributor",
+};
+
+
+export const ISSUE_WIZARD_PERSONAS_DETAIL = Object.freeze({
+ [IssueWizardPersona.EndUser]: {
+ name: 'End User',
+ description: 'I am trying to use a website.',
+ },
+ [IssueWizardPersona.Developer]: {
+ name: 'Web Developer',
+ description: 'I am trying to build something on a website.',
+ },
+ [IssueWizardPersona.Contributor]: {
+ name: 'Chromium Contributor',
+ description: 'I know about a problem in specific tests or code.',
+ }
+});
+
+export enum CustomQuestionType {
+ EMPTY, // this is used to define there is no subquestions
+ Text,
+ Input,
+ Select,
+}
+export type CustomQuestion = {
+ type: CustomQuestionType,
+ question: string,
+ answerPrefix?: string,
+ tip?: string,
+ options?: string[],
+ subQuestions?: CustomQuestion[] | null,
+};
+
+export type IssueCategory = {
+ name: string,
+ description: string,
+ persona: IssueWizardPersona,
+ enabled: boolean,
+ tip?: string,
+ component?: string,
+ customQuestions?: CustomQuestion[],
+ labels?: Array<string>,
+};
+
+export type SelectMenuOption = {
+ name: string,
+ description?: string,
+};
diff --git a/static_src/react/issue-wizard/IssueWizardUtils.tsx b/static_src/react/issue-wizard/IssueWizardUtils.tsx
new file mode 100644
index 0000000..e709115
--- /dev/null
+++ b/static_src/react/issue-wizard/IssueWizardUtils.tsx
@@ -0,0 +1,162 @@
+// Copyright 2022 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 {CustomQuestion, IssueCategory, SelectMenuOption, IssueWizardPersona} from "./IssueWizardTypes";
+
+
+const CHROME_VERSION_REX = /chrome\/(\d|\.)+/i;
+// this function is used to get the issue list belong to different persona
+// when a user group is selected a list of related issue categories will show up
+export function GetCategoriesByPersona (categories: IssueCategory[]): Map<IssueWizardPersona, SelectMenuOption[]> {
+ const categoriesByPersona = new Map<IssueWizardPersona, SelectMenuOption[]>();
+
+ categories.forEach((category) => {
+ if (category.enabled) {
+ const currentIssuePersona = category.persona;
+ const currentCategories = categoriesByPersona.get(currentIssuePersona) ?? [];
+ currentCategories.push({
+ name: category.name,
+ description: category.description,
+ });
+ categoriesByPersona.set(currentIssuePersona, currentCategories);
+ }
+ });
+
+ return categoriesByPersona;
+}
+
+// this function is used to get the customer questions belong to different issue category
+// the customer question page will render base on these data
+export function GetQuestionsByCategory(categories: IssueCategory[]): Map<string, CustomQuestion[] | null> {
+ const questionsByCategory = new Map<string, CustomQuestion[] | null>();
+ categories.forEach((category) => {
+ questionsByCategory.set(category.name, category.customQuestions ?? null);
+ })
+ return questionsByCategory;
+}
+
+// this function is used to convert the options list fit for render use SelectMenu
+export function GetSelectMenuOptions(optionsList: string[]): SelectMenuOption[] {
+ const selectMenuOptionList = new Array<SelectMenuOption>();
+ optionsList.forEach((option) => {
+ selectMenuOptionList.push({name: option});
+ });
+ return selectMenuOptionList;
+}
+
+/**
+ * Detects the user's operating system.
+ */
+ export function getOs() {
+ const userAgent = window.navigator.userAgent,
+ platform = window.navigator.platform,
+ macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
+ windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
+ iosPlatforms = ['iPhone', 'iPad', 'iPod'];
+
+ if (macosPlatforms.indexOf(platform) !== -1) {
+ return'Mac OS';
+ } else if (iosPlatforms.indexOf(platform) !== -1) {
+ return 'iOS';
+ } else if (windowsPlatforms.indexOf(platform) !== -1) {
+ return 'Windows';
+ } else if (/Android/.test(userAgent)) {
+ return 'Android';
+ } else if (/Linux/.test(platform)) {
+ return 'Linux';
+ } else if (/\bCrOS\b/.test(userAgent)) {
+ return 'Chrome OS';
+ }
+
+ return 'Unknown / Other';
+
+}
+
+// this function is used to get the tip belong to different issue category
+// used for render detail page
+export function getTipByCategory(categories: IssueCategory[]): Map<string, string> {
+ const tipByCategory = new Map<string, string>();
+ categories.forEach((category) => {
+ if (category.tip) {
+ tipByCategory.set(category.name, category.tip);
+ }
+ })
+ return tipByCategory;
+}
+
+// this function is used to get the component value for each issue category used for make issue
+export function getCompValByCategory(categories: IssueCategory[]): Map<string, string> {
+ const compValByCategory = new Map<string, string>();
+ categories.forEach((category) => {
+ if (category.component) {
+ compValByCategory.set(category.name, category.component);
+ }
+ })
+ return compValByCategory;
+}
+
+export function getLabelsByCategory(categories: IssueCategory[]): Map<string, Array<string>> {
+ const labelsByCategory = new Map<string, Array<string>>();
+ categories.forEach((category) => {
+ if (category.labels) {
+ labelsByCategory.set(category.name, category.labels);
+ }
+ })
+ return labelsByCategory;
+}
+
+
+export function buildIssueDescription(
+ reproduceStep: string,
+ description: string,
+ comments: string,
+ os: string,
+ chromeVersion: string,
+ channel: string,
+ ): string {
+ const issueDescription =
+ "<b>Steps to reproduce the problem:</b>\n" + reproduceStep.trim() + "\n\n"
+ + "<b>Problem Description:</b>\n" + description.trim() + "\n\n"
+ + "<b>Additional Comments:</b>\n" + comments.trim() + "\n\n"
+ + "<b>Chrome version: </b>" + chromeVersion.trim() + " <b>Channel: </b>" + channel + "\n\n"
+ + "<b>OS:</b>" + os.trim();
+ return issueDescription;
+}
+
+export function buildIssueLabels(category: string, osName: string, chromeVersion: string, configLabels: Array<string> | null | undefined): Array<any> {
+ const labels = [
+ {label:'via-wizard-'+category},
+ {label:'Pri-2'},
+ ];
+
+ const os = osName.split(' ')[0];
+ if (os !== 'Unknown/Other') {
+ labels.push({
+ label: 'OS-'+os
+ })
+ }
+ const mainChromeVersion = chromeVersion.split('.').length > 0 ? chromeVersion.split('.')[0] : null;
+ if (mainChromeVersion !== null) {
+ labels.push({
+ label:'Needs-Triage-M'+mainChromeVersion
+ });
+ }
+
+ if (configLabels) {
+ configLabels.forEach((v) => {
+ labels.push({label: v});
+ })
+ }
+ return labels;
+}
+
+
+export function getChromeVersion() {
+ const userAgent = window.navigator.userAgent;
+ var browser= userAgent.match(CHROME_VERSION_REX) || [];
+ if (browser.length > 0) {
+ return browser[0].split('/')[1];
+ }
+ return "<Copy from:'about:version'>";
+}
diff --git a/static_src/react/issue-wizard/LandingStep.tsx b/static_src/react/issue-wizard/LandingStep.tsx
index 2925e87..3a83b2c 100644
--- a/static_src/react/issue-wizard/LandingStep.tsx
+++ b/static_src/react/issue-wizard/LandingStep.tsx
@@ -9,6 +9,10 @@
import Checkbox, { CheckboxProps } from '@material-ui/core/Checkbox';
import SelectMenu from './SelectMenu.tsx';
import { RadioDescription } from './RadioDescription/RadioDescription.tsx';
+import {GetCategoriesByPersona} from './IssueWizardUtils.tsx';
+import {ISSUE_WIZARD_QUESTIONS} from './IssueWizardConfig.ts';
+import DotMobileStepper from './DotMobileStepper.tsx';
+import {IssueWizardPersona} from './IssueWizardTypes.tsx';
const CustomCheckbox = withStyles({
root: {
@@ -28,9 +32,6 @@
flex: {
display: 'flex',
},
- inlineBlock: {
- display: 'inline-block',
- },
warningBox: {
minHeight: '10vh',
borderStyle: 'solid',
@@ -39,7 +40,7 @@
borderRadius: '8px',
background: yellow[50],
padding: '0px 20px 1em',
- margin: '30px 0px'
+ margin: '1rem 0'
},
warningHeader: {
color: yellow[800],
@@ -54,27 +55,79 @@
},
header: {
color: grey[900],
- fontSize: '28px',
- marginTop: '6vh',
+ fontSize: '1.5rem',
+ margin: '1rem 0',
},
subheader: {
color: grey[700],
- fontSize: '18px',
- lineHeight: '32px',
+ fontSize: '1.125rem',
+ margin: '1rem 0',
+ },
+ alertDetail: {
+ fontSize: '16px',
+ },
+ link: {
+ fontSize: '20px',
+ fontWeight: 'bolder',
+ textDecoration: 'underline',
},
red: {
color: red[600],
},
+ line: {
+ color: grey[200],
+ marginTop: '1.5rem',
+ minWidth: '360px',
+ }
});
-export default function LandingStep({ checkExisting, setCheckExisting, userType, setUserType, category, setCategory }:
- { checkExisting: boolean, setCheckExisting: Function, userType: string, setUserType: Function, category: string, setCategory: Function }) {
+type Props = {
+ userPersona: IssueWizardPersona,
+ setUserPersona: Function,
+ category: string,
+ setCategory: Function,
+ setActiveStep: Function,
+};
+
+export default function LandingStep(props: Props) {
+
+ const {userPersona, setUserPersona, category, setCategory, setActiveStep} = props;
const classes = useStyles();
+ const categoriesByPersonaMap = GetCategoriesByPersona(ISSUE_WIZARD_QUESTIONS);
+
+ const [categoryList, setCategoryList] = React.useState(categoriesByPersonaMap.get(userPersona));
+ const [checkExisting, setCheckExisting] = React.useState(false);
+
const handleCheckChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCheckExisting(event.target.checked);
};
+ const onSelectUserPersona = (userPersona: string) => {
+ setUserPersona(userPersona);
+ setCategoryList(categoriesByPersonaMap.get(userPersona));
+ setCategory('');
+ }
+
+ const contributorAlert = () => {
+ return (
+ <div>
+ <div className={classes.subheader}>
+ Prefer to file an issue manually?
+ </div>
+ <div className={classes.alertDetail}>
+ It's usually best to work through this short wizard so that your issue is given the labels needed for the right team to see it.
+ Otherwise it might take longer for your issue to be triaged and resolved.
+ </div>
+ <div className={classes.alertDetail}>
+ However, if you are a Chromium contributor and none of the other options apply, you may use the
+ <a className={classes.link} href="entry"> regular issue entry form</a>.
+ </div>
+ </div>
+ );
+ }
+
+ const nextEnabled = (userPersona != IssueWizardPersona.Contributor) && checkExisting && (category != '');
return (
<>
<p className={classes.header}>Report an issue with Chromium</p>
@@ -86,27 +139,35 @@
<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>
+ <RadioDescription selectedRadio={userPersona} onClickRadio={onSelectUserPersona} />
+ { userPersona === IssueWizardPersona.Contributor ? contributorAlert() :
<div>
- <div className={classes.star}>*</div>
- <FormControlLabel className={classes.pad}
- control={
- <CustomCheckbox
- checked={checkExisting}
- onChange={handleCheckChange}
- name="warningCheck"
+ <div className={classes.subheader}>
+ Which of the following best describes the issue that you are reporting? <span className={classes.red}>*</span>
+ </div>
+ <SelectMenu optionsList={categoryList} selectedOption={category} setOption={setCategory} />
+ <div className={classes.warningBox}>
+ <p className={classes.warningHeader}> <span className={classes.star}>*</span>Avoid duplicate issue reports:</p>
+ <div>
+ <FormControlLabel className={classes.pad}
+ control={
+ <CustomCheckbox
+ checked={checkExisting}
+ onChange={handleCheckChange}
+ name="warningCheck"
+ />
+ }
+ label={
+ <span>By checking this box, I'm acknowledging that I have searched for <a href="/p/chromium/issues/list" target="_blank">existing issues</a> that already report this problem.</span>
+ }
/>
- }
- label="By checking this box, I'm acknowledging that I have searched for existing issues that already report this problem."
- />
+ </div>
+ </div>
</div>
- </div>
+ }
+ { userPersona === IssueWizardPersona.Contributor ? null :
+ <DotMobileStepper nextEnabled={nextEnabled} activeStep={0} setActiveStep={setActiveStep}/>
+ }
</>
);
-}
\ No newline at end of file
+}
diff --git a/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx b/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx
index 296e449..6d1398f 100644
--- a/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx
+++ b/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx
@@ -3,12 +3,12 @@
// 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 { render, screen, cleanup, fireEvent } from '@testing-library/react';
import { assert } from 'chai';
import sinon from 'sinon';
import { RadioDescription } from './RadioDescription.tsx';
+import {IssueWizardPersona} from '../IssueWizardTypes.tsx';
describe('RadioDescription', () => {
afterEach(cleanup);
@@ -29,7 +29,7 @@
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'} />);
+ render(<RadioDescription selectedRadio={IssueWizardPersona.Developer} />);
const checkedRadio = screen.getByRole('radio', { name: /Web Developer/i });
assert.isTrue(checkedRadio.checked);
@@ -43,25 +43,25 @@
// Using the sinon.js testing library to create a function for testing.
const setValue = sinon.stub();
- render(<RadioDescription setValue={setValue} />);
+ render(<RadioDescription onClickRadio={setValue} />);
const radio = screen.getByRole('radio', { name: /Web Developer/i });
- userEvent.click(radio);
+ fireEvent.click(radio);
// Asserts that "Web Developer" was passed into our "setValue" function.
- sinon.assert.calledWith(setValue, 'Web Developer');
+ sinon.assert.calledWith(setValue, IssueWizardPersona.Developer);
});
it('sets radio value when any part of the parent RoleSelection is clicked', () => {
const setValue = sinon.stub();
- render(<RadioDescription setValue={setValue} />);
+ render(<RadioDescription onClickRadio={setValue} />);
// Click text in the RoleSelection component
const p = screen.getByText('End User');
- userEvent.click(p);
+ fireEvent.click(p);
// Asserts that "End User" was passed into our "setValue" function.
- sinon.assert.calledWith(setValue, 'End User');
+ sinon.assert.calledWith(setValue, IssueWizardPersona.EndUser);
});
-});
\ No newline at end of file
+});
diff --git a/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx b/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx
index 9a5a7d2..d371ef7 100644
--- a/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx
+++ b/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx
@@ -5,12 +5,7 @@
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import { RoleSelection } from './RoleSelection/RoleSelection.tsx';
-
-const userGroups = Object.freeze({
- END_USER: 'End User',
- WEB_DEVELOPER: 'Web Developer',
- CONTRIBUTOR: 'Chromium Contributor',
-});
+import {ISSUE_WIZARD_PERSONAS_DETAIL, IssueWizardPersona} from '../IssueWizardTypes.tsx';
const useStyles = makeStyles({
flex: {
@@ -19,41 +14,48 @@
}
});
+const getUserGroupSelectors = (
+ value: IssueWizardPersona,
+ onSelectorClick:
+ (selector: string) =>
+ (event: React.MouseEvent<HTMLElement>) => any) => {
+ const selectors = new Array();
+ Object.entries(ISSUE_WIZARD_PERSONAS_DETAIL).forEach(([key, persona]) => {
+ selectors.push(
+ <RoleSelection
+ checked={IssueWizardPersona[value] === key}
+ handleOnClick={onSelectorClick(key)}
+ value={persona.name}
+ description={persona.description}
+ inputProps={{ 'aria-label': persona.name }}
+ />
+ );
+ });
+ return selectors;
+}
/**
* RadioDescription contains a set of radio buttons and descriptions (RoleSelection)
* to be chosen from in the landing step of the Issue Wizard.
*
* @returns React.ReactElement
*/
-export const RadioDescription = ({ value, setValue }: { value: string, setValue: Function }): React.ReactElement => {
+type Props = {
+ selectedRadio: IssueWizardPersona,
+ onClickRadio: Function,
+}
+
+export const RadioDescription = (props: Props): React.ReactElement => {
+ const { selectedRadio, onClickRadio } = props;
const classes = useStyles();
const handleRoleSelectionClick = (userGroup: string) =>
- (event: React.MouseEvent<HTMLElement>) => setValue(userGroup)
+ (event: React.MouseEvent<HTMLElement>) => onClickRadio(userGroup);
+
+ const userGroupsSelectors = getUserGroupSelectors(selectedRadio, handleRoleSelectionClick);
return (
<div className={classes.flex}>
- <RoleSelection
- checked={value === userGroups.END_USER}
- handleOnClick={handleRoleSelectionClick(userGroups.END_USER)}
- value={userGroups.END_USER}
- description="I am a user trying to do something on a website."
- inputProps={{ 'aria-label': userGroups.END_USER }}
- />
- <RoleSelection
- checked={value === userGroups.WEB_DEVELOPER}
- handleOnClick={handleRoleSelectionClick(userGroups.WEB_DEVELOPER)}
- value={userGroups.WEB_DEVELOPER}
- description="I am a web developer trying to build something."
- inputProps={{ 'aria-label': userGroups.WEB_DEVELOPER }}
- />
- <RoleSelection
- checked={value === userGroups.CONTRIBUTOR}
- handleOnClick={handleRoleSelectionClick(userGroups.CONTRIBUTOR)}
- value={userGroups.CONTRIBUTOR}
- description="I know about a problem in specific tests or code."
- inputProps={{ 'aria-label': userGroups.CONTRIBUTOR }}
- />
+ {userGroupsSelectors}
</div>
);
-}
\ No newline at end of file
+}
diff --git a/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx b/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx
index 803a7b7..f1f1933 100644
--- a/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx
+++ b/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx
@@ -10,7 +10,8 @@
const useStyles = makeStyles({
container: {
width: '320px',
- height: '150px',
+ minWidth: '140px',
+ height: '160px',
position: 'relative',
display: 'inline-block',
cursor: 'pointer',
@@ -18,15 +19,14 @@
text: {
position: 'absolute',
display: 'inline-block',
- left: '55px',
},
title: {
- marginTop: '7px',
- fontSize: '20px',
+ margin: '0.5rem 0',
+ fontSize: '1.125rem',
color: grey[900],
},
subheader: {
- fontSize: '16px',
+ fontSize: '0.875rem',
color: grey[800],
},
line: {
diff --git a/static_src/react/issue-wizard/SelectMenu.test.tsx b/static_src/react/issue-wizard/SelectMenu.test.tsx
index 13efef6..b25baea 100644
--- a/static_src/react/issue-wizard/SelectMenu.test.tsx
+++ b/static_src/react/issue-wizard/SelectMenu.test.tsx
@@ -14,7 +14,7 @@
let container: React.RenderResult;
beforeEach(() => {
- container = render(<SelectMenu />).container;
+ container = render(<SelectMenu optionsList = {['op1', 'op2']} />).container;
});
it('renders', () => {
@@ -22,7 +22,7 @@
assert.isNotNull(form)
});
- it('renders options on click', () => {
+ it('renders options on click', async () => {
const input = document.getElementById('outlined-select-category');
if (!input) {
throw new Error('Input is undefined');
@@ -31,8 +31,8 @@
userEvent.click(input)
// 14 is the current number of options in the select menu
- const count = screen.getAllByTestId('select-menu-item').length;
+ const count = (await screen.findAllByTestId('select-menu-item')).length;
- assert.equal(count, 14);
+ assert.equal(count, 2);
});
-});
\ No newline at end of file
+});
diff --git a/static_src/react/issue-wizard/SelectMenu.tsx b/static_src/react/issue-wizard/SelectMenu.tsx
index 3b0b96d..f440d55 100644
--- a/static_src/react/issue-wizard/SelectMenu.tsx
+++ b/static_src/react/issue-wizard/SelectMenu.tsx
@@ -7,65 +7,7 @@
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',
- },
-];
+import {SelectMenuOption} from './IssueWizardTypes.tsx';
const theme: Theme = createTheme();
@@ -74,15 +16,19 @@
display: 'flex',
flexWrap: 'wrap',
maxWidth: '65%',
+ marginRight: '1rem',
},
textField: {
- marginLeft: theme.spacing(1),
- marginRight: theme.spacing(1),
+ margin: '0',
},
menu: {
width: '100%',
minWidth: '300px',
},
+ description: {
+ fontSize: 'small',
+ color: 'gray',
+ },
}), {defaultTheme: theme});
/**
@@ -92,8 +38,17 @@
*
* @return ReactElement.
*/
-export default function SelectMenu({option, setOption}: {option: string, setOption: Function}) {
+type Props = {
+ optionsList: SelectMenuOption[] | null,
+ selectedOption: SelectMenuOption | null,
+ setOption: Function,
+};
+
+export default function SelectMenu(props: Props) {
const classes = useStyles();
+
+ const {optionsList, selectedOption, setOption} = props;
+
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
setOption(event.target.value as string);
};
@@ -105,7 +60,7 @@
select
label=''
className={classes.textField}
- value={option}
+ value={selectedOption}
onChange={handleChange}
InputLabelProps={{shrink: false}}
SelectProps={{
@@ -117,17 +72,25 @@
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>
- ))}
+ {
+ optionsList?.map(option => (
+ <MenuItem
+ className={classes.menu}
+ key={option.name}
+ value={option.name}
+ data-testid="select-menu-item"
+ >
+ <div>
+ <div>{option.name}</div>
+ {
+ option.description ?
+ <div className={classes.description}>{option.description}</div>
+ : null
+ }
+ </div>
+ </MenuItem>))
+ }
</TextField>
</form>
);
-}
\ No newline at end of file
+}
diff --git a/static_src/react/issue-wizard/SubmitSuccessStep.tsx b/static_src/react/issue-wizard/SubmitSuccessStep.tsx
new file mode 100644
index 0000000..7654b7c
--- /dev/null
+++ b/static_src/react/issue-wizard/SubmitSuccessStep.tsx
@@ -0,0 +1,31 @@
+ // 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} from '@material-ui/styles';
+
+const userStyles = makeStyles({
+ content: {
+ fontSize: '15px',
+ marginBottom: '15px',
+ }
+});
+
+type Props = {
+ issueID: string
+}
+export default function SubmitSuccessStep({issueID} : Props): React.ReactElement {
+ const classes = userStyles();
+ const issueLink = '/p/chromium/issues/detail?id=' + issueID;
+ return (
+ <>
+ <h1>Well done!</h1>
+ <div className={classes.content}>
+ <div>Your issue has successfully submitted! Thank you for your contribution to maintaining Chromium.</div>
+ <div>Click <a href={issueLink}>here</a> to see your filed bug.</div>
+ </div>
+ <img src='/static/images/dog.png'/>
+ </>
+ );
+}
diff --git a/static_src/react/mr-react-autocomplete.test.ts b/static_src/react/mr-react-autocomplete.test.ts
index 8553c36..21d6c9e 100644
--- a/static_src/react/mr-react-autocomplete.test.ts
+++ b/static_src/react/mr-react-autocomplete.test.ts
@@ -85,13 +85,23 @@
it('_options gets component names', () => {
element.vocabularyName = 'component';
element._components = new Map([
- ['Infra>UI', {docstring: 'Test docs'}],
- ['Bird>Penguin', {docstring: 'Test docs'}],
+ ['Infra>UI', {path: 'Infra>UI', docstring: 'Test docs'}],
+ ['Bird>Penguin', {path: 'Bird>Penguin', docstring: 'Test docs'}],
]);
assert.deepEqual(element._options(), ['Infra>UI', 'Bird>Penguin']);
});
+ it('_options does not get deprecated components', () => {
+ element.vocabularyName = 'component';
+ element._components = new Map([
+ ['Infra>UI>Deprecated', {path: 'Infra>UI>Deprecated', deprecated: true, docstring: 'Test docs'}],
+ ['Infra>UI>NotDeprecated', {path: 'Infra>UI>NotDeprecated', docstring: 'Test docs'}],
+ ]);
+
+ assert.deepEqual(element._options(), ['Infra>UI>NotDeprecated']);
+ });
+
it('_options gets label names', () => {
element.vocabularyName = 'label';
element._labels = new Map([
diff --git a/static_src/react/mr-react-autocomplete.tsx b/static_src/react/mr-react-autocomplete.tsx
index 65a045f..c9a17ff 100644
--- a/static_src/react/mr-react-autocomplete.tsx
+++ b/static_src/react/mr-react-autocomplete.tsx
@@ -153,7 +153,7 @@
_options(): string[] {
switch (this.vocabularyName) {
case 'component': {
- return [...this._components.keys()];
+ return [...this._components.values()].filter((c) => !c.deprecated).map((c) => c.path);
} case 'label': {
// The label map keys are lowercase. Use the LabelDef label name instead.
return [...this._labels.values()].map((labelDef: LabelDef) => labelDef.label);
diff --git a/static_src/react/tests/AttachmentUploader.test.tsx b/static_src/react/tests/AttachmentUploader.test.tsx
new file mode 100644
index 0000000..344822e
--- /dev/null
+++ b/static_src/react/tests/AttachmentUploader.test.tsx
@@ -0,0 +1,52 @@
+// 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 {assert} from 'chai';
+import {cleanup, render} from '@testing-library/react';
+import AttachmentUploader from 'react/issue-wizard/AttachmentUploader.tsx';
+
+describe('IssueWizard Attachment Uploader', () => {
+ afterEach(cleanup);
+
+ it('render', () => {
+ render(<AttachmentUploader files={[]} setFiles={()=>{}}/>)
+ const uploadButton = document.getElementById('file-uploader');
+ assert.isNotNull(uploadButton);
+ });
+
+ it('render files name', () => {
+ const files = [
+ {name: '1.txt'},
+ {name: '2.txt'},
+ {name: '3.txt'},
+ ];
+ render(<AttachmentUploader files={files} setFiles={()=>{}}/>)
+ const items = document.querySelectorAll('li');
+ assert.equal(items.length, 3);
+
+ assert.include(items[0].textContent, '1.txt');
+ assert.include(items[1].textContent, '2.txt');
+ assert.include(items[2].textContent, '3.txt');
+ });
+
+ it('remove files', () => {
+ let files = [
+ {name: '1.txt'},
+ {name: '2.txt'},
+ {name: '3.txt'},
+ ];
+ render(<AttachmentUploader files={files} setFiles={(f: Array<any>)=>{files = f;}} setSubmitEnable={()=>{}}/>)
+ const items = document.querySelectorAll('li');
+ assert.equal(items.length, 3);
+
+ const removeButton = items[1].querySelector('button');
+ assert.isNotNull(removeButton);
+
+ removeButton?.click();
+ assert.equal(files.length, 2);
+ assert.equal(files[0].name, '1.txt');
+ assert.equal(files[1].name, '3.txt');
+ })
+});
diff --git a/static_src/react/tests/ConfirmBackModal.test.tsx b/static_src/react/tests/ConfirmBackModal.test.tsx
new file mode 100644
index 0000000..94856d4
--- /dev/null
+++ b/static_src/react/tests/ConfirmBackModal.test.tsx
@@ -0,0 +1,19 @@
+// Copyright 2022 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 {assert} from 'chai';
+import {cleanup, render} from '@testing-library/react';
+import {ConfirmBackModal} from 'react/issue-wizard/ConfirmBackModal.tsx';
+
+describe('IssueWizard confirm back modal', () => {
+
+ afterEach(cleanup);
+
+ it('render', () => {
+ render(<ConfirmBackModal enable={true} setEnable={()=>{}} confirmBack={()=>{}}/>);
+ const buttons = document.querySelectorAll('Button');
+ assert.equal(2, buttons.length);
+ });
+});
diff --git a/static_src/react/tests/CustomQuestionsStep.test.tsx b/static_src/react/tests/CustomQuestionsStep.test.tsx
new file mode 100644
index 0000000..0b43dee
--- /dev/null
+++ b/static_src/react/tests/CustomQuestionsStep.test.tsx
@@ -0,0 +1,39 @@
+// 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 {assert} from 'chai';
+import {render, cleanup} from '@testing-library/react';
+import CustomQuestionsStep from 'react/issue-wizard/CustomQuestionsStep.tsx';
+import {CustomQuestionType} from 'react/issue-wizard/IssueWizardTypes.tsx';
+
+describe('IssueWizard CustomQuestionsStep', () => {
+ afterEach(cleanup);
+ it('renders', async () => {
+ render(<CustomQuestionsStep questions={[]}/>);
+ const stepper = document.getElementById("mobile-stepper")
+
+ assert.isNotNull(stepper);
+ });
+
+ it('render InputType Question', async () => {
+ const questionList = [{
+ type: CustomQuestionType.Input,
+ question: "this is a test",
+ }]
+ const {container} = render(<CustomQuestionsStep questions={questionList}/>);
+ const input = container.querySelector('input');
+ assert.isNotNull(input);
+ })
+
+ it('render TextType Question', async () => {
+ const questionList = [{
+ type: CustomQuestionType.Text,
+ question: "this is a test",
+ }]
+ const {container} = render(<CustomQuestionsStep questions={questionList}/>);
+ const input = container.querySelector('textarea');
+ assert.isNotNull(input);
+ })
+});
diff --git a/static_src/react/tests/IssueWizardDescriptionUtils.test.tsx b/static_src/react/tests/IssueWizardDescriptionUtils.test.tsx
new file mode 100644
index 0000000..6e5edae
--- /dev/null
+++ b/static_src/react/tests/IssueWizardDescriptionUtils.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 {assert, expect} from 'chai';
+import {expandDescriptions} from 'react/issue-wizard/IssueWizardDescriptionsUtils.tsx';
+
+describe('IssueWizardDescriptionsUtils', () => {
+ it('get expandDescription and labels', () => {
+ const {expandDescription, expandLabels} = expandDescriptions(
+ 'Network / Downloading',
+ ['test url'],
+ false,
+ 'test',
+ [],
+ )
+ assert.equal(expandLabels.length, 1);
+ expect(expandDescription).to.contain("test url");
+ });
+
+ it('get proper component value base on user answer', () => {
+ const {expandDescription, expandLabels, compVal} = expandDescriptions(
+ 'Content',
+ ['test url', 'LABELS: Yes - this is'],
+ false,
+ 'test',
+ [],
+ )
+ assert.equal(expandLabels.length, 1);
+ assert.equal(expandLabels[0].label, 'Type-Bug');
+ assert.equal(compVal, 'Blink');
+ expect(expandDescription).to.contain("test url");
+ });
+});
diff --git a/static_src/react/tests/IssueWizardUtils.test.tsx b/static_src/react/tests/IssueWizardUtils.test.tsx
new file mode 100644
index 0000000..4211160
--- /dev/null
+++ b/static_src/react/tests/IssueWizardUtils.test.tsx
@@ -0,0 +1,74 @@
+// 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 {assert, expect} from 'chai';
+import {IssueWizardPersona, IssueCategory, CustomQuestionType} from '../issue-wizard/IssueWizardTypes.tsx';
+import {GetCategoriesByPersona, GetQuestionsByCategory, buildIssueDescription, getChromeVersion} from '../issue-wizard/IssueWizardUtils.tsx';
+
+describe('IssueWizardUtils', () => {
+ it('generate the issue categories to user persona map', () => {
+ const categories: IssueCategory[]= [
+ {
+ name: 't1',
+ description: 'd1',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ },
+ {
+ name: 't2',
+ description: 'd2',
+ persona: IssueWizardPersona.EndUser,
+ enabled: false,
+ },
+ ];
+
+ const categoriesByPersonaMap = GetCategoriesByPersona(categories);
+ const validCategories = categoriesByPersonaMap.get(IssueWizardPersona.EndUser);
+
+ assert.equal(validCategories?.length, 1);
+ assert.equal(validCategories[0].name, 't1');
+ assert.equal(validCategories[0].description, 'd1');
+ });
+
+ it('generate custom questions to issue categories map', () => {
+ const categories: IssueCategory[]= [
+ {
+ name: 't1',
+ description: 'd1',
+ persona: IssueWizardPersona.EndUser,
+ enabled: true,
+ customQuestions: [
+ {
+ type: CustomQuestionType.Text,
+ question: 'q1',
+ }
+ ]
+ },
+ ];
+
+ const questionsByCategoryMap = GetQuestionsByCategory(categories);
+ const questions = questionsByCategoryMap.get('t1');
+
+ assert.equal(questions?.length, 1);
+ assert.equal(questions[0].question, 'q1');
+ });
+
+ it('create issue description', () => {
+ const description = buildIssueDescription('reproduce', 'description', 'comments', 'Mac', 'Chrome');
+ expect(description).to.contains('Steps to reproduce the problem:');
+ expect(description).to.contains('Problem Description:');
+ expect(description).to.contains('Additional Comments:');
+ });
+
+ it('test the chrome version regex match', () => {
+ const navigatorMock = {
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36'
+ };
+ Object.defineProperty(window, 'navigator', {
+ value: navigatorMock
+ });
+ const chrome_version = getChromeVersion();
+ assert(chrome_version, '98.0.4758.109');
+ });
+});