fix(docs,asinput): semantic-release & jest docs; asInput a11y fix

docs(readme): add semantic-release documentation

docs(readme): add semantic-release notes to documentation

docs(readme): add cli gif link

docs(readme): fix some formatting

docs(readme): add more background information

fix(asinput): Accessibility fixes for asInput (#97)

* fix(asinput): Add screen-reader text for danger icon

* fix(asinput): Load form-control-feedback on page load w/ aria-live

fix(readme): Document how to run jest tests in Chrome DevTools (#96)

Also adds an npm run test-debug script.
This commit is contained in:
jaebradley 2017-12-11 21:12:43 -05:00 committed by Ari Rizzitano
parent 213b4faed1
commit 7bc95c139f
9 changed files with 459 additions and 31 deletions

View File

@ -30,6 +30,13 @@ exports[`Storyshots CheckBox basic usage 1`] = `
>
check me out!
</label>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput1"
>
<span />
</div>
</div>
`;
@ -51,6 +58,13 @@ exports[`Storyshots CheckBox call a function 1`] = `
>
check out the console
</label>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput1"
>
<span />
</div>
</div>
`;
@ -72,6 +86,13 @@ exports[`Storyshots CheckBox default checked 1`] = `
>
(un)check me out
</label>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput1"
>
<span />
</div>
</div>
`;
@ -93,6 +114,13 @@ exports[`Storyshots CheckBox disabled 1`] = `
>
you cannot check me out
</label>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput1"
>
<span />
</div>
</div>
`;
@ -204,7 +232,7 @@ exports[`Storyshots InputSelect basic usage 1`] = `
Fruits
</label>
<select
aria-describedby={undefined}
aria-describedby="error-asInput2"
className="form-control"
id="asInput2"
name="fruits"
@ -234,6 +262,13 @@ exports[`Storyshots InputSelect basic usage 1`] = `
banana
</option>
</select>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput2"
>
<span />
</div>
</div>
`;
@ -248,7 +283,7 @@ exports[`Storyshots InputSelect separate labels and values 1`] = `
New England States
</label>
<select
aria-describedby={undefined}
aria-describedby="error-asInput2"
className="form-control"
id="asInput2"
name="new-england-states"
@ -288,6 +323,13 @@ exports[`Storyshots InputSelect separate labels and values 1`] = `
Vermont
</option>
</select>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput2"
>
<span />
</div>
</div>
`;
@ -302,7 +344,7 @@ exports[`Storyshots InputSelect separate option groups 1`] = `
Northeast States
</label>
<select
aria-describedby={undefined}
aria-describedby="error-asInput2"
className="form-control"
id="asInput2"
name="northeast-states"
@ -390,6 +432,13 @@ exports[`Storyshots InputSelect separate option groups 1`] = `
</option>
</optgroup>
</select>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput2"
>
<span />
</div>
</div>
`;
@ -404,7 +453,7 @@ exports[`Storyshots InputSelect with validation 1`] = `
Favorite Color
</label>
<select
aria-describedby={undefined}
aria-describedby="error-asInput2"
className="form-control"
id="asInput2"
name="color"
@ -449,6 +498,13 @@ exports[`Storyshots InputSelect with validation 1`] = `
purple
</option>
</select>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput2"
>
<span />
</div>
</div>
`;
@ -489,7 +545,7 @@ exports[`Storyshots InputText focus test 1`] = `
Data Input
</label>
<input
aria-describedby={undefined}
aria-describedby="error-data"
aria-invalid={false}
className="form-control"
disabled={false}
@ -503,6 +559,13 @@ exports[`Storyshots InputText focus test 1`] = `
type="text"
value=""
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-data"
>
<span />
</div>
</div>
</div>
`;
@ -518,7 +581,7 @@ exports[`Storyshots InputText minimal usage 1`] = `
First Name
</label>
<input
aria-describedby={undefined}
aria-describedby="error-asInput3"
aria-invalid={false}
className="form-control"
disabled={false}
@ -532,6 +595,13 @@ exports[`Storyshots InputText minimal usage 1`] = `
type="text"
value="Foo Bar"
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput3"
>
<span />
</div>
</div>
`;
@ -546,7 +616,7 @@ exports[`Storyshots InputText validation 1`] = `
Username
</label>
<input
aria-describedby="undefined description-username"
aria-describedby="error-username description-username"
aria-invalid={false}
className="form-control"
disabled={false}
@ -560,6 +630,13 @@ exports[`Storyshots InputText validation 1`] = `
type="text"
value=""
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-username"
>
<span />
</div>
<small
className="form-text"
id="description-username"
@ -580,7 +657,7 @@ exports[`Storyshots InputText validation with danger theme 1`] = `
Username
</label>
<input
aria-describedby="undefined description-asInput3"
aria-describedby="error-asInput3 description-asInput3"
aria-invalid={false}
className="form-control"
disabled={false}
@ -598,6 +675,13 @@ exports[`Storyshots InputText validation with danger theme 1`] = `
type="text"
value=""
/>
<div
aria-live="polite"
className="form-control-feedback invalid-feedback"
id="error-asInput3"
>
<span />
</div>
<small
className="form-text"
id="description-asInput3"
@ -1089,7 +1173,7 @@ exports[`Storyshots Modal modal with element body 1`] = `
E-Mail Address
</label>
<input
aria-describedby={undefined}
aria-describedby="error-asInput3"
aria-invalid={false}
className="form-control"
disabled={false}
@ -1103,6 +1187,13 @@ exports[`Storyshots Modal modal with element body 1`] = `
type="text"
value=""
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput3"
>
<span />
</div>
</div>
<button
className="btn"
@ -2076,7 +2167,7 @@ exports[`Storyshots Textarea minimal usage 1`] = `
First Name
</label>
<textarea
aria-describedby={undefined}
aria-describedby="error-asInput4"
aria-invalid={false}
className="form-control"
disabled={false}
@ -2093,6 +2184,13 @@ exports[`Storyshots Textarea minimal usage 1`] = `
}
value="Foo Bar"
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput4"
>
<span />
</div>
</div>
`;
@ -2107,7 +2205,7 @@ exports[`Storyshots Textarea scrollable 1`] = `
Information
</label>
<textarea
aria-describedby={undefined}
aria-describedby="error-asInput4"
aria-invalid={false}
className="form-control"
disabled={false}
@ -2124,6 +2222,13 @@ exports[`Storyshots Textarea scrollable 1`] = `
}
value="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput4"
>
<span />
</div>
</div>
`;
@ -2138,7 +2243,7 @@ exports[`Storyshots Textarea validation 1`] = `
Username
</label>
<textarea
aria-describedby="undefined description-asInput4"
aria-describedby="error-asInput4 description-asInput4"
aria-invalid={false}
className="form-control"
disabled={false}
@ -2155,6 +2260,13 @@ exports[`Storyshots Textarea validation 1`] = `
}
value=""
/>
<div
aria-live="polite"
className="form-control-feedback"
id="error-asInput4"
>
<span />
</div>
<small
className="form-text"
id="description-asInput4"

