├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── jest.config.js ├── other ├── CODE_OF_CONDUCT.md ├── EXAMPLES.md ├── MAINTAINING.md ├── ROADMAP.md ├── manual-releases.md ├── screenshot.png └── setup-test-framework.js ├── package.json └── src ├── __tests__ ├── __snapshots__ │ ├── dom-nodes.js.snap │ ├── example.js.snap │ ├── serializer.js.snap │ └── snapshot-diff.js.snap ├── dom-nodes.js ├── example.js ├── matchers.js ├── replace-selectors.js ├── serializer.js ├── snapshot-diff.js └── utils.js ├── matchers.js ├── replace-selectors.js ├── serializer.js └── utils.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "jest-glamor-react", 3 | "projectOwner": "kentcdodds", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "contributors": [ 11 | { 12 | "login": "MicheleBertoli", 13 | "name": "Michele Bertoli", 14 | "avatar_url": "https://avatars1.githubusercontent.com/u/1308971?v=3", 15 | "profile": "http://michele.berto.li", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "test" 20 | ] 21 | }, 22 | { 23 | "login": "kentcdodds", 24 | "name": "Kent C. Dodds", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 26 | "profile": "https://kentcdodds.com", 27 | "contributions": [ 28 | "code", 29 | "doc", 30 | "infra", 31 | "test" 32 | ] 33 | }, 34 | { 35 | "login": "mitchellhamilton", 36 | "name": "Mitchell Hamilton", 37 | "avatar_url": "https://avatars2.githubusercontent.com/u/11481355?v=3", 38 | "profile": "https://hamil.town", 39 | "contributions": [ 40 | "code", 41 | "doc", 42 | "test" 43 | ] 44 | }, 45 | { 46 | "login": "jhurley23", 47 | "name": "jhurley23", 48 | "avatar_url": "https://avatars2.githubusercontent.com/u/11878516?v=3", 49 | "profile": "https://github.com/jhurley23", 50 | "contributions": [ 51 | "code", 52 | "test", 53 | "doc" 54 | ] 55 | }, 56 | { 57 | "login": "megaurav2002", 58 | "name": "Gaurav Talwar", 59 | "avatar_url": "https://avatars0.githubusercontent.com/u/27758243?v=4", 60 | "profile": "https://github.com/megaurav2002", 61 | "contributions": [] 62 | }, 63 | { 64 | "login": "hjylewis", 65 | "name": "Henry Lewis", 66 | "avatar_url": "https://avatars3.githubusercontent.com/u/6494737?v=4", 67 | "profile": "http://hjylewis.com/", 68 | "contributions": [ 69 | "bug", 70 | "code" 71 | ] 72 | }, 73 | { 74 | "login": "asvetliakov", 75 | "name": "Alexey Svetliakov", 76 | "avatar_url": "https://avatars2.githubusercontent.com/u/8881674?v=4", 77 | "profile": "https://github.com/asvetliakov", 78 | "contributions": [ 79 | "code", 80 | "test" 81 | ] 82 | }, 83 | { 84 | "login": "jameswlane", 85 | "name": "James W Lane", 86 | "avatar_url": "https://avatars2.githubusercontent.com/u/794161?v=4", 87 | "profile": "http://jameswlane.com", 88 | "contributions": [ 89 | "bug", 90 | "code", 91 | "test" 92 | ] 93 | }, 94 | { 95 | "login": "brentertz", 96 | "name": "Brent Ertz", 97 | "avatar_url": "https://avatars1.githubusercontent.com/u/202773?v=4", 98 | "profile": "https://github.com/brentertz", 99 | "contributions": [ 100 | "doc" 101 | ] 102 | }, 103 | { 104 | "login": "Ailrun", 105 | "name": "Junyoung Clare Jang", 106 | "avatar_url": "https://avatars3.githubusercontent.com/u/12473268?v=4", 107 | "profile": "http://ailrun.github.io/", 108 | "contributions": [ 109 | "code", 110 | "test" 111 | ] 112 | } 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | * `jest-glamor-react` version: 15 | * `node` version: 16 | * `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | ``` 22 | 23 | What you did: 24 | 25 | What happened: 26 | 27 | 28 | 29 | Reproduction repository: 30 | 31 | 35 | 36 | Problem description: 37 | 38 | Suggested solution: 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | .DS_Store 7 | .eslintcache 8 | 9 | # these cause more harm than good 10 | # when working with contributors 11 | package-lock.json 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: false, 9 | jsxBracketSameLine: false, 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - ~/.npm 6 | notifications: 7 | email: false 8 | node_js: '8' 9 | install: npm install 10 | script: npm run validate 11 | after_success: kcd-scripts travis-after-success 12 | branches: 13 | only: master 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release). 4 | You can see it on the [releases page](../../releases). 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ series 6 | [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. `$ npm install` to install dependencies 12 | 3. `$ npm start validate` to validate you've got it working 13 | 4. Create a branch for your PR 14 | 15 | This project uses [`p-s`][p-s] and you can run `npm start` to see what scripts are available. 16 | 17 | ## Add yourself as a contributor 18 | 19 | This project follows the [all contributors][all-contributors] specification. To add yourself to the table of 20 | contributors on the README.md, please use the automated script as part of your PR: 21 | 22 | ```console 23 | npm start "contrib.add " 24 | ``` 25 | 26 | Follow the prompt. If you've already added yourself to the list and are making a new type of contribution, you can run 27 | it again and select the added contribution type. 28 | 29 | ## Committing and Pushing changes 30 | 31 | This project uses [`semantic-release`][semantic-release] to do automatic releases and generate a changelog based on the 32 | commit history. So we follow [a convention][convention] for commit messages. Please follow this convention for your 33 | commit messages. 34 | 35 | You can use `commitizen` to help you to follow [the convention][convention] 36 | 37 | Once you are ready to commit the changes, please use the below commands 38 | 39 | 1. `git add ` 40 | 2. `$ npm start commit` 41 | 42 | ... and follow the instruction of the interactive prompt. 43 | 44 | ### opt into git hooks 45 | 46 | There are git hooks set up with this project that are automatically installed when you install dependencies. They're 47 | really handy, but are turned off by default (so as to not hinder new contributors). You can opt into these by creating 48 | a file called `.opt-in` at the root of the project and putting this inside: 49 | 50 | ``` 51 | commit-msg 52 | pre-commit 53 | ``` 54 | 55 | ## Help needed 56 | 57 | Please checkout the [ROADMAP.md][roadmap] and raise an issue to discuss 58 | any of the items in the want to do or might do list. 59 | 60 | Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks! 61 | 62 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 63 | [semantic-release]: https://npmjs.com/package/semantic-release 64 | [convention]: https://github.com/conventional-changelog/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 65 | [all-contributors]: https://github.com/kentcdodds/all-contributors 66 | [roadmap]: ./other/ROADMAP.md 67 | [p-s]: https://npmjs.com/package/p-s 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Michele Bertoli, Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-glamor-react 2 | 3 | Jest utilities for Glamor and React 4 | 5 | [![Build Status][build-badge]][build] 6 | [![Code Coverage][coverage-badge]][coverage] 7 | [![version][version-badge]][package] 8 | [![downloads][downloads-badge]][npm-stat] 9 | [![MIT License][license-badge]][license] 10 | 11 | [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) 12 | [![PRs Welcome][prs-badge]][prs] 13 | [![Donate][donate-badge]][donate] 14 | [![Code of Conduct][coc-badge]][coc] 15 | [![Roadmap][roadmap-badge]][roadmap] 16 | [![Examples][examples-badge]][examples] 17 | 18 | [![Watch on GitHub][github-watch-badge]][github-watch] 19 | [![Star on GitHub][github-star-badge]][github-star] 20 | [![Tweet][twitter-badge]][twitter] 21 | 22 | Sponsor 23 | 24 | ## The problem 25 | 26 | If you use [`glamor`][glamor] as your CSS-in-JS solution, and you use 27 | [snapshot testing][snapshot] with [jest][jest] then you probably have some test 28 | snapshots that look like: 29 | 30 | ```html 31 |

34 | Hello World 35 |

36 | ``` 37 | 38 | And that's not super helpful from a styling perspective. Especially when there 39 | are changes to the class, you can see that it changed, but you have to look 40 | through the code to know _what_ caused the class name to change. 41 | 42 | ## This solution 43 | 44 | This allows your snapshots to look more like: 45 | 46 | ```html 47 | .css-0, 48 | [data-css-0] { 49 | font-size: 1.5em; 50 | text-align: center; 51 | color: palevioletred; 52 | } 53 | 54 |

57 | Hello World 58 |

59 | ``` 60 | 61 | This is much more helpful because now you can see the CSS applied and over time 62 | it becomes even more helpful to see how that changes over time. 63 | 64 | This builds on the work from [@MicheleBertoli][michelebertoli] in 65 | [`jest-styled-components`][jest-styled-components] to bring a similar experience 66 | to React projects that use [`glamor`][glamor]. 67 | 68 | ## Table of Contents 69 | 70 | 71 | 72 | 73 | 74 | * [Preview](#preview) 75 | * [Installation](#installation) 76 | * [Usage](#usage) 77 | * [Custom matchers](#custom-matchers) 78 | * [toHaveStyleRule(property, value)](#tohavestyleruleproperty-value) 79 | * [Integration with snapshot-diff](#integration-with-snapshot-diff) 80 | * [Inspiration](#inspiration) 81 | * [Other Solutions](#other-solutions) 82 | * [Contributors](#contributors) 83 | * [LICENSE](#license) 84 | 85 | 86 | 87 | ### Preview 88 | 89 | Terminal Screenshot 95 | 96 | ## Installation 97 | 98 | This module is distributed via [npm][npm] which is bundled with [node][node] and 99 | should be installed as one of your project's `devDependencies`: 100 | 101 | ``` 102 | npm install --save-dev jest-glamor-react 103 | ``` 104 | 105 | ## Usage 106 | 107 | At the top of your test file: 108 | 109 | ```javascript 110 | import serializer from 'jest-glamor-react' 111 | 112 | expect.addSnapshotSerializer(serializer) 113 | ``` 114 | 115 | Or in your Jest serializer config: 116 | 117 | ```javascript 118 | { 119 | "snapshotSerializers": [ 120 | "jest-glamor-react" 121 | ] 122 | } 123 | ``` 124 | 125 | If you have set jest.config variable `"testEnvironment": "node"`, you will need to manually mock up browser gloabl objects so it is recommended to use `"testEnvironment": "jsdom"` instead. 126 | 127 | Here are some components: 128 | 129 | ```javascript 130 | import React from 'react' 131 | import * as glamor from 'glamor' 132 | 133 | function Wrapper(props) { 134 | const className = glamor.css({ 135 | padding: '4em', 136 | background: 'papayawhip', 137 | }) 138 | return
139 | } 140 | 141 | function Title(props) { 142 | const className = glamor.css({ 143 | fontSize: '1.5em', 144 | textAlign: 'center', 145 | color: 'palevioletred', 146 | }) 147 | return

148 | } 149 | ``` 150 | 151 | Here's how we'd test them with `ReactDOM.render`: 152 | 153 | ```javascript 154 | import React from 'react' 155 | import ReactDOM from 'react-dom' 156 | 157 | function render(ui) { 158 | const div = document.createElement('div') 159 | ReactDOM.render(ui, div) 160 | return div.children[0] 161 | } 162 | 163 | test('react-dom', () => { 164 | const node = render( 165 | 166 | Hello World, this is my first glamor styled component! 167 | , 168 | ) 169 | expect(node).toMatchSnapshot() 170 | }) 171 | ``` 172 | 173 | And here's how we'd test them with `react-test-renderer`: 174 | 175 | ```javascript 176 | import React from 'react' 177 | import renderer from 'react-test-renderer' 178 | 179 | test('react-test-renderer', () => { 180 | const tree = renderer 181 | .create( 182 | 183 | Hello World, this is my first glamor styled component! 184 | , 185 | ) 186 | .toJSON() 187 | 188 | expect(tree).toMatchSnapshot() 189 | }) 190 | ``` 191 | 192 | Works with enzyme too: 193 | 194 | ```javascript 195 | import * as enzyme from 'enzyme' 196 | import toJson from 'enzyme-to-json' 197 | 198 | test('enzyme', () => { 199 | const ui = ( 200 | 201 | Hello World, this is my first glamor styled component! 202 | 203 | ) 204 | 205 | expect(toJson(enzyme.shallow(ui))).toMatchSnapshot(`enzyme.shallow`) 206 | expect(toJson(enzyme.mount(ui))).toMatchSnapshot(`enzyme.mount`) 207 | expect(toJson(enzyme.render(ui))).toMatchSnapshot(`enzyme.render`) 208 | }) 209 | ``` 210 | 211 | ## Custom matchers 212 | 213 | ### toHaveStyleRule(property, value) 214 | 215 | `expect(node).toHaveStyleRule(property: string, value: string | RegExp)` 216 | 217 | #### Installation: 218 | 219 | ```javascript 220 | import serializer, {toHaveStyleRule} from 'jest-glamor-react' 221 | expect.addSnapshotSerializer(serializer) 222 | expect.extend({toHaveStyleRule}) 223 | ``` 224 | 225 | #### Usage: 226 | 227 | If we use the same examples as those above: 228 | 229 | ```javascript 230 | import React from 'react' 231 | import ReactDOM from 'react-dom' 232 | 233 | function render(ui) { 234 | const div = document.createElement('div') 235 | ReactDOM.render(ui, div) 236 | return div.children[0] 237 | } 238 | 239 | test('react-dom', () => { 240 | const node = render( 241 | 242 | Hello World, this is my first glamor styled component! 243 | , 244 | ) 245 | expect(node).toHaveStyleRule('background', 'papayawhip') 246 | }) 247 | ``` 248 | 249 | Or with `react-test-renderer`: 250 | 251 | ```javascript 252 | import React from 'react' 253 | import renderer from 'react-test-renderer' 254 | 255 | test('react-test-renderer', () => { 256 | const tree = renderer 257 | .create( 258 | 259 | Hello World, this is my first glamor styled component! 260 | , 261 | ) 262 | .toJSON() 263 | 264 | expect(tree).toHaveStyleRule('background', 'papayawhip') 265 | }) 266 | ``` 267 | 268 | Or using Enzyme: 269 | 270 | ```javascript 271 | import {mount} from 'enzyme' 272 | 273 | test('enzyme', () => { 274 | const wrapper = mount( 275 | 276 | Hello World, this is my first glamor styled component! 277 | , 278 | ) 279 | 280 | expect(wrapper).toHaveStyleRule('background', 'papayawhip') 281 | expect(wrapper.find(Title)).toHaveStyleRule('color', 'palevioletred') 282 | }) 283 | ``` 284 | 285 | ### Integration with snapshot-diff 286 | 287 | [`snapshot-diff`](https://github.com/jest-community/snapshot-diff) is this 288 | really neat project that can help you get more value out of your snapshots. 289 | As far as I know, this is the best example of how to integrate this serializer 290 | with that handy matcher: 291 | 292 | ```javascript 293 | import React from 'react' 294 | import ReactDOM from 'react-dom' 295 | import {Simulate} from 'react-dom/test-utils' 296 | import * as glamor from 'glamor' 297 | import {toMatchDiffSnapshot, getSnapshotDiffSerializer} from 'snapshot-diff' 298 | import serializer, {fromDOMNode} from 'jest-glamor-react' 299 | 300 | expect.addSnapshotSerializer(getSnapshotDiffSerializer()) 301 | expect.addSnapshotSerializer(serializer) 302 | expect.extend({toMatchDiffSnapshot}) 303 | 304 | function Button({count, ...props}) { 305 | const className = glamor.css({margin: 10 + count}) 306 | return 320 | ) 321 | } 322 | } 323 | 324 | test('snapshot diff works', () => { 325 | const control = render() 326 | const variable = render() 327 | Simulate.click(variable) 328 | expect(fromDOMNode(control)).toMatchDiffSnapshot(fromDOMNode(variable)) 329 | }) 330 | 331 | function render(ui) { 332 | const div = document.createElement('div') 333 | ReactDOM.render(ui, div) 334 | return div.children[0] 335 | } 336 | ``` 337 | 338 | The result of this snapshot is: 339 | 340 | ```diff 341 | Snapshot Diff: 342 | - First value 343 | + Second value 344 | 345 | .css-0, 346 | [data-css-0] { 347 | - margin: 10px; 348 | + margin: 11px; 349 | } 350 | 351 | - 352 | + 353 | ``` 354 | 355 | Pretty handy right?! 356 | 357 | Notice the `fromHTMLString` function you can import from `jest-glamor-react`. 358 | That's what `jest-glamor-react` uses internally if you try to snapshot a 359 | string that looks like HTML and includes `css-` in it. I can't think of any 360 | other context where it would be useful, so it's not documented beyond this 361 | example. 362 | 363 | ## Inspiration 364 | 365 | As mentioned earlier, [@MicheleBertoli][michelebertoli]'s 366 | [`jest-styled-components`][jest-styled-components] was a huge inspiration for 367 | this project. And much of the original code came from from that MIT Licensed 368 | project. Thank you so much Michele! 👏 369 | 370 | ## Other Solutions 371 | 372 | I'm unaware of other solutions. Please file a PR if you know of any! 373 | 374 | ## Contributors 375 | 376 | Thanks goes to these people ([emoji key][emojis]): 377 | 378 | 379 | 380 | 381 | | [
Michele Bertoli](http://michele.berto.li)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=MicheleBertoli "Code") [📖](https://github.com/kentcdodds/jest-glamor-react/commits?author=MicheleBertoli "Documentation") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=MicheleBertoli "Tests") | [
Kent C. Dodds](https://kentcdodds.com)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/jest-glamor-react/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=kentcdodds "Tests") | [
Mitchell Hamilton](https://hamil.town)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=mitchellhamilton "Code") [📖](https://github.com/kentcdodds/jest-glamor-react/commits?author=mitchellhamilton "Documentation") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=mitchellhamilton "Tests") | [
jhurley23](https://github.com/jhurley23)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=jhurley23 "Code") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=jhurley23 "Tests") [📖](https://github.com/kentcdodds/jest-glamor-react/commits?author=jhurley23 "Documentation") | [
Gaurav Talwar](https://github.com/megaurav2002)
| [
Henry Lewis](http://hjylewis.com/)
[🐛](https://github.com/kentcdodds/jest-glamor-react/issues?q=author%3Ahjylewis "Bug reports") [💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=hjylewis "Code") | [
Alexey Svetliakov](https://github.com/asvetliakov)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=asvetliakov "Code") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=asvetliakov "Tests") | 382 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 383 | | [
James W Lane](http://jameswlane.com)
[🐛](https://github.com/kentcdodds/jest-glamor-react/issues?q=author%3Ajameswlane "Bug reports") [💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=jameswlane "Code") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=jameswlane "Tests") | [
Brent Ertz](https://github.com/brentertz)
[📖](https://github.com/kentcdodds/jest-glamor-react/commits?author=brentertz "Documentation") | [
Junyoung Clare Jang](http://ailrun.github.io/)
[💻](https://github.com/kentcdodds/jest-glamor-react/commits?author=Ailrun "Code") [⚠️](https://github.com/kentcdodds/jest-glamor-react/commits?author=Ailrun "Tests") | 384 | 385 | 386 | 387 | This project follows the [all-contributors][all-contributors] specification. Contributions of any kind welcome! 388 | 389 | ## LICENSE 390 | 391 | MIT 392 | 393 | [npm]: https://www.npmjs.com/ 394 | [node]: https://nodejs.org 395 | [build-badge]: https://img.shields.io/travis/kentcdodds/jest-glamor-react.svg?style=flat-square 396 | [build]: https://travis-ci.org/kentcdodds/jest-glamor-react 397 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/jest-glamor-react.svg?style=flat-square 398 | [coverage]: https://codecov.io/github/kentcdodds/jest-glamor-react 399 | [version-badge]: https://img.shields.io/npm/v/jest-glamor-react.svg?style=flat-square 400 | [package]: https://www.npmjs.com/package/jest-glamor-react 401 | [downloads-badge]: https://img.shields.io/npm/dm/jest-glamor-react.svg?style=flat-square 402 | [npm-stat]: http://npm-stat.com/charts.html?package=jest-glamor-react&from=2016-04-01 403 | [license-badge]: https://img.shields.io/npm/l/jest-glamor-react.svg?style=flat-square 404 | [license]: https://github.com/kentcdodds/jest-glamor-react/blob/master/other/LICENSE 405 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 406 | [prs]: http://makeapullrequest.com 407 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square 408 | [donate]: http://kcd.im/donate 409 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 410 | [coc]: https://github.com/kentcdodds/jest-glamor-react/blob/master/other/CODE_OF_CONDUCT.md 411 | [roadmap-badge]: https://img.shields.io/badge/%F0%9F%93%94-roadmap-CD9523.svg?style=flat-square 412 | [roadmap]: https://github.com/kentcdodds/jest-glamor-react/blob/master/other/ROADMAP.md 413 | [examples-badge]: https://img.shields.io/badge/%F0%9F%92%A1-examples-8C8E93.svg?style=flat-square 414 | [examples]: https://github.com/kentcdodds/jest-glamor-react/blob/master/other/EXAMPLES.md 415 | [github-watch-badge]: https://img.shields.io/github/watchers/kentcdodds/jest-glamor-react.svg?style=social 416 | [github-watch]: https://github.com/kentcdodds/jest-glamor-react/watchers 417 | [github-star-badge]: https://img.shields.io/github/stars/kentcdodds/jest-glamor-react.svg?style=social 418 | [github-star]: https://github.com/kentcdodds/jest-glamor-react/stargazers 419 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20jest-glamor-react!%20https://github.com/kentcdodds/jest-glamor-react%20%F0%9F%91%8D 420 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/jest-glamor-react.svg?style=social 421 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 422 | [all-contributors]: https://github.com/kentcdodds/all-contributors 423 | [glamor]: https://www.npmjs.com/package/glamor 424 | [snapshot]: http://facebook.github.io/jest/docs/snapshot-testing.html 425 | [jest]: http://facebook.github.io/jest/ 426 | [michelebertoli]: https://github.com/MicheleBertoli 427 | [jest-styled-components]: https://github.com/styled-components/jest-styled-components 428 | [cxs]: https://www.npmjs.com/package/cxs 429 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const serializer = require('./dist/serializer') 2 | const {toHaveStyleRule} = require('./dist/matchers') 3 | 4 | module.exports = serializer 5 | module.exports.toHaveStyleRule = toHaveStyleRule 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestConfig = require('kcd-scripts/jest') 2 | 3 | module.exports = Object.assign(jestConfig, { 4 | setupTestFrameworkScriptFile: require.resolve( 5 | './other/setup-test-framework.js', 6 | ), 7 | }) 8 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kent+coc@doddsfamily.us. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /other/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | There aren't any examples yet! Want to add one? See `CONTRIBUTING.md` 4 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | 4 | 5 | 6 | 7 | **Table of Contents** 8 | 9 | * [Code of Conduct](#code-of-conduct) 10 | * [Issues](#issues) 11 | * [Pull Requests](#pull-requests) 12 | * [Release](#release) 13 | * [Thanks!](#thanks) 14 | 15 | 16 | 17 | This is documentation for maintainers of this project. 18 | 19 | ## Code of Conduct 20 | 21 | Please review, understand, and be an example of it. Violations of the code of conduct are 22 | taken seriously, even (especially) for maintainers. 23 | 24 | ## Issues 25 | 26 | We want to support and build the community. We do that best by helping people learn to solve 27 | their own problems. We have an issue template and hopefully most folks follow it. If it's 28 | not clear what the issue is, invite them to create a minimal reproduction of what they're trying 29 | to accomplish or the bug they think they've found. 30 | 31 | Once it's determined that a code change is necessary, point people to 32 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request. 33 | If they're the one who needs the feature, they're the one who can build it. If they need 34 | some hand holding and you have time to lend a hand, please do so. It's an investment into 35 | another human being, and an investment into a potential maintainer. 36 | 37 | Remember that this is open source, so the code is not yours, it's ours. If someone needs a change 38 | in the codebase, you don't have to make it happen yourself. Commit as much time to the project 39 | as you want/need to. Nobody can ask any more of you than that. 40 | 41 | ## Pull Requests 42 | 43 | As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either 44 | way is fine. 45 | 46 | When we receive a pull request, a travis build is kicked off automatically (see the `.travis.yml` 47 | for what runs in the travis build). We avoid merging anything that breaks the travis build. 48 | 49 | Please review PRs and focus on the code rather than the individual. You never know when this is 50 | someone's first ever PR and we want their experience to be as positive as possible, so be 51 | uplifting and constructive. 52 | 53 | When you merge the pull request, 99% of the time you should use the 54 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps 55 | our git history clean, but more importantly, this allows us to make any necessary changes to the 56 | commit message so we release what we want to release. See the next section on Releases for more 57 | about that. 58 | 59 | ## Release 60 | 61 | Our releases are automatic. They happen whenever code lands into `master`. A travis build gets 62 | kicked off and if it's successful, a tool called 63 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is used to 64 | automatically publish a new release to npm as well as a changelog to GitHub. It is only able to 65 | determine the version and whether a release is necessary by the git commit messages. With this 66 | in mind, **please brush up on [the commit message convention][commit] which drives our releases.** 67 | 68 | > One important note about this: Please make sure that commit messages do NOT contain the words 69 | > "BREAKING CHANGE" in them unless we want to push a major version. I've been burned by this 70 | > more than once where someone will include "BREAKING CHANGE: None" and it will end up releasing 71 | > a new major version. Not a huge deal honestly, but kind of annoying... 72 | 73 | ## Thanks! 74 | 75 | Thank you so much for helping to maintain this project! 76 | 77 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 78 | -------------------------------------------------------------------------------- /other/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Project Roadmap 2 | 3 | This is where we'll define a few things about the library's goals. 4 | 5 | We haven't filled this out yet though. Care to help? See `CONTRIBUTING.md` 6 | 7 | ## Want to do 8 | 9 | ## Might do 10 | 11 | ## Wont do 12 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | This project has an automated release set up. So things are only released when there are 10 | useful changes in the code that justify a release. But sometimes things get messed up one way or another 11 | and we need to trigger the release ourselves. When this happens, simply bump the number below and commit 12 | that with the following commit message based on your needs: 13 | 14 | **Major** 15 | 16 | ``` 17 | fix(release): manually release a major version 18 | 19 | There was an issue with a major release, so this manual-releases.md 20 | change is to release a new major version. 21 | 22 | Reference: # 23 | 24 | BREAKING CHANGE: 25 | ``` 26 | 27 | **Minor** 28 | 29 | ``` 30 | feat(release): manually release a minor version 31 | 32 | There was an issue with a minor release, so this manual-releases.md 33 | change is to release a new minor version. 34 | 35 | Reference: # 36 | ``` 37 | 38 | **Patch** 39 | 40 | ``` 41 | fix(release): manually release a patch version 42 | 43 | There was an issue with a patch release, so this manual-releases.md 44 | change is to release a new patch version. 45 | 46 | Reference: # 47 | ``` 48 | 49 | The number of times we've had to do a manual release is: 2 50 | -------------------------------------------------------------------------------- /other/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/jest-glamor-react/61cb3d0d7acef4f34128a7d3a75f60ebc97730e2/other/screenshot.png -------------------------------------------------------------------------------- /other/setup-test-framework.js: -------------------------------------------------------------------------------- 1 | // this is the jest setupTestFrameworkScriptFile 2 | import Enzyme from 'enzyme' 3 | import Adapter from 'enzyme-adapter-react-16' 4 | 5 | Enzyme.configure({adapter: new Adapter()}) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-glamor-react", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Jest utilities for Glamor and React", 5 | "main": "./index.js", 6 | "scripts": { 7 | "add-contributor": "kcd-scripts contributors add", 8 | "build": "kcd-scripts build", 9 | "lint": "kcd-scripts lint", 10 | "test": "kcd-scripts test", 11 | "setup": "npm install && npm run validate", 12 | "validate": "kcd-scripts validate", 13 | "precommit": "kcd-scripts precommit" 14 | }, 15 | "files": ["dist", "index.js"], 16 | "keywords": [], 17 | "author": "Kent C. Dodds (http://kentcdodds.com/)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "chalk": "^2.3.2", 21 | "css": "^2.2.1", 22 | "is-html": "^1.1.0", 23 | "pretty-format": "^22.4.0" 24 | }, 25 | "devDependencies": { 26 | "enzyme": "^3.3.0", 27 | "enzyme-adapter-react-16": "^1.1.1", 28 | "enzyme-to-json": "^3.3.1", 29 | "glamor": "^2.20.24", 30 | "kcd-scripts": "0.36.1", 31 | "react": "^16.2.0", 32 | "react-addons-test-utils": "^15.4.2", 33 | "react-dom": "^16.2.0", 34 | "react-test-renderer": "^16.2.0", 35 | "snapshot-diff": "^0.3.0" 36 | }, 37 | "peerDependencies": { 38 | "glamor": "^2 || ^3" 39 | }, 40 | "eslintConfig": { 41 | "extends": ["./node_modules/kcd-scripts/eslint.js"], 42 | "rules": { 43 | "jsx-a11y/heading-has-content": "off" 44 | } 45 | }, 46 | "eslintIgnore": [ 47 | "node_modules", 48 | "coverage", 49 | "dist", 50 | "storybook-static", 51 | "typings" 52 | ], 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/kentcdodds/jest-glamor-react.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/kentcdodds/jest-glamor-react/issues" 59 | }, 60 | "homepage": "https://github.com/kentcdodds/jest-glamor-react#readme" 61 | } 62 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/dom-nodes.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Appended class: 5. general tests: Appended class - {"&.button":{"color":"green"}} 1`] = ` 4 | .css-0, 5 | [data-css-1].button { 6 | color: green; 7 | } 8 | 9 |
12 | `; 13 | 14 | exports[`Child selector: 6. general tests: Child selector - {"> div":{"display":"inline-block"}} 1`] = ` 15 | .css-0> div, 16 | [data-css-0]> div { 17 | display: inline-block; 18 | } 19 | 20 |
23 | `; 24 | 25 | exports[`can take multiple snapshots 1`] = ` 26 | .css-0, 27 | [data-css-0] { 28 | padding: 4em; 29 | background: papayawhip; 30 | } 31 | 32 |
35 | `; 36 | 37 | exports[`can take multiple snapshots 2`] = ` 38 | .css-0, 39 | [data-css-0] { 40 | padding: 4em; 41 | background: papayawhip; 42 | } 43 | 44 |
47 | `; 48 | 49 | exports[`data attributes: 1. general tests: data attributes - {"backgroundColor":"rebeccapurple","margin":2} 1`] = ` 50 | .css-0, 51 | [data-css-0] { 52 | background-color: rebeccapurple; 53 | margin: 2px; 54 | } 55 | 56 |
59 | `; 60 | 61 | exports[`doesn't mess up stuff that does't have styles 1`] = `
`; 62 | 63 | exports[`doesn't mess up stuff when styles have a child selector 1`] = ` 64 | .css-0> div, 65 | [data-css-0]> div { 66 | display: inline-block; 67 | } 68 | 69 |
70 | 73 |
74 | `; 75 | 76 | exports[`media queries: 2. general tests: media queries - {"fontSize":24,"margin":12,"@media (max-width: 641px)":{"fontSize":20,"margin":10}} 1`] = ` 77 | .css-0, 78 | [data-css-0] { 79 | font-size: 24px; 80 | margin: 12px; 81 | } 82 | 83 | @media (max-width: 641px) { 84 | .css-0, 85 | [data-css-0] { 86 | font-size: 20px; 87 | margin: 10px; 88 | } 89 | } 90 | 91 |
94 | `; 95 | 96 | exports[`pseudo elements: 3. general tests: pseudo elements - {"& button":{"color":"green"}} 1`] = ` 97 | .css-0 button, 98 | [data-css-0] button { 99 | color: green; 100 | } 101 | 102 |
105 | `; 106 | 107 | exports[`pseudo states: 4. general tests: pseudo states - {":hover":{"backgroundColor":"blue"}} 1`] = ` 108 | .css-0, 109 | [data-css-1]:hover { 110 | background-color: blue; 111 | } 112 | 113 |
116 | `; 117 | 118 | exports[`works when the root element does not have styles 1`] = ` 119 | .css-0, 120 | [data-css-0] { 121 | padding: 4em; 122 | background: papayawhip; 123 | } 124 | 125 |
126 |
129 |
130 | `; 131 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/example.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react-dom snapshot 1`] = ` 4 | .css-0, 5 | [data-css-0] { 6 | padding: 4em; 7 | background: papayawhip; 8 | } 9 | 10 | .css-1, 11 | [data-css-1] { 12 | font-size: 1.5em; 13 | text-align: center; 14 | color: palevioletred; 15 | } 16 | 17 |
20 |

23 | Hello World, this is my first glamor styled component! 24 |

25 |
26 | `; 27 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/serializer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Appended class: 5. general tests: Appended class - {"&.button":{"color":"green"}} 1`] = ` 4 | .css-0, 5 | [data-css-1].button { 6 | color: green; 7 | } 8 | 9 |
12 | `; 13 | 14 | exports[`Child selector: 6. general tests: Child selector - {"> div":{"display":"inline-block"}} 1`] = ` 15 | .css-0> div, 16 | [data-css-0]> div { 17 | display: inline-block; 18 | } 19 | 20 |
23 | `; 24 | 25 | exports[`data attributes: 1. general tests: data attributes - {"backgroundColor":"rebeccapurple","margin":2} 1`] = ` 26 | .css-0, 27 | [data-css-0] { 28 | background-color: rebeccapurple; 29 | margin: 2px; 30 | } 31 | 32 |
35 | `; 36 | 37 | exports[`doesn't mess up stuff that does't have styles 1`] = `
`; 38 | 39 | exports[`doesn't mess up stuff when styles have a child selector 1`] = ` 40 | .css-0> div, 41 | [data-css-0]> div { 42 | display: inline-block; 43 | } 44 | 45 |
46 | 49 |
50 | `; 51 | 52 | exports[`enzyme: enzyme.mount 1`] = ` 53 | .css-0, 54 | [data-css-0] { 55 | padding: 4em; 56 | background: papayawhip; 57 | } 58 | 59 | .css-1, 60 | [data-css-1] { 61 | font-size: 1.5em; 62 | text-align: center; 63 | color: palevioletred; 64 | } 65 | 66 | 67 |
70 | 71 | <h1 72 | className="css-1" 73 | > 74 | Hello World, this is my first glamor styled component! 75 | </h1> 76 | 77 |
78 |
79 | `; 80 | 81 | exports[`enzyme: enzyme.render 1`] = ` 82 | .css-0, 83 | [data-css-0] { 84 | padding: 4em; 85 | background: papayawhip; 86 | } 87 | 88 | .css-1, 89 | [data-css-1] { 90 | font-size: 1.5em; 91 | text-align: center; 92 | color: palevioletred; 93 | } 94 | 95 |
98 |