View File

@ -73,6 +73,18 @@ npm run test
To add unit tests for a component, create a file in your component's directory named `<ComponentName>.test.js`. Jest will automatically pick up this file and run the tests as part of the suite. Take a look at [Dropdown.test.jsx](https://github.com/edx/paragon/blob/master/src/Dropdown/Dropdown.test.jsx) or [CheckBox.test.jsx](https://github.com/edx/paragon/blob/master/src/CheckBox/CheckBox.test.jsx) for examples of good component unit tests.
#### Run Unit Tests in Chrome DevTools Inspector
To run the unit tests in the Chrome DevTools inspector, run:
```
npm run debug-test
```
Then, open `chrome://inspect` in your Chrome browser and select the "node_modules/.bin/jest" target to open the Chrome DevTools. You can set breakpoints in Chrome DevTools or insert a `debugger;` statement into the code to pause execution at that point.
![Screenshot of Chrome on the chrome://inspect page](docs/inspect-chrome-jest.png)
### Snapshot Testing
Jest has built-in [snapshot testing](http://facebook.github.io/jest/docs/en/snapshot-testing.html#snapshot-testing-with-jest) functionality which serves as a good means of smoketesting components to ensure they render in a predictable way. Paragon's Jest snapshots are automatically generated from components' Storybook stories using the [Storyshots addon](https://github.com/storybooks/storybook/blob/4b6a93acfbaf044d85dd8ee7a7671239ea1ba01d/addons/storyshots/README.md) -- pretty cool, huh?
@ -88,3 +100,75 @@ If the snapshot tests fail, it's generally pretty easy to tell whether it's happ
### Coverage
Paragon measures code coverage using Jest's built-in `--coverage` flag (which I believe uses istanbul under the hood) and report it via [Coveralls](https://coveralls.io/github/edx/paragon). Shoot for 100% test coverage on your PRs, but use your best judgment if you're really struggling to cover those last few lines. At the very least, don't *reduce* total coverage. Coveralls will fail your build if your PR reduces coverage.
## Semantic Release
Paragon uses the [`semantic-release` package](https://github.com/semantic-release/semantic-release) to automate its release process (creating Git tags, creating GitHub releases, and publishing to NPM).
### Commit Messages
[`semantic-release` analyzes commit messages to determine whether to create a `major`, `minor`, or `patch` release](https://github.com/semantic-release/semantic-release#default-commit-message-format) (or to skip a release).
Paragon currently uses [the default commit analyzer release rules](https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js#L8-L11) which means that there are **4** commit types that will trigger a release:
1. `feat` (`minor` release)
2. `fix` (`patch` release)
3. `perf` (`patch` release)
4. `breaking` (`major` release)
#### `Angular` Commit Message Convention
Paragon currently uses [the `Angular` commit message convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153). As documented in the previously linked gist, a commit that follows the `Angular` commit message convention has four parts:
1. A `type` - is this commit a `feat`, `fix`, `chore`, `docs`, etc.? There is a set of `type` values to choose from, but again, **only the `feat`, `fix`, `perf`, and `breaking`** `type` values will trigger a release.
2. A `scope` - what is this commit impacting? Did you fix a bug in the `Hyperlink` component? Did you add a new feature to the `RadioButtonGroup` component? Currently, the `scope` must be lower-case and `-` separated though switching this to being `camelCase` is currently being investigated.
3. A `subject` - provide a short description of _what_ your change is.
* use imperative, present tense language (so `change` not `changed` or `changes`)
* don't capitalize the first letter
* don't add a period (`.`) at the end
* there is a 50 character limit
4. A `body` (optional) - add more detail about your change
5. A `footer` (optional)
* > All breaking changes have to be mentioned in footer with the description of the change, justification and migration notes - [`Angular` commit message specification](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#breaking-changes)
##### Examples
This will lead to a patch version bump
> fix(someComponent): fix tab accessibility issue in someComponent
This will lead to a minor version bump
> feat(newComponent): add newComponent`
This will lead to a patch version bump - note the body
>fix(anotherComponent): fix escape key accessibility issue
>
> Unable to clear anotherComponent's popup using escape key.
> By updating the onKeyPress event handler to close popup window on escape key press, accessibility issue was resolved.
#### Using `commitlint` in Paragon
Paragon uses the [`commitlint`](https://github.com/marionebl/commitlint) package to lint commit messages. Paragon currently has [a `commit message` hook](https://github.com/edx/paragon/blob/master/package.json#L20) that runs `commitlint`'s, well, commit linting, on the given commit message using the configuration specified in [`commitlint.config.js`](https://github.com/edx/paragon/blob/master/commitlint.config.js). If a commit message fails linting, the commit associated with that message does not end up getting committed.
`commitlint` also comes with [a helpful CLI](https://github.com/marionebl/commitlint/tree/master/@commitlint/cli) that walks one through the entire semantic commit process. This CLI can be triggered by running the `npm run gc` command.
![example-commitlint](https://media.giphy.com/media/3ohs7H8y9Qi7HOHWko/giphy.gif)
As noted above, the only required fields are **`type`, `scope`, and `subject`**. The `body` and `footer` can be skipped through the CLI by typing `:skip`.
### Pull Requests
Since all pull requests should be `squashed` / `rebased` down to a single semantically-formatted commit, it is recommended that **all pull requests have a title that matches the commit message**. This way, whether you're merging a pull request, squashing and merging a pull request, or rebasing and merging a pull request, the correct commit message will be analyzed by `semantic-release`.
### Merging, Building, and Releasing
> When `semantic-release` is set up it will do that after every successful continuous integration build of your master branch (or any other branch you specify) and publish the new version for you
> - [`semantic-release` README](https://github.com/semantic-release/semantic-release)
Release-related activity only happens
1. After a successful Travis build on `master` ([the `semantic-release` NPM command is only executed as part of the `after_success` Travis hook](https://github.com/edx/paragon/blob/master/.travis.yml#L21-L22).)
2. If commit(s) exist with a format that would trigger a release
If a release occurs, a new GitHub release should be found with relevant notes that are parsed from commits associated with that release.
So for the following release
![example-release](https://imgur.com/RqFG7ND.png)
the commit message for that release was `fix(asinput): Override state value when props value changes`, and a new `patch` release was created with release notes from the commit message.
After a GitHub release is created, the package is then published to NPM.

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

193
package-lock.json generated
View File

@ -8376,6 +8376,199 @@
"throat": "4.1.0"
}
},
"jest-cli": {
"version": "21.2.1",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-21.2.1.tgz",
"integrity": "sha512-T1BzrbFxDIW/LLYQqVfo94y/hhaj1NzVQkZgBumAC+sxbjMROI7VkihOdxNR758iYbQykL2ZOWUBurFgkQrzdg==",
"dev": true,
"requires": {
"ansi-escapes": "3.0.0",
"chalk": "2.3.0",
"glob": "7.1.2",
"graceful-fs": "4.1.11",
"is-ci": "1.0.10",
"istanbul-api": "1.2.1",
"istanbul-lib-coverage": "1.1.1",
"istanbul-lib-instrument": "1.9.1",
"istanbul-lib-source-maps": "1.2.2",
"jest-changed-files": "21.2.0",
"jest-config": "21.2.1",
"jest-environment-jsdom": "21.2.1",
"jest-haste-map": "21.2.0",
"jest-message-util": "21.2.1",
"jest-regex-util": "21.2.0",
"jest-resolve-dependencies": "21.2.0",
"jest-runner": "21.2.1",
"jest-runtime": "21.2.1",
"jest-snapshot": "21.2.1",
"jest-util": "21.2.1",
"micromatch": "2.3.11",
"node-notifier": "5.1.2",
"pify": "3.0.0",
"slash": "1.0.0",
"string-length": "2.0.0",
"strip-ansi": "4.0.0",
"which": "1.3.0",
"worker-farm": "1.5.2",
"yargs": "9.0.1"
},
"dependencies": {
"ansi-escapes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz",
"integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==",
"dev": true
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
"wrap-ansi": "2.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
}
}
},
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
"dev": true,
"requires": {
"graceful-fs": "4.1.11",
"parse-json": "2.2.0",
"pify": "2.3.0",
"strip-bom": "3.0.0"
},
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
}
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
"error-ex": "1.3.1"
}
},
"path-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
"integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
"dev": true,
"requires": {
"pify": "2.3.0"
},
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
"integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
"dev": true,
"requires": {
"load-json-file": "2.0.0",
"normalize-package-data": "2.4.0",
"path-type": "2.0.0"
}
},
"read-pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
"integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
"dev": true,
"requires": {
"find-up": "2.1.0",
"read-pkg": "2.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "3.0.0"
}
},
"yargs": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz",
"integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=",
"dev": true,
"requires": {
"camelcase": "4.1.0",
"cliui": "3.2.0",
"decamelize": "1.2.0",
"get-caller-file": "1.0.2",
"os-locale": "2.1.0",
"read-pkg-up": "2.0.0",
"require-directory": "2.1.1",
"require-main-filename": "1.0.1",
"set-blocking": "2.0.0",
"string-width": "2.1.1",
"which-module": "2.0.0",
"y18n": "3.2.1",
"yargs-parser": "7.0.0"
}
}
}
},
"jest-config": {
"version": "21.2.1",
"resolved": "https://registry.npmjs.org/jest-config/-/jest-config-21.2.1.tgz",

View File

@ -13,6 +13,7 @@
"build": "NODE_ENV=production webpack",
"build-storybook": "build-storybook",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand --coverage",
"deploy-storybook": "storybook-to-ghpages",
"gc": "commit",
"lint": "eslint --ext .js --ext .jsx .",
@ -69,6 +70,7 @@
"husky": "^0.14.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^21.0.1",
"jest-cli": "^21.2.1",
"node-sass": "^4.5.3",
"postcss-scss": "^1.0.1",
"react-router-dom": "^4.1.1",

View File

@ -76,6 +76,7 @@ storiesOf('InputText', module)
feedback = {
isValid: false,
validationMessage: 'Username must be at least 3 characters in length.',
dangerIconDescription: 'Error',
};
}
return feedback;

View File

@ -1,5 +1,6 @@
@import "~bootstrap/scss/_forms";
@import "~bootstrap/scss/mixins/_forms";
@import "~bootstrap/scss/utilities/_screenreaders.scss";
.fa-icon-spacing {
padding: 0px 5px 0px 0px;

View File

@ -150,6 +150,7 @@ describe('asInput()', () => {
};
wrapper = mount(<InputTestComponent {...props} />);
});
it('without theme', () => {
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
@ -157,19 +158,32 @@ describe('asInput()', () => {
expect(err.exists()).toEqual(true);
expect(err.text()).toEqual(validationResult.validationMessage);
});
it('with danger theme', () => {
wrapper.setProps({ themes: ['danger'] });
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
validationResult.dangerIconDescription = 'Error';
// error div exists on the page when form is loaded
const err = wrapper.find('.form-control-feedback');
expect(err.exists()).toEqual(true);
expect(err.text()).toEqual(validationResult.validationMessage);
expect(err.hasClass('invalid-feedback')).toEqual(true);
expect(err.prop('aria-live')).toEqual('polite');
expect(err.text()).toEqual('');
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
expect(err.exists()).toEqual(true);
expect(err.text()).toEqual(validationResult.dangerIconDescription +
validationResult.validationMessage);
const dangerIcon = wrapper.find('.fa-exclamation-circle');
expect(dangerIcon.exists()).toEqual(true);
expect(dangerIcon.hasClass('fa')).toEqual(true);
const dangerIconDescription = wrapper.find('.sr-only');
expect(dangerIconDescription.exists()).toEqual(true);
expect(dangerIconDescription.text()).toEqual(validationResult.dangerIconDescription);
const inputElement = wrapper.find('.form-control');
expect(inputElement.hasClass('is-invalid')).toEqual(true);
});

View File

@ -60,24 +60,45 @@ const asInput = (WrappedComponent, labelFirst = true) => {
const descriptionId = `description-${this.state.id}`;
const desc = {};
if (!this.state.isValid) {
const hasDangerTheme = this.hasDangerTheme();
desc.error = (
<div className={classNames(styles['form-control-feedback'], { [styles['invalid-feedback']]: hasDangerTheme })} id={errorId} key="0">
{ hasDangerTheme &&
<div
className={classNames(
styles['form-control-feedback'],
{ [styles['invalid-feedback']]: hasDangerTheme },
)}
id={errorId}
key="0"
aria-live="polite"
>
{ this.state.isValid ? (
<span />
) : [
(hasDangerTheme &&
<span key="0">
<span
className={classNames(FontAwesomeStyles.fa, FontAwesomeStyles['fa-exclamation-circle'], styles['fa-icon-spacing'])}
className={classNames(
FontAwesomeStyles.fa,
FontAwesomeStyles['fa-exclamation-circle'],
styles['fa-icon-spacing'],
)}
aria-hidden
/>
}
<span>
{this.state.validationMessage}
<span
className={classNames(styles['sr-only'])}
>
{this.state.dangerIconDescription}
</span>
</span>
),
<span key="1">
{this.state.validationMessage}
</span>,
]}
</div>
);
desc.describedBy = errorId;
}
if (this.props.description) {
desc.description = (