101 | Hello World, this is my first glamor styled component! 102 |

103 |
104 | `; 105 | 106 | exports[`enzyme: enzyme.shallow 1`] = ` 107 | .css-0, 108 | [data-css-0] { 109 | padding: 4em; 110 | background: papayawhip; 111 | } 112 | 113 |
116 | 117 | Hello World, this is my first glamor styled component! 118 | 119 |
120 | `; 121 | 122 | exports[`media queries: 2. general tests: media queries - {"fontSize":24,"margin":12,"@media (max-width: 641px)":{"fontSize":20,"margin":10}} 1`] = ` 123 | .css-0, 124 | [data-css-0] { 125 | font-size: 24px; 126 | margin: 12px; 127 | } 128 | 129 | @media (max-width: 641px) { 130 | .css-0, 131 | [data-css-0] { 132 | font-size: 20px; 133 | margin: 10px; 134 | } 135 | } 136 | 137 |
140 | `; 141 | 142 | exports[`pseudo elements: 3. general tests: pseudo elements - {"& button":{"color":"green"}} 1`] = ` 143 | .css-0 button, 144 | [data-css-0] button { 145 | color: green; 146 | } 147 | 148 |
151 | `; 152 | 153 | exports[`pseudo states: 4. general tests: pseudo states - {":hover":{"backgroundColor":"blue"}} 1`] = ` 154 | .css-0, 155 | [data-css-1]:hover { 156 | background-color: blue; 157 | } 158 | 159 |
162 | `; 163 | 164 | exports[`react-test-renderer 1`] = ` 165 | .css-0, 166 | [data-css-0] { 167 | padding: 4em; 168 | background: papayawhip; 169 | } 170 | 171 | .css-1, 172 | [data-css-1] { 173 | font-size: 1.5em; 174 | text-align: center; 175 | color: palevioletred; 176 | } 177 | 178 |
181 |

184 | Hello World, this is my first glamor styled component! 185 |

186 |
187 | `; 188 | 189 | exports[`works when the root element does not have styles 1`] = ` 190 | .css-0, 191 | [data-css-0] { 192 | padding: 4em; 193 | background: papayawhip; 194 | } 195 | 196 |
197 |
200 |
201 | `; 202 | 203 | exports[`works with strings 1`] = ` 204 | .css-0, 205 | [data-css-0] { 206 | margin: 10px; 207 | } 208 | 209 |
212 | `; 213 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/snapshot-diff.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot diff works 1`] = ` 4 | Snapshot Diff: 5 | - First value 6 | + Second value 7 | 8 | .css-0, 9 | [data-css-0] { 10 | - margin: 10px; 11 | + margin: 11px; 12 | } 13 | 14 | 20 | `; 21 | -------------------------------------------------------------------------------- /src/__tests__/dom-nodes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import * as glamor from 'glamor' 4 | import serializer from '../serializer' 5 | 6 | expect.addSnapshotSerializer(serializer) 7 | 8 | function Wrapper(props) { 9 | const className = glamor.css({ 10 | padding: '4em', 11 | background: 'papayawhip', 12 | }) 13 | return
14 | } 15 | 16 | test('works when the root element does not have styles', () => { 17 | const div = render( 18 |
19 | 20 |
, 21 | ) 22 | 23 | expect(div).toMatchSnapshot() 24 | }) 25 | 26 | test(`doesn't mess up stuff that does't have styles`, () => { 27 | expect(render(
)).toMatchSnapshot() 28 | }) 29 | 30 | test('can take multiple snapshots', () => { 31 | const div = render() 32 | expect(div).toMatchSnapshot() 33 | expect(div).toMatchSnapshot() 34 | }) 35 | 36 | test(`doesn't mess up stuff when styles have a child selector`, () => { 37 | const style = glamor.css({ 38 | '> div': { 39 | display: 'inline-block', 40 | }, 41 | }) 42 | 43 | const div = render( 44 |
45 | 46 |
, 47 | ) 48 | 49 | expect(div).toMatchSnapshot() 50 | }) 51 | 52 | const generalTests = [ 53 | { 54 | title: 'data attributes', 55 | styles: { 56 | backgroundColor: 'rebeccapurple', 57 | margin: 2, 58 | }, 59 | }, 60 | { 61 | title: 'media queries', 62 | styles: { 63 | fontSize: 24, 64 | margin: 12, 65 | '@media (max-width: 641px)': { 66 | fontSize: 20, 67 | margin: 10, 68 | }, 69 | }, 70 | }, 71 | { 72 | title: 'pseudo elements', 73 | styles: { 74 | '& button': { 75 | color: 'green', 76 | }, 77 | }, 78 | }, 79 | { 80 | title: 'pseudo states', 81 | styles: { 82 | ':hover': { 83 | backgroundColor: 'blue', 84 | }, 85 | }, 86 | }, 87 | { 88 | title: 'Appended class', 89 | styles: { 90 | '&.button': { 91 | color: 'green', 92 | }, 93 | }, 94 | }, 95 | { 96 | title: 'Child selector', 97 | styles: { 98 | '> div': { 99 | display: 'inline-block', 100 | }, 101 | }, 102 | }, 103 | ] 104 | 105 | generalTests.forEach(({title, styles}, index) => { 106 | test(title, () => { 107 | const div = render(
) 108 | expect(div).toMatchSnapshot( 109 | `${index + 1}. general tests: ${title} - ${JSON.stringify(styles)}`, 110 | ) 111 | }) 112 | }) 113 | 114 | function render(ui) { 115 | const div = document.createElement('div') 116 | ReactDOM.render(ui, div) 117 | return div.children[0] 118 | } 119 | -------------------------------------------------------------------------------- /src/__tests__/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import * as glamor from 'glamor' 4 | import serializer from '../serializer' 5 | import {toHaveStyleRule} from '../matchers' 6 | 7 | expect.addSnapshotSerializer(serializer) 8 | expect.extend({toHaveStyleRule}) 9 | 10 | function Wrapper(props) { 11 | const className = glamor.css({ 12 | padding: '4em', 13 | background: 'papayawhip', 14 | }) 15 | return
16 | } 17 | 18 | function Title(props) { 19 | const className = glamor.css({ 20 | fontSize: '1.5em', 21 | textAlign: 'center', 22 | color: 'palevioletred', 23 | }) 24 | return

25 | } 26 | 27 | function render(ui) { 28 | const div = document.createElement('div') 29 | ReactDOM.render(ui, div) 30 | return div.children[0] 31 | } 32 | 33 | test('react-dom snapshot', () => { 34 | const node = render( 35 | 36 | Hello World, this is my first glamor styled component! 37 | , 38 | ) 39 | expect(node).toMatchSnapshot() 40 | }) 41 | 42 | test('react-dom toHaveStyleRule', () => { 43 | const node = render( 44 | 45 | Hello World, this is my first glamor styled component! 46 | , 47 | ) 48 | expect(node).toHaveStyleRule('background', 'papayawhip') 49 | }) 50 | -------------------------------------------------------------------------------- /src/__tests__/matchers.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import renderer from 'react-test-renderer' 4 | import * as glamor from 'glamor' 5 | import * as enzyme from 'enzyme' 6 | import {toHaveStyleRule} from '../matchers' 7 | import serializer from '../serializer' 8 | 9 | expect.addSnapshotSerializer(serializer) 10 | expect.extend({toHaveStyleRule}) 11 | 12 | function Wrapper(props) { 13 | const className = glamor.css({ 14 | padding: '4em', 15 | background: 'papayawhip', 16 | }) 17 | return
18 | } 19 | 20 | function Title(props) { 21 | const className = glamor.css({ 22 | fontSize: '1.5em', 23 | textAlign: 'center', 24 | color: 'palevioletred', 25 | }) 26 | return

27 | } 28 | 29 | test('react-test-renderer', () => { 30 | const wrapper = renderer 31 | .create( 32 | 33 | Hello World, this is my first glamor styled component! 34 | , 35 | ) 36 | .toJSON() 37 | 38 | expect(wrapper).toHaveStyleRule('background', 'papayawhip') 39 | expect(wrapper).not.toHaveStyleRule('color', 'palevioletred') 40 | expect(wrapper).toHaveStyleRule('padding', expect.anything()) 41 | 42 | const title = wrapper.children[0] 43 | expect(title).not.toHaveStyleRule('background', 'papayawhip') 44 | expect(title).toHaveStyleRule('color', 'palevioletred') 45 | expect(title).toHaveStyleRule('text-align', expect.stringMatching(/cen.*$/)) 46 | }) 47 | 48 | test('enzyme', () => { 49 | const ui = ( 50 | 51 | Hello World, this is my first glamor styled component! 52 | 53 | ) 54 | 55 | const enzymeMethods = ['shallow', 'mount', 'render'] 56 | enzymeMethods.forEach(method => { 57 | const wrapper = enzyme[method](ui) 58 | expect(wrapper).toHaveStyleRule('background', 'papayawhip') 59 | expect(wrapper).not.toHaveStyleRule('color', 'palevioletred') 60 | expect(wrapper).toHaveStyleRule('padding', expect.anything()) 61 | 62 | const title = method === 'render' ? wrapper.find('h1') : wrapper.find(Title) 63 | expect(title).not.toHaveStyleRule('background', 'papayawhip') 64 | expect(title).toHaveStyleRule('color', 'palevioletred') 65 | expect(title).toHaveStyleRule('text-align', expect.stringMatching(/cen.*$/)) 66 | }) 67 | }) 68 | 69 | test('dom nodes', () => { 70 | const div = document.createElement('div') 71 | ReactDOM.render( 72 | 73 | Hello World, this is my first glamor styled component! 74 | , 75 | div, 76 | ) 77 | const wrapper = div.children[0] 78 | expect(wrapper).toHaveStyleRule('background', 'papayawhip') 79 | expect(wrapper).not.toHaveStyleRule('color', 'palevioletred') 80 | 81 | const title = wrapper.children[0] 82 | expect(title).not.toHaveStyleRule('background', 'papayawhip') 83 | expect(title).toHaveStyleRule('color', 'palevioletred') 84 | }) 85 | 86 | describe('toHaveStyleRule', () => { 87 | it('handles bad input', () => { 88 | const badResult = toHaveStyleRule(null, 'background', null) 89 | expect(badResult.pass).toBe(false) 90 | expect(badResult.message()).toBe('Property not found: background') 91 | }) 92 | 93 | it('gives helpful error messages', () => { 94 | const wrapper = renderer.create().toJSON() 95 | 96 | expect( 97 | toHaveStyleRule(wrapper, 'background', 'palevioletred').message(), 98 | ).toContain('Expected background to match:') 99 | expect( 100 | toHaveStyleRule(wrapper, 'background', 'papayawhip').message(), 101 | ).toContain('Expected background not to match:') 102 | }) 103 | 104 | it('can recieve string, regex values, or asymmetricMatchers (like expect.anything())', () => { 105 | const wrapper = renderer.create().toJSON() 106 | 107 | expect(toHaveStyleRule(wrapper, 'background', /papayawhip/).pass).toBe(true) 108 | expect(toHaveStyleRule(wrapper, 'background', 'papayawhip').pass).toBe(true) 109 | expect(toHaveStyleRule(wrapper, 'background', expect.anything()).pass).toBe( 110 | true, 111 | ) 112 | expect( 113 | toHaveStyleRule(wrapper, 'background', expect.stringContaining('paya')) 114 | .pass, 115 | ).toBe(true) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/__tests__/replace-selectors.js: -------------------------------------------------------------------------------- 1 | import {replaceSelectors} from '../replace-selectors' 2 | 3 | test('Replaces a single class', () => { 4 | const selectors = ['.css-12345'] 5 | const styles = ` 6 | .css-12345, 7 | [data-css-12345] { 8 | font-size: 1.5em; 9 | text-align: center; 10 | color: palevioletred; 11 | } 12 | ` 13 | const code = ` 14 |

17 | Hello World, this is my first glamor styled component! 18 |

19 | ` 20 | const result = replaceSelectors(selectors, styles, code) 21 | expect(result).toMatch(/(css-0)/) 22 | expect(result).not.toMatch(/(css-12345)/) 23 | }) 24 | 25 | test('Replaces multiple glamor classes', () => { 26 | const selectors = ['.css-12345', '.css-67890'] 27 | const styles = ` 28 | .css-12345, 29 | [data-css-12345] { 30 | font-size: 1.5em; 31 | text-align: center; 32 | color: palevioletred; 33 | } 34 | 35 | .css-67890, 36 | [data-css-67890] { 37 | font-size: 1.5em; 38 | text-align: center; 39 | color: palevioletred; 40 | } 41 | ` 42 | const code = ` 43 |
46 |

49 | Hello World, this is my first glamor styled component! 50 |

51 |
52 | ` 53 | const result = replaceSelectors(selectors, styles, code) 54 | expect(result).toMatch(/(css-0)/) 55 | expect(result).toMatch(/(css-1)/) 56 | }) 57 | 58 | test('does not replace non-glamor classes', () => { 59 | const selectors = ['.p-4em'] 60 | const styles = ` 61 | .p-4em, 62 | [data-p-4em] { 63 | padding: 4em; 64 | } 65 | ` 66 | const code = ` 67 |
70 | Hello World 71 |
72 | ` 73 | const result = replaceSelectors(selectors, styles, code) 74 | expect(result).not.toMatch(/(css-0)/) 75 | expect(result).toMatch(/(p-4em)/) 76 | }) 77 | 78 | test('only replaces classes beginning with "css-"', () => { 79 | const selectors = ['.not-css-css-1234'] 80 | const styles = ` 81 | .not-css-css-1234, 82 | [data-not-css-css-1234] { 83 | padding: 4em; 84 | } 85 | ` 86 | const code = ` 87 |
90 | Hello World 91 |
92 | ` 93 | const result = replaceSelectors(selectors, styles, code) 94 | expect(result).not.toMatch(/(css-0)/) 95 | expect(result).toMatch(/(not-css-css-1234)/) 96 | }) 97 | 98 | test('replaces css-nil classes', () => { 99 | const selectors = ['.css-nil'] 100 | const styles = `` 101 | const code = ` 102 |

105 | Hello World, this is my first glamor styled component! 106 |

107 | ` 108 | const result = replaceSelectors(selectors, styles, code) 109 | expect(result).toMatch(/(css-0)/) 110 | expect(result).not.toMatch(/(css-nil)/) 111 | }) 112 | 113 | test('replaces css-label- classes', () => { 114 | const selectors = ['.css-label-12345'] 115 | const styles = ` 116 | .css-label-12345, 117 | [data-css-label-12345] { 118 | font-size: 1.5em; 119 | text-align: center; 120 | color: palevioletred; 121 | } 122 | ` 123 | const code = ` 124 |

127 | Hello World, this is my first glamor styled component! 128 |

129 | ` 130 | const result = replaceSelectors(selectors, styles, code) 131 | expect(result).toMatch(/(css-0)/) 132 | expect(result).not.toMatch(/(css-label-12345)/) 133 | }) 134 | 135 | test('replaces data attributes', () => { 136 | const selectors = ['[data-css-12345]', '.css-12345'] 137 | const styles = ` 138 | .css-12345, 139 | [data-css-12345] { 140 | font-size: 1.5em; 141 | text-align: center; 142 | color: palevioletred; 143 | } 144 | ` 145 | const code = ` 146 |

149 | Hello World, this is my first glamor styled component! 150 |

151 | ` 152 | const result = replaceSelectors(selectors, styles, code) 153 | expect(result).toMatch(/(data-css-0)/) 154 | expect(result).toMatch(/(css-0)/) 155 | expect(result).not.toMatch(/(css-12345)/) 156 | expect(result).not.toMatch(/(data-css-12345)/) 157 | }) 158 | -------------------------------------------------------------------------------- /src/__tests__/serializer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import * as glamor from 'glamor' 4 | import * as enzyme from 'enzyme' 5 | import toJson from 'enzyme-to-json' 6 | import serializer from '../serializer' 7 | 8 | expect.addSnapshotSerializer(serializer) 9 | 10 | function Wrapper(props) { 11 | const className = glamor.css({ 12 | padding: '4em', 13 | background: 'papayawhip', 14 | }) 15 | return
16 | } 17 | 18 | function Title(props) { 19 | const className = glamor.css({ 20 | fontSize: '1.5em', 21 | textAlign: 'center', 22 | color: 'palevioletred', 23 | }) 24 | return

25 | } 26 | 27 | test('react-test-renderer', () => { 28 | const tree = renderer 29 | .create( 30 | 31 | Hello World, this is my first glamor styled component! 32 | , 33 | ) 34 | .toJSON() 35 | 36 | expect(tree).toMatchSnapshot() 37 | }) 38 | 39 | test('enzyme', () => { 40 | const ui = ( 41 | 42 | Hello World, this is my first glamor styled component! 43 | 44 | ) 45 | 46 | const enzymeMethods = ['shallow', 'mount', 'render'] 47 | enzymeMethods.forEach(method => { 48 | const tree = enzyme[method](ui) 49 | expect(toJson(tree)).toMatchSnapshot(`enzyme.${method}`) 50 | }) 51 | }) 52 | 53 | test('works when the root element does not have styles', () => { 54 | const tree = renderer 55 | .create( 56 |
57 | 58 |
, 59 | ) 60 | .toJSON() 61 | 62 | expect(tree).toMatchSnapshot() 63 | }) 64 | 65 | test(`doesn't mess up stuff that does't have styles`, () => { 66 | const tree = renderer.create(
).toJSON() 67 | 68 | expect(tree).toMatchSnapshot() 69 | }) 70 | 71 | test('works with strings', () => { 72 | const className = glamor.css({margin: 10}) 73 | const html = `
` 74 | expect(html).toMatchSnapshot() 75 | }) 76 | 77 | test(`doesn't mess up stuff when styles have a child selector`, () => { 78 | const style = glamor.css({ 79 | '> div': { 80 | display: 'inline-block', 81 | }, 82 | }) 83 | 84 | const tree = renderer 85 | .create( 86 |
87 | 88 |
, 89 | ) 90 | .toJSON() 91 | 92 | expect(tree).toMatchSnapshot() 93 | }) 94 | 95 | const generalTests = [ 96 | { 97 | title: 'data attributes', 98 | styles: { 99 | backgroundColor: 'rebeccapurple', 100 | margin: 2, 101 | }, 102 | }, 103 | { 104 | title: 'media queries', 105 | styles: { 106 | fontSize: 24, 107 | margin: 12, 108 | '@media (max-width: 641px)': { 109 | fontSize: 20, 110 | margin: 10, 111 | }, 112 | }, 113 | }, 114 | { 115 | title: 'pseudo elements', 116 | styles: { 117 | '& button': { 118 | color: 'green', 119 | }, 120 | }, 121 | }, 122 | { 123 | title: 'pseudo states', 124 | styles: { 125 | ':hover': { 126 | backgroundColor: 'blue', 127 | }, 128 | }, 129 | }, 130 | { 131 | title: 'Appended class', 132 | styles: { 133 | '&.button': { 134 | color: 'green', 135 | }, 136 | }, 137 | }, 138 | { 139 | title: 'Child selector', 140 | styles: { 141 | '> div': { 142 | display: 'inline-block', 143 | }, 144 | }, 145 | }, 146 | ] 147 | 148 | generalTests.forEach(({title, styles}, index) => { 149 | test(title, () => { 150 | const tree = renderer.create(
) 151 | expect(tree).toMatchSnapshot( 152 | `${index + 1}. general tests: ${title} - ${JSON.stringify(styles)}`, 153 | ) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /src/__tests__/snapshot-diff.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {Simulate} from 'react-dom/test-utils' 4 | import * as glamor from 'glamor' 5 | import {toMatchDiffSnapshot, getSnapshotDiffSerializer} from 'snapshot-diff' 6 | import serializer, {fromHTMLString, fromDOMNode} from '../serializer' 7 | 8 | expect.addSnapshotSerializer(getSnapshotDiffSerializer()) 9 | expect.addSnapshotSerializer(serializer) 10 | expect.extend({toMatchDiffSnapshot}) 11 | 12 | function Button({count, ...props}) { 13 | const className = glamor.css({margin: 10 + count}) 14 | return 28 | ) 29 | } 30 | } 31 | 32 | test('snapshot diff works', () => { 33 | const control = render() 34 | const variable = render() 35 | Simulate.click(variable) 36 | // normally I'd probably just use fromDOMNode for both of these 37 | // but I'm using fromHTMLString to make sure that it works. 38 | expect(fromHTMLString(control.outerHTML)).toMatchDiffSnapshot( 39 | fromDOMNode(variable), 40 | ) 41 | }) 42 | 43 | function render(ui) { 44 | const div = document.createElement('div') 45 | ReactDOM.render(ui, div) 46 | return div.children[0] 47 | } 48 | 49 | /* eslint react/prop-types:0 */ 50 | -------------------------------------------------------------------------------- /src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import * as enzyme from 'enzyme' 2 | import renderer from 'react-test-renderer' 3 | import React from 'react' 4 | import {getClassNames, getNodes, getSelectors} from '../utils' 5 | 6 | const enzymeMethods = ['shallow', 'mount', 'render'] 7 | 8 | describe('getClassNames', () => { 9 | it('handles nothing being passed to it', () => { 10 | expect(getClassNames()).toEqual([]) 11 | }) 12 | it('handles bad data being passed to it', () => { 13 | expect(getClassNames({})).toEqual([]) 14 | }) 15 | describe('react-test-renderer', () => { 16 | it('can grab the className prop', () => { 17 | const wrapper = renderer.create(
).toJSON() 18 | expect(getClassNames(wrapper)).toEqual(['test']) 19 | }) 20 | it('can grab the class prop', () => { 21 | /* eslint-disable react/no-unknown-property */ 22 | const wrapper = renderer.create(
).toJSON() 23 | /* eslint-enable react/no-unknown-property */ 24 | expect(getClassNames(wrapper)).toEqual(['test']) 25 | }) 26 | it('returns an empty string if there is no valid prop', () => { 27 | const wrapper = renderer.create(
).toJSON() 28 | expect(getClassNames(wrapper)).toEqual([]) 29 | }) 30 | }) 31 | describe('enzyme', () => { 32 | it('can grab the className prop', () => { 33 | enzymeMethods.forEach(method => { 34 | const wrapper = enzyme[method](
) 35 | expect(getClassNames(wrapper)).toEqual(['test']) 36 | }) 37 | }) 38 | it('handle subcomponents', () => { 39 | enzymeMethods.forEach(method => { 40 | const wrapper = enzyme[method]( 41 |
42 | 43 |
, 44 | ) 45 | const subcomponent = wrapper.find('span') 46 | expect(getClassNames(subcomponent)).toEqual(['test2']) 47 | }) 48 | }) 49 | it('returns an empty string if there is no valid prop', () => { 50 | enzymeMethods.forEach(method => { 51 | const wrapper = enzyme[method](
) 52 | expect(getClassNames(wrapper)).toEqual([]) 53 | }) 54 | }) 55 | it('handles nothing being passed to it', () => { 56 | expect(getClassNames()).toEqual([]) 57 | }) 58 | }) 59 | }) 60 | 61 | describe('getNodes', () => { 62 | it('works on enzyme ReactWrappers', () => { 63 | const wrapper = enzyme.mount( 64 |
65 |
66 |
, 67 | ) 68 | expect(getNodes(wrapper)).toHaveLength(1) 69 | }) 70 | }) 71 | 72 | describe('getSelectors', () => { 73 | it('works on enzyme ReactWrappers', () => { 74 | const wrapper = enzyme.mount( 75 |
76 |
77 |
, 78 | ) 79 | expect(getSelectors(wrapper)).toHaveLength(1) 80 | }) 81 | it('works when a node has no props (edge case)', () => { 82 | const wrapper = renderer.create(
).toJSON() 83 | const nodes = getNodes(wrapper) 84 | // Simulate a node with no props. 85 | delete nodes[0].props 86 | expect(getSelectors(nodes)).toHaveLength(0) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /src/matchers.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const {getAST, getClassNames} = require('./utils') 3 | 4 | const getGlamorStyleSheet = () => require('glamor').styleSheet 5 | 6 | /* 7 | * Taken from 8 | * https://github.com/facebook/jest/blob/be4bec387d90ac8d6a7596be88bf8e4994bc3ed9/packages/expect/src/jasmine_utils.js#L234 9 | */ 10 | function isA(typeName, value) { 11 | return Object.prototype.toString.apply(value) === `[object ${typeName}]` 12 | } 13 | 14 | /* 15 | * Taken from 16 | * https://github.com/facebook/jest/blob/be4bec387d90ac8d6a7596be88bf8e4994bc3ed9/packages/expect/src/jasmine_utils.js#L36 17 | */ 18 | function isAsymmetric(obj) { 19 | return obj && isA('Function', obj.asymmetricMatch) 20 | } 21 | 22 | function valueMatches(declaration, value) { 23 | if (value instanceof RegExp) { 24 | return value.test(declaration.value) 25 | } 26 | 27 | if (isAsymmetric(value)) { 28 | return value.asymmetricMatch(declaration.value) 29 | } 30 | 31 | return value === declaration.value 32 | } 33 | 34 | function toHaveStyleRule(received, property, value) { 35 | const selectors = getClassNames(received) 36 | const ast = getAST(selectors, getGlamorStyleSheet()) 37 | 38 | const classNames = getClassNames(received) 39 | const rules = ast.stylesheet.rules.filter(rule => 40 | classNames.some(cn => rule.selectors.includes(`.${cn}`)), 41 | ) 42 | 43 | const declaration = rules 44 | .reduce((decs, rule) => Object.assign([], decs, rule.declarations), []) 45 | .filter(dec => dec.type === 'declaration' && dec.property === property) 46 | .pop() 47 | 48 | if (!declaration) { 49 | return { 50 | pass: false, 51 | message: () => `Property not found: ${property}`, 52 | } 53 | } 54 | 55 | const pass = valueMatches(declaration, value) 56 | 57 | const message = () => 58 | `Expected ${property}${pass ? ' not ' : ' '}to match:\n` + 59 | ` ${chalk.green(value)}\n` + 60 | 'Received:\n' + 61 | ` ${chalk.red(declaration.value)}` 62 | 63 | return { 64 | pass, 65 | message, 66 | } 67 | } 68 | 69 | module.exports = { 70 | toHaveStyleRule, 71 | } 72 | -------------------------------------------------------------------------------- /src/replace-selectors.js: -------------------------------------------------------------------------------- 1 | const replaceSelectors = (selectors, styles, code) => { 2 | // let index = 0 3 | const selectorIds = selectors.reduce((acc, selector) => { 4 | if (selector.includes('[data-css-')) { 5 | acc.add(/\[data-css-(.*)\]/g.exec(selector)[1]) 6 | } else if (selector.includes('.css-')) { 7 | acc.add(/\.css-([^ >]*)/g.exec(selector)[1]) 8 | } 9 | return acc 10 | }, new Set()) 11 | return Array.from(selectorIds).reduce((string, id, index) => { 12 | return string.split(id).join(index) 13 | }, `${styles}\n\n${code}`) 14 | } 15 | 16 | module.exports = {replaceSelectors} 17 | -------------------------------------------------------------------------------- /src/serializer.js: -------------------------------------------------------------------------------- 1 | const prettyFormat = require('pretty-format') 2 | const isHtml = require('is-html') 3 | const {replaceSelectors} = require('./replace-selectors') 4 | const { 5 | getNodes, 6 | markNodes, 7 | getSelectors, 8 | getStylesAndAllSelectors, 9 | } = require('./utils') 10 | 11 | // doing this to make it easier for users to mock things 12 | // like switching between development mode and whatnot. 13 | const getGlamorStyleSheet = () => require('glamor').styleSheet 14 | 15 | function createSerializer(styleSheet) { 16 | // eslint-disable-next-line complexity 17 | function test(val) { 18 | if (!val) { 19 | return false 20 | } 21 | return ( 22 | isHtmlStringWithGlamorClasses(val) || 23 | (!val.serializedWithJestGlamorReact && 24 | (val.$$typeof === Symbol.for('react.test.json') || 25 | (val instanceof HTMLElement && !isBeingSerialized(val)))) 26 | ) 27 | } 28 | 29 | function print(val, printer) { 30 | if (typeof val === 'string') { 31 | return fromHTMLString(val) 32 | } 33 | const nodes = getNodes(val) 34 | markNodes(nodes) 35 | const selectors = getSelectors(nodes) 36 | const {styles, allSelectors} = getStylesAndAllSelectors( 37 | selectors, 38 | styleSheet, 39 | ) 40 | const printedVal = printer(val) 41 | // allows us to take multiple snapshots 42 | val.serializedWithJestGlamorReact = false 43 | if (styles) { 44 | return replaceSelectors(allSelectors, styles, printedVal) 45 | } else { 46 | return printedVal 47 | } 48 | } 49 | 50 | return {test, print} 51 | } 52 | 53 | function isBeingSerialized(node) { 54 | let currentNode = node 55 | 56 | while (currentNode) { 57 | if (currentNode.serializedWithJestGlamorReact) { 58 | return true 59 | } 60 | currentNode = currentNode.parentNode 61 | } 62 | return false 63 | } 64 | 65 | function isHtmlStringWithGlamorClasses(string) { 66 | // is-html was letting a "Snapshot Diff:" get through 67 | return ( 68 | typeof string === 'string' && 69 | string.startsWith('<') && 70 | isHtml(string) && 71 | string.includes('css-') 72 | ) 73 | } 74 | 75 | function fromDOMNode(div, extract = r => r.children[0]) { 76 | const root = document.createElement('div') 77 | // not appending because we don't want to mess up the div they give us 78 | root.innerHTML = div.outerHTML 79 | const nodes = getNodes(root) 80 | const selectors = getSelectors(nodes) 81 | const {styles, allSelectors} = getStylesAndAllSelectors( 82 | selectors, 83 | getGlamorStyleSheet(), 84 | ) 85 | return replaceSelectors( 86 | allSelectors, 87 | styles, 88 | prettyFormat(extract(root), { 89 | plugins: [ 90 | prettyFormat.plugins.DOMElement, 91 | prettyFormat.plugins.DOMCollection, 92 | ], 93 | }), 94 | ) 95 | } 96 | 97 | function fromHTMLString(string) { 98 | const div = document.createElement('div') 99 | div.innerHTML = string 100 | return fromDOMNode(div, root => root.children[0].children[0]) 101 | } 102 | 103 | module.exports = Object.assign(createSerializer, { 104 | ...createSerializer(getGlamorStyleSheet), 105 | fromHTMLString, 106 | fromDOMNode, 107 | }) 108 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const css = require('css') 2 | 3 | function getNodes(node, nodes = []) { 4 | if (node.children) { 5 | const children = 6 | typeof node.children === 'function' ? node.children() : node.children 7 | if (Array.isArray(children)) { 8 | children.forEach(child => getNodes(child, nodes)) 9 | } 10 | } 11 | 12 | if (typeof node === 'object') { 13 | nodes.push(node) 14 | } 15 | 16 | return nodes 17 | } 18 | 19 | function markNodes(nodes) { 20 | nodes.forEach(node => { 21 | node.serializedWithJestGlamorReact = true 22 | }) 23 | } 24 | 25 | function getSelectors(nodes) { 26 | return nodes.reduce((selectors, node) => { 27 | if (node instanceof global.HTMLElement) { 28 | return getSelectorsFromDOM(selectors, node) 29 | } else { 30 | return getSelectorsFromProps(selectors, node) 31 | } 32 | }, []) 33 | } 34 | 35 | // eslint-disable-next-line 36 | function getSelectorsFromProps(selectors, node) { 37 | const props = typeof node.props === 'function' ? node.props() : node.props 38 | if (!props) return [] 39 | const className = props.className || props.class 40 | if (className) { 41 | selectors = selectors.concat( 42 | className 43 | .toString() 44 | .split(' ') 45 | .map(cn => `.${cn}`), 46 | ) 47 | } 48 | const dataProps = Object.keys(props).reduce((dProps, key) => { 49 | if (key.startsWith('data-')) { 50 | dProps.push(`[${key}]`) 51 | } 52 | return dProps 53 | }, []) 54 | if (dataProps.length) { 55 | selectors = selectors.concat(dataProps) 56 | } 57 | return selectors 58 | } 59 | 60 | function getSelectorsFromDOM(selectors, node) { 61 | const root = document.createElement('div') 62 | root.appendChild(node) 63 | const allChildren = root.querySelectorAll('*') 64 | selectors = Array.from(allChildren).reduce((s, c) => { 65 | s.push(...Array.from(c.classList).map(cn => `.${cn}`)) 66 | s.push( 67 | ...c 68 | .getAttributeNames() 69 | .filter(key => key.startsWith('data-')) 70 | .map(key => `[${key}]`), 71 | ) 72 | return s 73 | }, selectors) 74 | return selectors 75 | } 76 | 77 | function filterChildSelector(baseSelector) { 78 | if (baseSelector.slice(-1) === '>') { 79 | return baseSelector.slice(0, -1) 80 | } 81 | return baseSelector 82 | } 83 | 84 | function getAST(nodeSelectors, styleSheet) { 85 | const tags = 86 | typeof styleSheet === 'function' ? styleSheet().tags : styleSheet.tags 87 | const styles = tags 88 | .map(tag => /* istanbul ignore next */ tag.textContent || '') 89 | .join('\n') 90 | const ast = css.parse(styles) 91 | const rules = ast.stylesheet.rules.filter(filter) 92 | const mediaQueries = getMediaQueries(ast, filter) 93 | 94 | ast.stylesheet.rules = [...rules, ...mediaQueries] 95 | 96 | return ast 97 | 98 | function filter(rule) { 99 | if (rule.type === 'rule') { 100 | return rule.selectors.some(selector => { 101 | const baseSelector = filterChildSelector( 102 | selector.split(/:| |\./).filter(s => !!s)[0], 103 | ) 104 | return nodeSelectors.some( 105 | sel => sel === baseSelector || sel === `.${baseSelector}`, 106 | ) 107 | }) 108 | } 109 | return false 110 | } 111 | } 112 | 113 | function getStylesAndAllSelectors(nodeSelectors, styleSheet) { 114 | const ast = getAST(nodeSelectors, styleSheet) 115 | const allSelectors = ast.stylesheet.rules.reduce((s, r) => { 116 | if (r.selectors) { 117 | s.push(...r.selectors) 118 | } 119 | return s 120 | }, []) 121 | const styles = css.stringify(ast) 122 | return {styles, allSelectors} 123 | } 124 | 125 | function getMediaQueries(ast, filter) { 126 | return ast.stylesheet.rules 127 | .filter(rule => rule.type === 'media' || rule.type === 'supports') 128 | .reduce((acc, mediaQuery) => { 129 | mediaQuery.rules = mediaQuery.rules.filter(filter) 130 | 131 | if (mediaQuery.rules.length) { 132 | return acc.concat(mediaQuery) 133 | } 134 | 135 | return acc 136 | }, []) 137 | } 138 | 139 | function shouldDive(node) { 140 | return typeof node.dive === 'function' && typeof node.type() !== 'string' 141 | } 142 | 143 | function isTagWithClassName(node) { 144 | return node.prop('className') && typeof node.type() === 'string' 145 | } 146 | 147 | function getClassNameFromEnzyme(received) { 148 | const tree = shouldDive(received) ? received.dive() : received 149 | const components = tree.findWhere(isTagWithClassName) 150 | return components.length && components.first().prop('className') 151 | } 152 | 153 | function getClassNameFromTestRenderer(received) { 154 | return received.props.className || received.props.class 155 | } 156 | 157 | function getClassNameFromCheerio(received) { 158 | /* istanbul ignore next */ 159 | if (received[0].type === 'root') { 160 | // I'm honestly not sure we need this, but it was here 161 | // and I don't want to break it... 162 | return received 163 | .children() 164 | .first() 165 | .attr('class') 166 | } else { 167 | return received.attr('class') 168 | } 169 | } 170 | 171 | // eslint-disable-next-line complexity 172 | function getClassNames(received) { 173 | let className 174 | 175 | if (received) { 176 | if (received instanceof HTMLElement) { 177 | className = received.className 178 | } else if (received.$$typeof === Symbol.for('react.test.json')) { 179 | className = getClassNameFromTestRenderer(received) 180 | } else if (typeof received.findWhere === 'function') { 181 | className = getClassNameFromEnzyme(received) 182 | } else if (received.cheerio === '[cheerio object]') { 183 | className = getClassNameFromCheerio(received) 184 | } 185 | } 186 | 187 | return className ? className.split(/\s/) : [] 188 | } 189 | 190 | module.exports = { 191 | getNodes, 192 | markNodes, 193 | getSelectors, 194 | getAST, 195 | getStylesAndAllSelectors, 196 | getClassNames, 197 | } 198 | --------------------------------------------------------------------------------