├── .all-contributorsrc ├── .babelrc ├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── worklows │ └── add-issue-to-project.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .renovaterc.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config ├── karma │ ├── karmaConfig.js │ ├── karmaConfigBrowserStack.js │ ├── karmaConfigHeadless.js │ ├── karmaConfigHeadlessCI.js │ └── karmaConfigLocal.js └── webpackConfig.js ├── docs └── images │ └── Browserstack-logo@2x.png ├── package-lock.json ├── package.json ├── src ├── HOCs │ ├── imgixProvider.jsx │ ├── index.js │ └── shouldComponentUpdateHOC.jsx ├── HOFs │ ├── constants.js │ ├── index.js │ ├── propFormatter.js │ └── propMerger.js ├── array-findindex.js ├── common.js ├── config.js ├── constants.js ├── constructUrl.js ├── extractQueryParams.js ├── findClosest.js ├── index.js ├── react-imgix-bg.jsx ├── react-imgix.jsx └── targetWidths.js └── test ├── helpers ├── index.js └── shallowUntilTarget.js ├── integration └── react-imgix.test.jsx ├── setup.js ├── setupIntegration.js ├── setupUnit.js ├── tests.webpack.js └── unit ├── attributes.test.js ├── build-url.test.js ├── collapse-params.test.js ├── config.test.js ├── encoding.test.js ├── extract-query-params.test.js ├── find-closest.test.js ├── format-props.test.js ├── format-src.test.js ├── helpers └── shallowUntilTarget.test.jsx ├── imgix-provider.test.jsx ├── propMerger.test.js ├── react-imgix.test.jsx └── should-component-update.test.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "frederickfogerty", 10 | "name": "Frederick Fogerty", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/615334?v=4", 12 | "profile": "https://github.com/frederickfogerty", 13 | "contributions": [ 14 | "code", 15 | "doc", 16 | "maintenance", 17 | "question" 18 | ] 19 | }, 20 | { 21 | "login": "sherwinski", 22 | "name": "sherwinski", 23 | "avatar_url": "https://avatars3.githubusercontent.com/u/15919091?v=4", 24 | "profile": "https://github.com/sherwinski", 25 | "contributions": [ 26 | "code", 27 | "doc", 28 | "maintenance", 29 | "question" 30 | ] 31 | }, 32 | { 33 | "login": "jayeb", 34 | "name": "Jason Eberle", 35 | "avatar_url": "https://avatars2.githubusercontent.com/u/609840?v=4", 36 | "profile": "http://jayeb.com", 37 | "contributions": [ 38 | "code", 39 | "doc", 40 | "maintenance", 41 | "question" 42 | ] 43 | }, 44 | { 45 | "login": "paulstraw", 46 | "name": "Paul Straw", 47 | "avatar_url": "https://avatars2.githubusercontent.com/u/117288?v=4", 48 | "profile": "https://paulstraw.com", 49 | "contributions": [ 50 | "maintenance" 51 | ] 52 | }, 53 | { 54 | "login": "kellysutton", 55 | "name": "Kelly Sutton", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/47004?v=4", 57 | "profile": "https://kellysutton.com", 58 | "contributions": [ 59 | "maintenance" 60 | ] 61 | }, 62 | { 63 | "login": "rbliss", 64 | "name": "Richard Bliss", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/108509?v=4", 66 | "profile": "https://github.com/rbliss", 67 | "contributions": [ 68 | "code", 69 | "test" 70 | ] 71 | }, 72 | { 73 | "login": "ekosz", 74 | "name": "Eric Koslow", 75 | "avatar_url": "https://avatars1.githubusercontent.com/u/212829?v=4", 76 | "profile": "https://github.com/ekosz", 77 | "contributions": [ 78 | "code", 79 | "doc" 80 | ] 81 | }, 82 | { 83 | "login": "baldurh", 84 | "name": "Baldur Helgason", 85 | "avatar_url": "https://avatars1.githubusercontent.com/u/1823617?v=4", 86 | "profile": "https://github.com/baldurh", 87 | "contributions": [ 88 | "code" 89 | ] 90 | }, 91 | { 92 | "login": "modosc", 93 | "name": "jonathan schatz", 94 | "avatar_url": "https://avatars3.githubusercontent.com/u/2231664?v=4", 95 | "profile": "https://github.com/modosc", 96 | "contributions": [ 97 | "code" 98 | ] 99 | }, 100 | { 101 | "login": "theolampert", 102 | "name": "Theo", 103 | "avatar_url": "https://avatars3.githubusercontent.com/u/4714866?v=4", 104 | "profile": "http://theo.sh", 105 | "contributions": [ 106 | "code" 107 | ] 108 | }, 109 | { 110 | "login": "tstirrat15", 111 | "name": "Tanner Stirrat", 112 | "avatar_url": "https://avatars0.githubusercontent.com/u/2581423?v=4", 113 | "profile": "https://github.com/tstirrat15", 114 | "contributions": [ 115 | "code", 116 | "bug" 117 | ] 118 | }, 119 | { 120 | "login": "nickhavenly", 121 | "name": "Nicholas Suski", 122 | "avatar_url": "https://avatars0.githubusercontent.com/u/25750763?v=4", 123 | "profile": "https://github.com/nickhavenly", 124 | "contributions": [ 125 | "code" 126 | ] 127 | }, 128 | { 129 | "login": "minfawang", 130 | "name": "voiceup", 131 | "avatar_url": "https://avatars1.githubusercontent.com/u/8814693?v=4", 132 | "profile": "https://github.com/minfawang", 133 | "contributions": [ 134 | "code" 135 | ] 136 | }, 137 | { 138 | "login": "kochis", 139 | "name": "Craig Kochis", 140 | "avatar_url": "https://avatars3.githubusercontent.com/u/814934?v=4", 141 | "profile": "https://github.com/kochis", 142 | "contributions": [ 143 | "code" 144 | ] 145 | }, 146 | { 147 | "login": "dennisschaaf", 148 | "name": "Dennis Schaaf", 149 | "avatar_url": "https://avatars1.githubusercontent.com/u/116382?v=4", 150 | "profile": "https://github.com/dennisschaaf", 151 | "contributions": [ 152 | "code" 153 | ] 154 | }, 155 | { 156 | "login": "andykent", 157 | "name": "Andy Kent", 158 | "avatar_url": "https://avatars3.githubusercontent.com/u/614?v=4", 159 | "profile": "http://adkent.com", 160 | "contributions": [ 161 | "code" 162 | ] 163 | }, 164 | { 165 | "login": "GLosch", 166 | "name": "Gabby Losch", 167 | "avatar_url": "https://avatars2.githubusercontent.com/u/5502159?v=4", 168 | "profile": "https://github.com/GLosch", 169 | "contributions": [ 170 | "code" 171 | ] 172 | }, 173 | { 174 | "login": "stephencookdev", 175 | "name": "Stephen Cook", 176 | "avatar_url": "https://avatars1.githubusercontent.com/u/8496655?v=4", 177 | "profile": "https://stephencookdev.co.uk/", 178 | "contributions": [ 179 | "code", 180 | "bug" 181 | ] 182 | }, 183 | { 184 | "login": "enagorny", 185 | "name": "Eugene Nagorny", 186 | "avatar_url": "https://avatars0.githubusercontent.com/u/1202150?v=4", 187 | "profile": "https://github.com/enagorny", 188 | "contributions": [ 189 | "doc" 190 | ] 191 | }, 192 | { 193 | "login": "samuelgiles", 194 | "name": "Samuel Giles", 195 | "avatar_url": "https://avatars1.githubusercontent.com/u/2643026?v=4", 196 | "profile": "http://samuelgil.es", 197 | "contributions": [ 198 | "doc" 199 | ] 200 | }, 201 | { 202 | "login": "rexxars", 203 | "name": "Espen Hovlandsdal", 204 | "avatar_url": "https://avatars2.githubusercontent.com/u/48200?v=4", 205 | "profile": "https://espen.codes/", 206 | "contributions": [ 207 | "doc" 208 | ] 209 | }, 210 | { 211 | "login": "danielfarrell", 212 | "name": "Daniel Farrell", 213 | "avatar_url": "https://avatars2.githubusercontent.com/u/13850?v=4", 214 | "profile": "http://danielfarrell.com/", 215 | "contributions": [ 216 | "doc" 217 | ] 218 | }, 219 | { 220 | "login": "luizcieslak", 221 | "name": "Luiz Fernando da Silva Cieslak", 222 | "avatar_url": "https://avatars0.githubusercontent.com/u/14146176?v=4", 223 | "profile": "http://cieslak.dev", 224 | "contributions": [ 225 | "doc" 226 | ] 227 | }, 228 | { 229 | "login": "worldsoup", 230 | "name": "Nick Gottlieb", 231 | "avatar_url": "https://avatars2.githubusercontent.com/u/1475986?v=4", 232 | "profile": "https://github.com/worldsoup", 233 | "contributions": [ 234 | "doc" 235 | ] 236 | }, 237 | { 238 | "login": "pgrimaud", 239 | "name": "Pierre Grimaud", 240 | "avatar_url": "https://avatars1.githubusercontent.com/u/1866496?v=4", 241 | "profile": "https://github.com/pgrimaud", 242 | "contributions": [ 243 | "doc" 244 | ] 245 | }, 246 | { 247 | "login": "luqven", 248 | "name": "Luis H. Ball Jr.", 249 | "avatar_url": "https://avatars.githubusercontent.com/u/16711614?v=4", 250 | "profile": "http://www.luisball.com", 251 | "contributions": [ 252 | "code" 253 | ] 254 | } 255 | ], 256 | "contributorsPerLine": 7, 257 | "projectName": "react-imgix", 258 | "projectOwner": "imgix", 259 | "repoType": "github", 260 | "repoHost": "https://github.com", 261 | "skipCi": true 262 | } 263 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "modules": false 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-transform-object-assign", 13 | "inline-package-json", 14 | "transform-es2017-object-entries", 15 | "@babel/plugin-syntax-dynamic-import", 16 | "@babel/plugin-syntax-import-meta", 17 | "@babel/plugin-proposal-class-properties", 18 | "@babel/plugin-proposal-json-strings", 19 | [ 20 | "@babel/plugin-proposal-decorators", 21 | { 22 | "legacy": true 23 | } 24 | ], 25 | "@babel/plugin-proposal-function-sent", 26 | "@babel/plugin-proposal-export-namespace-from", 27 | "@babel/plugin-proposal-numeric-separator", 28 | "@babel/plugin-proposal-throw-expressions", 29 | "@babel/plugin-proposal-export-default-from", 30 | "@babel/plugin-proposal-logical-assignment-operators", 31 | "@babel/plugin-proposal-optional-chaining", 32 | [ 33 | "@babel/plugin-proposal-pipeline-operator", 34 | { 35 | "proposal": "minimal" 36 | } 37 | ], 38 | "@babel/plugin-proposal-nullish-coalescing-operator", 39 | "@babel/plugin-proposal-do-expressions", 40 | "@babel/plugin-proposal-function-bind" 41 | ], 42 | "env": { 43 | "commonjs": { 44 | "presets": [ 45 | "@babel/preset-react", 46 | "@babel/preset-env" 47 | ], 48 | "plugins": [ 49 | "@babel/plugin-syntax-dynamic-import", 50 | "@babel/plugin-syntax-import-meta", 51 | "@babel/plugin-proposal-class-properties", 52 | "@babel/plugin-proposal-json-strings", 53 | [ 54 | "@babel/plugin-proposal-decorators", 55 | { 56 | "legacy": true 57 | } 58 | ], 59 | "@babel/plugin-proposal-function-sent", 60 | "@babel/plugin-proposal-export-namespace-from", 61 | "@babel/plugin-proposal-numeric-separator", 62 | "@babel/plugin-proposal-throw-expressions", 63 | "@babel/plugin-proposal-export-default-from", 64 | "@babel/plugin-proposal-logical-assignment-operators", 65 | "@babel/plugin-proposal-optional-chaining", 66 | [ 67 | "@babel/plugin-proposal-pipeline-operator", 68 | { 69 | "proposal": "minimal" 70 | } 71 | ], 72 | "@babel/plugin-proposal-nullish-coalescing-operator", 73 | "@babel/plugin-proposal-do-expressions", 74 | "@babel/plugin-proposal-function-bind" 75 | ] 76 | }, 77 | "test": { 78 | "presets": [ 79 | "@babel/preset-react", 80 | "@babel/preset-env" 81 | ], 82 | "plugins": [ 83 | "@babel/plugin-syntax-dynamic-import", 84 | "@babel/plugin-syntax-import-meta", 85 | "@babel/plugin-proposal-class-properties", 86 | "@babel/plugin-proposal-json-strings", 87 | [ 88 | "@babel/plugin-proposal-decorators", 89 | { 90 | "legacy": true 91 | } 92 | ], 93 | "@babel/plugin-proposal-function-sent", 94 | "@babel/plugin-proposal-export-namespace-from", 95 | "@babel/plugin-proposal-numeric-separator", 96 | "@babel/plugin-proposal-throw-expressions", 97 | "@babel/plugin-proposal-export-default-from", 98 | "@babel/plugin-proposal-logical-assignment-operators", 99 | "@babel/plugin-proposal-optional-chaining", 100 | [ 101 | "@babel/plugin-proposal-pipeline-operator", 102 | { 103 | "proposal": "minimal" 104 | } 105 | ], 106 | "@babel/plugin-proposal-nullish-coalescing-operator", 107 | "@babel/plugin-proposal-do-expressions", 108 | "@babel/plugin-proposal-function-bind" 109 | ] 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.1.0 5 | browser-tools: circleci/browser-tools@1.4.8 # see https://github.com/CircleCI-Public/browser-tools-orb/issues/33#issuecomment-1081974320 and https://github.com/cypress-io/circleci-orb/issues/437 6 | 7 | jobs: 8 | test: 9 | parameters: 10 | version: 11 | default: "current" 12 | description: Node.JS version to install 13 | type: string 14 | docker: 15 | - image: cimg/node:<>-browsers 16 | resource_class: large 17 | steps: 18 | - checkout 19 | - browser-tools/install-browser-tools: 20 | install-geckodriver: false 21 | - run: 22 | command: | 23 | google-chrome --version 24 | firefox --version 25 | name: Check install 26 | # Prevents build error on stable node version 27 | - run: echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> $BASH_ENV 28 | - node/install-packages 29 | - run: npm run test 30 | - run: 31 | name: "Test whether build is successful" 32 | command: yarn build 33 | deploy: 34 | docker: 35 | - image: cimg/node:current 36 | steps: 37 | - checkout 38 | - node/install-packages 39 | - run: npx semantic-release --generate-notes false 40 | 41 | workflows: 42 | test: 43 | jobs: 44 | - test: 45 | matrix: 46 | parameters: 47 | version: 48 | - "current" 49 | - "lts" 50 | - deploy: 51 | requires: 52 | - test 53 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners for repo 2 | * @imgix/imgix-sdk-team 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your issue has been discussed before. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please ensure the problem has been isolated and reduced. This link explains more: http://css-tricks.com/6263-reduced-test-cases/ 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. Please strive to reach the **root problem** of your issue to avoid the XY problem. See more: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem 14 | 15 | **To Reproduce** 16 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Thus, the contributors need a way to reproduce your issue - if we can't reproduce your issue, we can't help you! Also, please be as detailed as possible. 17 | 18 | [a link to a codesandox or repl.it; here is a link to a codesandbox with @imgix/vue installed which can be forked: https://codesandbox.io/s/vue-imgix-base-codesandbox-bhz8n] 19 | 20 | [alternatively, please provide a code example] 21 | 22 | ```js 23 | // A *self-contained* demonstration of the problem follows... 24 | // This should be able to be dropped into a file with @imgix/vue installed and just work 25 | ``` 26 | 27 | Steps to reproduce the behaviour: 28 | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 4. See error 33 | 34 | **Expected behaviour** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Screenshots** 38 | If applicable, add screenshots to help explain your problem. 39 | 40 | **Information:** 41 | 42 | - @imgix/vue version: [e.g. v1.0] 43 | - browser version: [include link from [https://www.whatsmybrowser.org/](https://www.whatsmybrowser.org/) or details about the OS used and browser version] 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your feature has already been discussed. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | - [ ] Please take a moment to find out whether your idea fits with the scope and aims of the project 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of how this feature would function. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the project 4 | --- 5 | 6 | **Before you submit:** 7 | 8 | - [ ] Please read the [contributing guidelines](CONTRIBUTING.md) 9 | - [ ] Please search through the existing issues (both open AND closed) to see if your question has already been discussed. Github issue search can be used for this: https://github.com/imgix/vue/issues?utf8=%E2%9C%93&q=is%3Aissue 10 | 11 | **Question** 12 | A clear and concise description of your question 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Description 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ## Checklist 18 | 19 | 22 | 23 | 24 | 25 | - [ ] Read the [contributing guidelines](CONTRIBUTING.md). 26 | - [ ] Each commit follows the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) spec format. 27 | - [ ] Update the readme (if applicable). 28 | - [ ] Update or add any necessary API documentation (if applicable) 29 | - [ ] All existing unit tests are still passing (if applicable). 30 | 31 | 32 | 33 | - [ ] Add some [steps](#steps-to-test) so we can test your bug fix or feature (if applicable). 34 | - [ ] Add new passing unit tests to cover the code introduced by your PR (if applicable). 35 | - [ ] Any breaking changes are specified on the commit on which they are introduced with `BREAKING CHANGE` in the body of the commit. 36 | - [ ] If this is a big feature with breaking changes, consider opening an issue to discuss first. This is completely up to you, but please keep in mind that your PR might not be accepted. 37 | -------------------------------------------------------------------------------- /.github/worklows/add-issue-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - labeled 8 | 9 | jobs: 10 | add-to-project: 11 | name: Add issue to project 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/add-to-project@v0.3.0 15 | with: 16 | project-url: https://github.com/orgs/imgix/projects/4 17 | github-token: ${{ secrets.GH_TOKEN }} 18 | labeled: bug, needs-triage 19 | label-operator: OR 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | 3 | es 4 | lib 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | package.json 3 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>imgix/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [9.8.1](https://github.com/imgix/react-imgix/compare/v9.8.0...v9.8.1) (2023-12-12) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * updates background component to merge props ([06a3430](https://github.com/imgix/react-imgix/commit/06a3430df7e47e03ebaf122e2dbb32a0c0762fe4)) 7 | 8 | ## [9.8.0](https://github.com/imgix/react-imgix/compare/v9.7.0...v9.8.0) (2023-08-04) 9 | 10 | 11 | ### Features 12 | 13 | * add use client directive to index ([daa840f](https://github.com/imgix/react-imgix/commit/daa840fb643cc1ec97443e886c8d7d7f87d096ec)) 14 | 15 | ## [9.7.0](https://github.com/imgix/react-imgix/compare/v9.6.0...v9.7.0) (2023-02-02) 16 | 17 | 18 | ### Features 19 | 20 | * allow width and height to be passed through htmlAttributes ([c8ef392](https://github.com/imgix/react-imgix/commit/c8ef392e2f54c36fdd69d70d33bbbdcbe2cb961c)) 21 | 22 | ## [9.6.0](https://github.com/imgix/react-imgix/compare/v9.5.4...v9.6.0) (2023-01-11) 23 | 24 | 25 | ### Features 26 | 27 | * **react-imgix-bg:** optionally disable path encoding ([4a1e206](https://github.com/imgix/react-imgix/commit/4a1e206903773c5c3ed5e8685c2d84147e2ac3c3)) 28 | 29 | ### [9.5.4](https://github.com/imgix/react-imgix/compare/v9.5.3...v9.5.4) (2022-10-04) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * source width/height HTML validation ([5feb09b](https://github.com/imgix/react-imgix/commit/5feb09bf599c5adfadef0a51f9d4413d0d36aebf)) 35 | * stop background infinite rerenders ([a455b6d](https://github.com/imgix/react-imgix/commit/a455b6d0ef85dafa3a131ea8a6a2a38110f5bb46)) 36 | 37 | ### [9.5.3](https://github.com/imgix/react-imgix/compare/v9.5.2...v9.5.3) (2022-10-04) 38 | 39 | ### [9.5.2](https://github.com/imgix/react-imgix/compare/v9.5.1...v9.5.2) (2022-07-26) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **ci:** build project between deploy steps and publishing ([2cd0cdf](https://github.com/imgix/react-imgix/commit/2cd0cdf03e567134f855b13d6926d8e93f9c0d49)) 45 | 46 | ### [9.5.1](https://github.com/imgix/react-imgix/compare/v9.5.0...v9.5.1) (2022-05-11) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * add support for react 18 ([0e01aa4](https://github.com/imgix/react-imgix/commit/0e01aa4d737c490c8c3c86778e5a99fec021bd5b)) 52 | 53 | ### [9.5.1-beta.1](https://github.com/imgix/react-imgix/compare/v9.5.0...v9.5.1-beta.1) (2022-05-10) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * add support for react 18 ([1da0bc3](https://github.com/imgix/react-imgix/commit/1da0bc38c83ccd609e177a269bffede199288dc9)) 59 | 60 | ## [9.5.0](https://github.com/imgix/react-imgix/compare/v9.4.0...v9.5.0) (2022-02-28) 61 | 62 | 63 | ### Features 64 | 65 | * allow path encoding to be disabled for Imgix component ([045bb42](https://github.com/imgix/react-imgix/commit/045bb4203b2664ce81e8fba23a721cf956bc80ea)) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * bug where _inPicture would be set on 's that were children of ([4d57c1f](https://github.com/imgix/react-imgix/commit/4d57c1f3e208b31a42b43aeff294fc3f3ebccd76)) 71 | 72 | ## [9.4.0](https://github.com/imgix/react-imgix/compare/v9.3.1...v9.4.0) (2022-02-08) 73 | 74 | 75 | ### Features 76 | 77 | * add alt top level prop ([d39a959](https://github.com/imgix/react-imgix/commit/d39a959dbe7f298e6ce90cec40c0f8093faa2487)) 78 | 79 | ### [9.3.1](https://github.com/imgix/react-imgix/compare/v9.3.0...v9.3.1) (2022-01-28) 80 | 81 | ## [9.3.0](https://github.com/imgix/react-imgix/compare/v9.2.0...v9.3.0) (2021-08-05) 82 | 83 | 84 | ### Features 85 | 86 | * add support for customizing srcset generation ([a00cf3a](https://github.com/imgix/react-imgix/commit/a00cf3ab4e501c4345732e8b26ba10fdef7b039b)) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * replace individual props with single srcSetOptions prop ([9fc01fe](https://github.com/imgix/react-imgix/commit/9fc01fe971931b84e723534bf225a8a927ce9fcd)) 92 | * revert changes to package-lock.json ([d85b371](https://github.com/imgix/react-imgix/commit/d85b371f27d30bc2a5c263760bb6a50aeab6ec2d)) 93 | 94 | ## [9.2.0](https://github.com/imgix/react-imgix/compare/v9.1.5...v9.2.0) (2021-06-17) 95 | 96 | 97 | ### Features 98 | 99 | * add collapseImgixParams function to shorten imgix params ([97e7155](https://github.com/imgix/react-imgix/commit/97e7155839e0c0a90a96fca07321cd2c7d9ab55b)) 100 | * create imgixProvider component ([cb7608b](https://github.com/imgix/react-imgix/commit/cb7608bfe2e38cbcf1cd24b2f6a479994e34d0ab)) 101 | * create mergeComponentProps & processProps ([19684ed](https://github.com/imgix/react-imgix/commit/19684ed3f388ef3c7fc796ac90ddc2221b717bcc)) 102 | * recursive merge imgixParams & htmlAttributes ([34231c3](https://github.com/imgix/react-imgix/commit/34231c3096a446bc7dee69fcfe5ee4c0d63e2356)) 103 | * wrap ReactImgix with props HOFs ([07c1d0a](https://github.com/imgix/react-imgix/commit/07c1d0ab6e6215a42498a712e6a1bee3ef1e050c)) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * move consumer validation to avoid unnecessary warnings ([b034d8a](https://github.com/imgix/react-imgix/commit/b034d8abd48b23979f715338420d01a32764bbb6)) 109 | * remove compose ([7860d12](https://github.com/imgix/react-imgix/commit/7860d12cff90e6cc56bdc02cd4c34665240cf835)) 110 | * shadows prop spelling ([41971fb](https://github.com/imgix/react-imgix/commit/41971fbdb00e088e90189d6d390df998b1e17aec)) 111 | 112 | ### [9.1.5](https://github.com/imgix/react-imgix/compare/v9.1.4...v9.1.5) (2021-06-02) 113 | 114 | ### [9.1.4](https://github.com/imgix/react-imgix/compare/v9.1.3...v9.1.4) (2021-05-28) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * constants out of sync with versions ([a977614](https://github.com/imgix/react-imgix/commit/a977614648923e97c968d79b4b40fc00f77307c1)) 120 | 121 | # Changelog 122 | 123 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 124 | 125 | ### [9.1.3](https://github.com/imgix/react-imgix/compare/v9.1.2...v9.1.3) (2021-05-28) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * **789:** ensure children passed to background component update ([643b809](https://github.com/imgix/react-imgix/commit/643b809664096930a07eb83094a5f8e295f2e68d)) 131 | 132 | ### [9.1.2](https://github.com/imgix/react-imgix/compare/v9.1.1...v9.1.2) (2021-05-20) 133 | 134 | ### [9.1.1](https://github.com/imgix/react-imgix/compare/v9.1.0...v9.1.1) (2021-04-24) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * re-render Background only if a larger image is needed ([#782](https://github.com/imgix/react-imgix/issues/782)) ([53bb104](https://github.com/imgix/react-imgix/commit/53bb10437274068d9aaea8e2ead0851525fc4076)), closes [#763](https://github.com/imgix/react-imgix/issues/763) 140 | 141 | ## [9.1.0](https://github.com/imgix/react-imgix/compare/v9.0.4...v9.1.0) (2021-04-23) 142 | 143 | ### Features 144 | 145 | * integrate @imgix/js-core into react-imgix ([#780](https://github.com/imgix/react-imgix/issues/780)) ([690e7b6](https://github.com/imgix/react-imgix/commit/690e7b6279aba5aaef797230fdf59e17ae388c5e)), closes [#763](https://github.com/imgix/react-imgix/issues/763) 146 | 147 | ### [9.0.4](https://github.com/imgix/react-imgix/compare/v9.0.3...v9.0.4) (2021-04-14) 148 | 149 | # Changelog 150 | 151 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 152 | 153 | ### [9.0.3](https://github.com/imgix/react-imgix/compare/v9.0.2...v9.0.3) (2020-11-19) 154 | 155 | ### [9.0.2](https://github.com/imgix/react-imgix/compare/v9.0.1...v9.0.2) (2020-04-02) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * **background:** fortify `hasDOMDimensions` check for null height ([#592](https://github.com/imgix/react-imgix/issues/592)) ([c7fb86e](https://github.com/imgix/react-imgix/commit/c7fb86e)) 161 | 162 | ### [9.0.1](https://github.com/imgix/react-imgix/compare/v9.0.0...v9.0.1) (2019-11-22) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * prevent overwriting htmlAttributes.ref ([#496](https://github.com/imgix/react-imgix/issues/496)) ([e15e1b2](https://github.com/imgix/react-imgix/commit/e15e1b2)) 168 | 169 | # [9.0.0](https://github.com/imgix/react-imgix/compare/v8.6.3...v9.0.0) (2019-11-01) 170 | 171 | This release brings the react-imgix API more in-line with that of imgix's rendering service. 172 | 173 | The largest change users will notice is that this project's component will no longer generate a default `fit=crop` parameter. The original intention behind this was that generated images would maintain aspect ratio when at least one of the dimensions were specified. However, the default imgix API behavior [sets `fit=clip`](https://docs.imgix.com/apis/url/size/fit#clip), which is now reflected in this project. 174 | Although this may not cause breaking changes for all users, it can result in unusual rendered image behavior in some cases. As such, we would rather err on the side of caution and provide users the ability to opt in to these changes via a major release. 175 | 176 | If you are currently relying on the default generation of `fit=crop` when rendering images, you will now have to manually specify it when invoking the component: 177 | 178 | ```jsx 179 | 184 | ``` 185 | 186 | The other major change relates to how the component determines an image's aspect ratio. Instead of appending a calculated height `h=` value based on specified dimensions, the URL string will now be built using the [imgix aspect ratio parameter](https://blog.imgix.com/2019/07/17/aspect-ratio-parameter-makes-cropping-even-easier) `ar=`. Luckily, the interface for specifying an aspect ratio is no different from before. However, users will have to pass in the `fit=crop` parameter in order for it to take effect: 187 | 188 | ```jsx 189 | 194 | ``` 195 | 196 | ### Refactor 197 | 198 | * refactor: use ar parameter instead of calculating aspect ratio ([#462](https://github.com/imgix/react-imgix/pull/462)) ([fbe8082](https://github.com/imgix/react-imgix/commit/fbe8082ddce2d61b31bf19bf72b4d4b492ea0751)) 199 | * refactor: replace findDOMNode with callback refs ([#476](https://github.com/imgix/react-imgix/pull/476)) ([db3a1d7](https://github.com/imgix/react-imgix/commit/db3a1d70037b485fada972fe68b885d8ac6e4fb9)) 200 | 201 | ### Bug Fixes 202 | 203 | * remove default fit parameter ([#484](https://github.com/imgix/react-imgix/issues/484)) ([fbe8082](https://github.com/imgix/react-imgix/commit/fbe8082)) 204 | 205 | ### Chore 206 | 207 | * chore(clean): remove all deprecatedProps and types ([#483](https://github.com/imgix/react-imgix/pull/483])) ([d036132](https://github.com/imgix/react-imgix/commit/d0361323e46152ff8698e8e1d3bb2a44f79342c4)) 208 | 209 | ### [8.6.4](https://github.com/imgix/react-imgix/compare/v8.6.3...v8.6.4) (2019-08-08) 210 | 211 | 212 | ### Features 213 | 214 | * perf: optimize URL handling ([#414](https://github.com/imgix/react-imgix/pull/414)) ([8d14dcb](https://github.com/imgix/react-imgix/commit/8d14dcb)) 215 | * perf: optimize `constructUrl` function ([#418](https://github.com/imgix/react-imgix/pull/418)) ([8d392a0](https://github.com/imgix/react-imgix/commit/8d392a0)) 216 | * perf: use string concatenation instead of template strings ([#420](https://github.com/imgix/react-imgix/pull/420)) ([f41cc73](https://github.com/imgix/react-imgix/commit/f41cc73)) 217 | * perf: use `Object.assign` instead of spread operator ([#423](https://github.com/imgix/react-imgix/pull/423)) ([29b25d5](https://github.com/imgix/react-imgix/commit/29b25d5)) 218 | 219 | 220 | 221 | ### [8.6.3](https://github.com/imgix/react-imgix/compare/v8.6.2...v8.6.3) (2019-07-11) 222 | 223 | 224 | ### Bug Fixes 225 | 226 | * render element as a fluid image by default ([#404](https://github.com/imgix/react-imgix/issues/404)) ([10a5434](https://github.com/imgix/react-imgix/commit/10a5434)) 227 | * width query param overrides in srcSet ([#406](https://github.com/imgix/react-imgix/issues/406)) ([5791d11](https://github.com/imgix/react-imgix/commit/5791d11)) 228 | 229 | 230 | 231 | # [8.6.2](https://github.com/imgix/react-imgix/compare/v8.6.1...v8.6.2) (2019-07-05) 232 | 233 | 234 | ### Features 235 | 236 | * perf: optimize url construction ([#395](https://github.com/imgix/react-imgix/issues/395)) ([25c0012](https://github.com/imgix/react-imgix/commit/25c0012)) 237 | 238 | 239 | 240 | ## [8.6.1](https://github.com/imgix/react-imgix/compare/v8.6.0...v8.6.1) (2019-04-17) 241 | 242 | 243 | ### Bug Fixes 244 | 245 | * **deps:** pin react-measure version to avoid regression ([#343](https://github.com/imgix/react-imgix/issues/343)) ([3344502](https://github.com/imgix/react-imgix/commit/3344502)) 246 | 247 | 248 | 249 | # [8.6.0](https://github.com/imgix/react-imgix/compare/v8.5.1...v8.6.0) (2019-04-04) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * ensure `fit` parameter will respect overriding value, fixes [#268](https://github.com/imgix/react-imgix/issues/268) ([#311](https://github.com/imgix/react-imgix/issues/311)) ([15b0073](https://github.com/imgix/react-imgix/commit/15b0073)), fixes [#268](https://github.com/imgix/react-imgix/issues/268) 255 | 256 | 257 | ### Features 258 | 259 | * append variable q parameters per dpr when rendering a fixed-size image ([#322](https://github.com/imgix/react-imgix/issues/322)) ([6594cea](https://github.com/imgix/react-imgix/commit/6594cea)), resolves [#129](https://github.com/imgix/react-imgix/issues/129) 260 | 261 | 262 | 263 | 264 | ## [8.5.1](https://github.com/imgix/react-imgix/compare/v8.5.0...v8.5.1) (2018-12-21) 265 | 266 | 267 | 268 | 269 | # [8.5.0](https://github.com/imgix/react-imgix/compare/v8.4.0...v8.5.0) (2018-12-21) 270 | 271 | 272 | ### Features 273 | 274 | * add container to render image as background behind children [WIP] ([#236](https://github.com/imgix/react-imgix/issues/236)) ([5c3ecf6](https://github.com/imgix/react-imgix/commit/5c3ecf6)), closes [#160](https://github.com/imgix/react-imgix/issues/160) 275 | 276 | 277 | 278 | 279 | # [8.4.0](https://github.com/imgix/react-imgix/compare/v8.3.1...v8.4.0) (2018-11-26) 280 | 281 | 282 | ### Features 283 | 284 | * expose url builder api ([#225](https://github.com/imgix/react-imgix/issues/225)) ([ae9b31b](https://github.com/imgix/react-imgix/commit/ae9b31b)), closes [#131](https://github.com/imgix/react-imgix/issues/131) 285 | * use exponential increase for srcset widths ([#224](https://github.com/imgix/react-imgix/issues/224)) ([bc5660c](https://github.com/imgix/react-imgix/commit/bc5660c)) 286 | 287 | 288 | 289 | 290 | ## [8.3.1](https://github.com/imgix/react-imgix/compare/v8.3.0...v8.3.1) (2018-11-06) 291 | 292 | 293 | 294 | 295 | # [8.3.0](https://github.com/imgix/react-imgix/compare/v8.2.0...v8.3.0) (2018-10-11) 296 | 297 | 298 | ### Features 299 | 300 | * add aspect ratio support by calculating client-side ([#201](https://github.com/imgix/react-imgix/issues/201)) ([7ce0411](https://github.com/imgix/react-imgix/commit/7ce0411)), closes [#161](https://github.com/imgix/react-imgix/issues/161) 301 | 302 | 303 | 304 | 305 | # [8.2.0](https://github.com/imgix/react-imgix/compare/v8.1.0...v8.2.0) (2018-10-01) 306 | 307 | 308 | ### Features 309 | 310 | * make warnings able to be disabled ([#168](https://github.com/imgix/react-imgix/issues/168)) ([4ef0299](https://github.com/imgix/react-imgix/commit/4ef0299)) 311 | 312 | 313 | 314 | 315 | # [8.1.0](https://github.com/imgix/react-imgix/compare/v8.0.1...v8.1.0) (2018-09-13) 316 | 317 | 318 | ### Features 319 | 320 | * add HTML attribute configuration, enabling use of third-party libraries e.g. lazysizes ([#166](https://github.com/imgix/react-imgix/issues/166)) ([8ced390](https://github.com/imgix/react-imgix/commit/8ced390)) 321 | 322 | 323 | 324 | 325 | ## [8.0.1](https://github.com/imgix/react-imgix/compare/v8.0.0...v8.0.1) (2018-08-26) 326 | 327 | 328 | ### Bug Fixes 329 | 330 | * update typo in warnings about old type prop ([b9fa1e5](https://github.com/imgix/react-imgix/commit/b9fa1e5)) 331 | 332 | 333 | 334 | 335 | # [8.0.0](https://github.com/imgix/react-imgix/compare/v7.2.0...v8.0.0) (2018-08-15) 336 | 337 | This is a very large update to this library with a lot of breaking changes. We apologise for any issues this may cause, and we have tried to reduce the number of breaking changes. We have also worked to batch up all these changes into one release to reduce its impacts. We do not plan on making breaking changes for a while after this, and will be focussed on adding features. 338 | 339 | The largest change in this major version bump is the move to width-based `srcSet` and `sizes` for responsiveness. This has a host of benefits, including better server rendering, better responsiveness, less potential for bugs, and perfomance improvements. This does mean that the old fitting-to-container-size behaviour has been removed. If this is necessary, an example of how this can be achieved can be found [here](./examples/fit-to-size-of-container.md) 340 | 341 | Please see the [Upgrade Guide](https://github.com/imgix/react-imgix#7x-to-80) for more details on what to change. 342 | 343 | ### Bug Fixes 344 | 345 | * warn the user when no passed as a child to fixes [#90](https://github.com/imgix/react-imgix/issues/90) ([#151](https://github.com/imgix/react-imgix/issues/151)) ([aab9358](https://github.com/imgix/react-imgix/commit/aab9358)) 346 | 347 | 348 | ### Features 349 | 350 | * implement responsiveness with srcSet and sizes ([#159](https://github.com/imgix/react-imgix/issues/159)) ([fa68df6](https://github.com/imgix/react-imgix/commit/fa68df6)), closes [#158](https://github.com/imgix/react-imgix/issues/158) 351 | * reduce props API surface area ([#162](https://github.com/imgix/react-imgix/issues/162)) ([9fb0cb9](https://github.com/imgix/react-imgix/commit/9fb0cb9)) 352 | * refactor picture and source behaviour into different components ([#163](https://github.com/imgix/react-imgix/issues/163)) ([64d9b8a](https://github.com/imgix/react-imgix/commit/64d9b8a)) 353 | 354 | 355 | ### BREAKING CHANGES 356 | 357 | * picture and source types have been changed to components. 358 | * faces is no longer set by default. 359 | * srcSet behaviour has changed to use sizes + srcSets 360 | * type=bg has been removed 361 | * the following props have been removed: aggressiveLoad, component, fluid, precision, defaultHeight, defaultWidth 362 | * generateSrcSet has been changed to disableSrcSet 363 | * A fallback image will no longer be created when using react-imgix in picture mode 364 | 365 | 366 | 367 | 368 | # [7.2.0](https://github.com/imgix/react-imgix/compare/v7.1.1...v7.2.0) (2018-06-30) 369 | 370 | 371 | ### Bug Fixes 372 | 373 | * alt text no longer cause images to render at wrong dimensions ([#146](https://github.com/imgix/react-imgix/issues/146)) ([d3183a6](https://github.com/imgix/react-imgix/commit/d3183a6)), closes [#41](https://github.com/imgix/react-imgix/issues/41) 374 | * typo in CONTRIBUTING ([74b996e](https://github.com/imgix/react-imgix/commit/74b996e)) 375 | 376 | 377 | ### Features 378 | 379 | * add ixlib url parameter to help Imgix support and analytics ([#145](https://github.com/imgix/react-imgix/issues/145)) ([44f3d32](https://github.com/imgix/react-imgix/commit/44f3d32)) 380 | 381 | 382 | 383 | 384 | 385 | ## [7.1.1](https://github.com/imgix/react-imgix/compare/v7.1.0...v7.1.1) (2017-12-15) 386 | 387 | 388 | 389 | # [7.1.0](https://github.com/imgix/react-imgix/compare/v7.0.0...v7.1.0) (2017-12-15) 390 | 391 | ### Features 392 | 393 | - document default height/width props ([#126](https://github.com/imgix/react-imgix/issues/126)) ([a792592](https://github.com/imgix/react-imgix/commit/a792592)) 394 | 395 | 396 | 397 | # [7.0.0](https://github.com/imgix/react-imgix/compare/v6.0.2...v7.0.0) (2017-11-17) 398 | 399 | ### Chores 400 | 401 | - **deps:** update all deps, move to react 16 for testing ([#120](https://github.com/imgix/react-imgix/issues/120)) ([99f1f14](https://github.com/imgix/react-imgix/commit/99f1f14)) 402 | 403 | ### BREAKING CHANGES 404 | 405 | - **deps:** only React v16 is now actively supported 406 | 407 | # v6.0.2 / 2017-10-26 408 | 409 | - Add single quotes to background url. (#119) - thanks @nickhavenly 410 | 411 | # v6.0.0 / 2017-05-05 412 | 413 | ### Breaking Changes 414 | 415 | - **React 0.14 no longer supported.** This `react-imgix` version drops official support for React 0.14. This package will probably still work with 0.14, but we will not accept bugs or issues relating to React 0.14. 416 | - **bg prop removed**. This prop was deprecated in the past, and has now been removed. Please upgrade all usages to: `type='bg'`. 417 | 418 | ### Important Note 419 | 420 | Since this package now uses `prop-types`, when using a React version below 15.5, there will be duplicate propTypes. To fix this, please upgrade to 15.5, which no longer exports React.PropTypes. 421 | 422 | ### Thanks 423 | 424 | A massive thanks for @modosc for helping with this release, and upgrading to React 15.5. 425 | 426 | - fix React 15.5 warnings (#104) - @modosc 427 | - pull in prop-types from a separate module, fix sinon deprecation warning (#102) 428 | - Update travis config 429 | - Run prettier on code 430 | - Change prettier to 120 line length 431 | - Use prettier rather than standard 432 | - Add prettier 433 | - Update deps 434 | 435 | # v5.4.0 / 2017-04-06 436 | 437 | - add onMounted callback with access to underlying node (#94) 438 | 439 | # v5.3.0 / 2017-03-24 440 | 441 | - Background Size adjustments (#89) 442 | - fix typo - deprecated warning (#81) 443 | 444 | # v5.2.0 / 2016-12-02 445 | 446 | - Picture element (#60) - thanks @modosc 447 | 448 | # v5.1.0 / 2016-11-15 449 | 450 | - **Added:** `crop` prop to override `crop` url parameter #57 - @rbliss 451 | - Add additional tests testing crop prop overriding faces and entropy props 452 | - Enable passing in specific crop options, useful for specifying fallbacks to the ‘faces’ crop option 453 | - Add server-side rendering note to `aggressiveLoad` 454 | - Merge pull request #53 from imgix/fred-new-node-versions 455 | - lts => 6 456 | - Avoid blank background urls and src attributes (#51) 457 | - Update node versions supported 458 | - chore(package): update standard to version 8.2.0 (#47) 459 | - chore(package): update mocha to version 3.0.1 (#37) 460 | 461 | # v5.0.0 / 2016-07-22 462 | 463 | - **Breaking:** Unused props on the Imgix component are no longer passed down, use imgProps instead. #34 - @theolampert 464 | 465 | # v4.0.0 / 2016-06-07 466 | 467 | - **Breaking:** Images with a height of 1 (i.e. 1 x image height) were being rendered as 1px high images. Oops. Now it no longer does that. ([#27]) 468 | 469 | [#27]: https://github.com/imgix/react-imgix/pull/27 470 | 471 | # v3.0.0 / 2016-05-11 472 | 473 | - Bump version to 3.0.0 474 | - Merge pull request #24 from imgix/23-aggressiveLoad-typo 475 | - Rename `aggresiveLoad` to `aggressiveLoad`. 476 | - Merge pull request #20 from imgix/url-and-base64-encoding 477 | - Ensure all query keys + B64 variants are encoded. 478 | - Make version links work in changelog 479 | - Add Changelog 480 | 481 | # v2.2.0 / 2016-02-23 482 | 483 | - **Feature:** `forceLayout` api, accessed by `refs.imgix.forceLayout()` ([#15]) 484 | 485 | [#15]: https://github.com/imgix/react-imgix/pull/15 486 | 487 | - 2.2.0 488 | - Merge pull request #15 from theolampert/master 489 | - exposes forceLayout method to parent component 490 | 491 | # v2.1.2 / 2016-02-18 492 | 493 | - Update to Babel 6 ([#10]) 494 | - Change child props behaviour to only pass down props not used, not every prop ([#9]) 495 | 496 | [#10]: https://github.com/imgix/react-imgix/pull/10 497 | [#9]: https://github.com/imgix/react-imgix/pull/9 498 | 499 | - Bump version to 2.1.2 500 | - Merge pull request #9 from imgix/better-child-props 501 | - Change child props behaviour to only pass down props not used, not every prop 502 | - Merge pull request #10 from imgix/babel-6 503 | - Add `transform-object-assign` Babel plugin. 504 | - Change test commands 505 | - Update code to pass new class spec 506 | - Add babel 6 presets 507 | - Update tests to babel 6 508 | - Upgrade deps to babel 6 509 | - Change urls in package.json to imgix repo 510 | - Add code climate badge to readme 511 | 512 | # v2.1.1 / 2015-11-18 513 | 514 | - **Feature:** `generateSrcSet` prop to generate a `srcSet` attribute for images, only when in `img` mode. Enabled by default. See [here](https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-srcset/) for more ([#4]) 515 | 516 | [#4]: https://github.com/imgix/react-imgix/pull/4 517 | 518 | - 2.1.1 519 | - Add generateSrcSet prop to Readme 520 | - Bump to 2.1.0 521 | - Merge pull request #4 from ekosz/patch-1 522 | - Automatically set srcSet attributes 523 | - Merge pull request #3 from ekosz/patch-1 524 | - Document customParams in README 525 | - Update README.md 526 | 527 | # v2.0.0 / 2015-11-05 528 | 529 | - **Breaking:** React 0.13 no longer supported. React 0.13 users should use `v1.x` 530 | - **Breaking:** Sets `background-size: cover` on the element when the `bg` prop is passed 531 | - `react` and `react-dom` added as peer dependencies 532 | - No longer imports the entire imgix.js library. Instead we just build the url ourselves. 533 | - **Feature:** Added `entropy` prop to support [Point of Interest Cropping](http://blog.imgix.com/2015/10/21/automatic-point-of-interest-cropping-with-imgix%202.html) 534 | 535 | * 2.0.0 536 | * Add entropy to propTypes 537 | * And point of interest cropping as 'entropy' prop 538 | * Merge pull request #1 from imgix/feature/remove-imgixjs 539 | * Remove imgix.js from component 540 | * Remove imgix.js depenency 541 | * Use local uri builder than than imgix.js 542 | * :art: 543 | * Copy support.js from coursera/react-imgix, rather than importing the whole imgix.js library 544 | * Upgrade api to React 0.14, introduces breaking change as we no longer support React 0.13 545 | * Add react to peer dependencies 546 | * Move from chai to mjackson/expect for tests 547 | * Use a react version matrix for Travis 548 | * Change other urls due to repo transfer 549 | * Change Travis url due to transfer of repo 550 | * Add code style badge 551 | * Ignore npm-debug.log 552 | * Running the tests didn't actually make it into the test commit -.- 553 | * Don't support old versions of node 554 | * Add badges to README 555 | * Add .travis.yml 556 | * Add some initial tests with mocha 557 | * Set backgroundSize: cover on component when it's in background mode 558 | * Add license 559 | 560 | # v1.0.4 / 2015-10-04 561 | 562 | - 1.0.4 563 | - Don't mutate props (oops) 564 | - Update README.md 565 | 566 | # v1.0.3 / 2015-09-24 567 | 568 | - 1.0.3 569 | - Fix typo in Readme, add import usage 570 | 571 | # v1.0.2 / 2015-09-23 572 | 573 | - 1.0.2 574 | - Add installation instructions to README 575 | 576 | # v1.0.1 / 2015-09-23 577 | 578 | - 1.0.1 579 | - Add readme 580 | - Remove unused resize prop 581 | 582 | # v1.0.0 / 2015-09-23 583 | 584 | - 1.0.0 585 | - No tests are fine 586 | - Add npm_debug.log to .gitignore and .npmignore 587 | - Change to babel stage 0 588 | - Add ReactImgix component 589 | - Initial Commit 590 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please read the imgix [Code of Conduct](https://github.com/imgix/code-of-conduct). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for investing your time in contributing to this project! Please take a moment to review this document in order to streamline the contribution process for you and any reviewers involved. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## New contributor guide 10 | 11 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 12 | 13 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 14 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 15 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 16 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 17 | 18 | ## Opening a Pull Request 19 | 20 | _To help the project's maintainers and community quickly understand the nature of your pull request, please be sure to do the following:_ 21 | 22 | 1. Include a descriptive Pull Request title. 23 | 2. Provide a detailed description that explains the nature of the change(s) introduced. This is not only helpful for your reviewer, but also for future users who may need to revisit your Pull Request for context purposes. Screenshots/video captures are helpful here! 24 | 3. Make incremental, modular changes, with a clean commit history. This helps reviewers understand your contribution more easily and maintain project quality. 25 | 26 | ### Checklist 27 | 28 | Check to see that you have completed each of the following before requesting a review of your Pull Request: 29 | 30 | - [ ] All existing unit tests are still passing (if applicable) 31 | - [ ] Add new passing unit tests to cover the code introduced by your PR 32 | - [ ] Update the README 33 | - [ ] Update or add any necessary API documentation 34 | - [ ] All commits in the branch adhere to the [conventional commit](#conventional-commit-spec) format: e.g. `fix: bug #issue-number` 35 | 36 | ## Conventional Commit Spec 37 | 38 | Commits should be in the format `(): `. This allows our team to leverage tooling for automatic releases and changelog generation. An example of a commit in this format might be: `docs(readme): fix typo in documentation` 39 | 40 | `type` can be any of the follow: 41 | 42 | - `feat`: a feature, or breaking change 43 | - `fix`: a bug-fix 44 | - `test`: Adding missing tests or correcting existing tests 45 | - `docs`: documentation only changes (readme, changelog, contributing guide) 46 | - `refactor`: a code change that neither fixes a bug nor adds a feature 47 | - `chore`: reoccurring tasks for project maintainability (example scopes: release, deps) 48 | - `config`: changes to tooling configurations used in the project 49 | - `build`: changes that affect the build system or external dependencies (example scopes: npm, bundler, gradle) 50 | - `ci`: changes to CI configuration files and scripts (example scopes: travis) 51 | - `perf`: a code change that improves performance 52 | - `style`: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 53 | 54 | `scope` is optional, and can be anything. 55 | `description` should be a short description of the change, written in the imperative-mood. 56 | 57 | ### Example workflow 58 | 59 | Follow this process if you'd like your work considered for inclusion in the 60 | project: 61 | 62 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 63 | and configure the remotes: 64 | 65 | ```bash 66 | # Clone your fork of the repo into the current directory 67 | git clone git@github.com:/react-imgix.git 68 | # Navigate to the newly cloned directory 69 | cd react-imgix 70 | # Assign the original repo to a remote called "upstream" 71 | git remote add upstream https://github.com/imgix/react-imgix 72 | ``` 73 | 74 | 2. If you cloned a while ago, get the latest changes from upstream: 75 | 76 | ```bash 77 | git checkout 78 | git pull upstream 79 | ``` 80 | 81 | 3. Create a new topic branch (off the main project development branch) to 82 | contain your feature, change, or fix: 83 | 84 | ```bash 85 | git checkout -b 86 | ``` 87 | 88 | 4. Commit your changes in logical chunks. Use Git's 89 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 90 | feature to tidy up your commits before making them public. 91 | 92 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 93 | 94 | ```bash 95 | git pull [--rebase] upstream 96 | ``` 97 | 98 | 6. Push your topic branch up to your fork: 99 | 100 | ```bash 101 | git push origin 102 | ``` 103 | 104 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 105 | with a clear title and description. 106 | 107 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 108 | license your work under the same license as that used by the project. 109 | 110 | ### Using ES6 111 | 112 | To install all development dependencies, in the project's root directory, run 113 | 114 | ``` 115 | npm install 116 | ``` 117 | 118 | Once you're configured, building the JavaScript from the command line is easy: 119 | 120 | ```sh 121 | npm run build # build react-imgix from source 122 | npm run build:watch # watch for changes and build automatically 123 | npm run test # run the test suite 124 | npm run test:watch # run the test suite, watching for changes (good for TDD) 125 | ``` 126 | 127 | To run the browser integration tests, use one of the following commands: 128 | 129 | ```sh 130 | npm run test:headless # runs the tests in Chrome headlessly 131 | npm run test:headless:watch # runs the tests in Chrome headlessly and watches for changes 132 | npm run test:browser # runs the tests in Chrome, Firefox, and, if on OS X, Safari, headlessly (excl. Safari) 133 | npm run test:browser:watch # runs the tests in Chrome, Firefox, and, if on OS X, Safari, headlessly (excl. Safari), and watches for changes 134 | ``` 135 | 136 | To run the integration tests across all browsers, a BrowserStack account must be available. 137 | 138 | ```sh 139 | BUILD_TAG=test-xxx && BROWSERSTACK_USERNAME=xxx && BROWSERSTACK_ACCESS_KEY=xxx && npm run test:browser:all 140 | ``` 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015 by Frederick Fogerty 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /config/karma/karmaConfig.js: -------------------------------------------------------------------------------- 1 | const webpack = require("karma-webpack"); 2 | const webpackConfig = require("../webpackConfig"); 3 | const baseConfig = { 4 | frameworks: ["mocha"], 5 | files: ["../../test/tests.webpack.js"], 6 | plugins: [ 7 | webpack, 8 | require("karma-firefox-launcher"), 9 | require("karma-safari-launcher"), 10 | "karma-mocha", 11 | "karma-mocha-reporter", 12 | "karma-chrome-launcher" 13 | ], 14 | preprocessors: { 15 | "../../test/tests.webpack.js": "webpack" 16 | }, 17 | reporters: ["mocha"], 18 | captureTimeout: 100000, 19 | browserConnectTimeout: 3000, 20 | browserNoActivityTimeout: 60000, 21 | webpack: webpackConfig, 22 | webpackMiddleware: {}, 23 | concurrency: 5 24 | }; 25 | 26 | /** 27 | * Local testing - headless browsers 28 | */ 29 | 30 | const headlessConfig = karmaConfig => { 31 | process.env.CHROME_BIN = require("puppeteer").executablePath(); 32 | const headlessConfig = { 33 | ...baseConfig, 34 | browsers: ["ChromeHeadless"], 35 | webpack: { 36 | ...baseConfig.webpack, 37 | stats: "errors-only" 38 | }, 39 | webpackMiddleware: { 40 | ...baseConfig.webpackMiddleware, 41 | stats: "errors-only" 42 | } 43 | }; 44 | karmaConfig.set(headlessConfig); 45 | }; 46 | 47 | /** 48 | * Local testing - full browsers 49 | */ 50 | 51 | const localConfig = karmaConfig => { 52 | const browsers = [ 53 | "ChromeHeadless", 54 | "FirefoxHeadless", 55 | ...(process.platform === "darwin" ? ["Safari"] : []) 56 | ]; 57 | karmaConfig.set({ 58 | ...baseConfig, 59 | browsers, 60 | customLaunchers: { 61 | FirefoxHeadless: { 62 | base: "Firefox", 63 | flags: ["-headless"] 64 | } 65 | }, 66 | 67 | webpack: { 68 | ...baseConfig.webpack, 69 | stats: "errors-only" 70 | }, 71 | webpackMiddleware: { 72 | ...baseConfig.webpackMiddleware, 73 | stats: "errors-only" 74 | } 75 | }); 76 | }; 77 | 78 | /** 79 | * CI testing - Chrome, Firefox, and (if available) BrowserStack 80 | */ 81 | 82 | const headlessConfigCI = karmaConfig => { 83 | const config = { 84 | ...baseConfig, 85 | reporters: [...baseConfig.reporters], 86 | browsers: ["ChromeTravis", "FirefoxHeadless"], 87 | customLaunchers: { 88 | ChromeTravis: { 89 | base: "ChromeHeadless", 90 | flags: ["--no-sandbox"] 91 | }, 92 | FirefoxHeadless: { 93 | base: "Firefox", 94 | flags: ["-headless"] 95 | } 96 | }, 97 | plugins: [...baseConfig.plugins], 98 | client: { 99 | mocha: { 100 | timeout: 20000 // 20 seconds 101 | } 102 | } 103 | }; 104 | 105 | karmaConfig.set(config); 106 | }; 107 | 108 | const availableOnWindows = browser => 109 | ["ie", "edge", "chrome", "firefox"].includes(browser); 110 | const availableOnOSX = browser => 111 | ["safari", "chrome", "firefox"].includes(browser); 112 | 113 | const oldestVersionFromRange = versionRange => { 114 | if (versionRange.includes("-")) { 115 | return versionRange.split("-")[0]; 116 | } 117 | return versionRange; 118 | }; 119 | 120 | const isDesktop = browserOrPlatform => 121 | ["chrome", "firefox", "safari", "edge", "ie"].includes(browserOrPlatform); 122 | 123 | // Update these values from https://www.browserstack.com/automate/capabilities#test-configuration-capabilities if the build fails 124 | const getOSVersionAndDeviceForMobileChromeVersion = version => { 125 | if (Number.parseFloat(version) >= 5) { 126 | // Have to approximate os version as chrome versions are not tied to android versions after 4.4 127 | return { 128 | os_version: "8.0", 129 | device: "Google Pixel 2" 130 | }; 131 | } 132 | return { 133 | os_version: "4.4", 134 | device: "Samsung Galaxy Tab 4" 135 | }; 136 | }; 137 | const getOSVersionAndDeviceForMobileSafariVersion = version => { 138 | const versionNumber = Number.parseFloat(version); 139 | if (10 <= versionNumber && versionNumber < 11) { 140 | return { 141 | os_version: "10.3", 142 | device: "iPhone 7" 143 | }; 144 | } 145 | if (11 <= versionNumber && versionNumber < 12) { 146 | return { 147 | os_version: "11.0", 148 | device: "iPhone 8" 149 | }; 150 | } 151 | if (12 <= versionNumber && versionNumber < 13) { 152 | return { 153 | os_version: "12.1", 154 | device: "iPhone XS" 155 | }; 156 | } 157 | // Try run test if version number is outside expected range 158 | return { 159 | os_version: `${versionNumber}.0`, 160 | device: "iPhone XS" 161 | }; 162 | }; 163 | 164 | const ensureBrowserVersionExistsOnBrowserStack = (browser, version) => { 165 | const versionNumber = Number.parseFloat(version); 166 | if (browser.toLowerCase() === "safari") { 167 | if (11 <= versionNumber && versionNumber < 12) { 168 | return "11.1"; 169 | } 170 | if (10 <= versionNumber && versionNumber < 11) { 171 | return "10.1"; 172 | } 173 | } 174 | return version; 175 | }; 176 | const ensureOSXVersionIsCorrect = (browser, version) => { 177 | const versionNumber = Number.parseFloat(version); 178 | if (browser.toLowerCase() === "safari") { 179 | if (12 <= versionNumber && versionNumber < 13) { 180 | return "Mojave"; 181 | } 182 | if (11 <= versionNumber && versionNumber < 12) { 183 | return "High Sierra"; 184 | } 185 | if (10 <= versionNumber && versionNumber < 11) { 186 | return "Sierra"; 187 | } 188 | } 189 | return "High Sierra"; 190 | }; 191 | 192 | const mapBrowsersListToBrowserStackLaunchers = browserslistList => { 193 | let browserStackConfigurationObjects = {}; 194 | browserslistList.forEach(browsersListItem => { 195 | const [browserOrPlatform, versionRange] = browsersListItem.split(" "); 196 | const version = oldestVersionFromRange(versionRange); 197 | if (isDesktop(browserOrPlatform)) { 198 | const versionSafe = ensureBrowserVersionExistsOnBrowserStack( 199 | browserOrPlatform, 200 | version 201 | ); 202 | if (availableOnWindows(browserOrPlatform)) { 203 | browserStackConfigurationObjects[ 204 | `bs_${browserOrPlatform}_${versionSafe}_windows` 205 | ] = { 206 | base: "BrowserStack", 207 | browser: browserOrPlatform, 208 | browser_version: versionSafe, 209 | os: "WINDOWS", 210 | os_version: "10" 211 | }; 212 | } 213 | if (availableOnOSX(browserOrPlatform)) { 214 | browserStackConfigurationObjects[ 215 | `bs_${browserOrPlatform}_${versionSafe}_os_x` 216 | ] = { 217 | base: "BrowserStack", 218 | browser: browserOrPlatform, 219 | browser_version: versionSafe, 220 | os: "OS X", 221 | os_version: ensureOSXVersionIsCorrect(browserOrPlatform, versionSafe) 222 | }; 223 | } 224 | } else { 225 | const isIOS = browserOrPlatform === "ios_saf"; 226 | const { os_version, device } = isIOS 227 | ? getOSVersionAndDeviceForMobileSafariVersion(version) 228 | : getOSVersionAndDeviceForMobileChromeVersion(version); 229 | browserStackConfigurationObjects[ 230 | `bs_${browserOrPlatform}_${device}_${os_version}` 231 | ] = { 232 | base: "BrowserStack", 233 | device, 234 | real_mobile: true, 235 | os: isIOS ? "ios" : "android", 236 | os_version 237 | }; 238 | } 239 | }); 240 | return { 241 | browsers: Object.keys(browserStackConfigurationObjects), 242 | customLaunchers: browserStackConfigurationObjects 243 | }; 244 | }; 245 | 246 | const fullConfig = karmaConfig => { 247 | if (!isBrowserStackAvailable()) { 248 | console.log("BrowserStack not available"); 249 | process.exit(0); 250 | } 251 | 252 | const browserslist = require("browserslist"); 253 | const { 254 | browsers: bsBrowsers, 255 | customLaunchers: customBSLaunchers 256 | } = mapBrowsersListToBrowserStackLaunchers(browserslist()); 257 | const bsBrowsersWithoutChromeAndFirefox = bsBrowsers.filter( 258 | browser => !(browser.includes("chrome") || browser.includes("firefox")) 259 | ); 260 | 261 | const config = { 262 | ...baseConfig, 263 | 264 | hostname: "bs-local.com", 265 | port: 9876, 266 | browserStack: { 267 | username: process.env.BROWSERSTACK_USERNAME, 268 | accessKey: process.env.BROWSERSTACK_ACCESS_KEY 269 | }, 270 | 271 | browsers: bsBrowsersWithoutChromeAndFirefox, 272 | reporters: [...baseConfig.reporters], 273 | customLaunchers: customBSLaunchers, 274 | plugins: [...baseConfig.plugins, "karma-browserstack-launcher"], 275 | client: { 276 | mocha: { 277 | timeout: 20000 // 20 seconds 278 | } 279 | } 280 | }; 281 | 282 | console.log( 283 | "Testing on browsers:\n", 284 | config.browsers.map(browser => ` - ${browser}`).join("\n") 285 | ); 286 | 287 | karmaConfig.set(config); 288 | }; 289 | 290 | function isBrowserStackAvailable() { 291 | if ( 292 | !(process.env.BROWSERSTACK_USERNAME && process.env.BROWSERSTACK_ACCESS_KEY) 293 | ) { 294 | return false; 295 | } 296 | 297 | // If the slugs are different, the PR comes from an outsider (not a collaborator) 298 | return ( 299 | process.env.TRAVIS_EVENT_TYPE !== "pull_request" || 300 | process.env.TRAVIS_PULL_REQUEST_SLUG === process.env.TRAVIS_REPO_SLUG 301 | ); 302 | } 303 | 304 | exports.full = fullConfig; 305 | exports.local = localConfig; 306 | exports.headless = headlessConfig; 307 | exports.headlessCI = headlessConfigCI; 308 | -------------------------------------------------------------------------------- /config/karma/karmaConfigBrowserStack.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karmaConfig").full; 2 | -------------------------------------------------------------------------------- /config/karma/karmaConfigHeadless.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karmaConfig").headless; 2 | -------------------------------------------------------------------------------- /config/karma/karmaConfigHeadlessCI.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karmaConfig").headlessCI; 2 | -------------------------------------------------------------------------------- /config/karma/karmaConfigLocal.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./karmaConfig").local; 2 | -------------------------------------------------------------------------------- /config/webpackConfig.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | module.exports = { 3 | mode: "development", 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.(js|jsx)$/, 8 | exclude: /node_modules/, 9 | use: [ 10 | { 11 | loader: "babel-loader" 12 | } 13 | ] 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: ["*", ".js", ".jsx"], 19 | modules: [path.resolve(__dirname, "../src"), "node_modules"] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /docs/images/Browserstack-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgix/react-imgix/7293fd716824443fe545f1a7f861b5a8cf84ccef/docs/images/Browserstack-logo@2x.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imgix", 3 | "version": "9.10.0", 4 | "description": "React Component for displaying an image from Imgix", 5 | "author": "Frederick Fogerty (https://github.com/frederickfogerty)", 6 | "contributors": [ 7 | "Frederick Fogerty (https://github.com/frederickfogerty)", 8 | "Max Kolyanov (https://github.com/maxkolyanov)", 9 | "Sherwin Heydarbeygi (https://github.com/sherwinski)", 10 | "Baldur Helgason (https://github.com/baldurh)", 11 | "Tanner Stirrat (https://github.com/tstirrat15)", 12 | "Stephen Cook (https://github.com/stephencookdev)", 13 | "Cecchi MacNaughton (https://github.com/cecchi)" 14 | ], 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/imgix/react-imgix/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/imgix/react-imgix.git" 22 | }, 23 | "homepage": "https://github.com/imgix/react-imgix#readme", 24 | "keywords": [ 25 | "react" 26 | ], 27 | "main": "lib/index.js", 28 | "module": "es/index.js", 29 | "jsnext:main": "es/index.js", 30 | "files": [ 31 | "lib/*", 32 | "es/*" 33 | ], 34 | "scripts": { 35 | "build": "npm run clean && npm run build:commonjs && npm run build:es", 36 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib --source-maps", 37 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es --source-maps", 38 | "build:watch": "cross-env BABEL_ENV=es babel src --out-dir es --watch", 39 | "clean": "rimraf lib es", 40 | "prepare": "npm run build", 41 | "prepublishOnly": "npm run build", 42 | "pretty": "prettier --write '{src,test}/**/*.{js,jsx}'", 43 | "release": "standard-version", 44 | "test:headless": "karma start config/karma/karmaConfigHeadless.js --singleRun", 45 | "test:headless:watch": "karma start config/karma/karmaConfigHeadless.js", 46 | "test:browser": "karma start config/karma/karmaConfigLocal.js --singleRun", 47 | "test:browser:watch": "karma start config/karma/karmaConfigLocal.js", 48 | "test:browser:ci": "karma start config/karma/karmaConfigHeadlessCI.js --singleRun", 49 | "test:browser:browserstack": "karma start config/karma/karmaConfigBrowserStack.js --singleRun", 50 | "test:browser:all": "npm run test:browser:ci && npm run test:browser:browserstack", 51 | "test:watch": "jest --watch", 52 | "test": "jest && npm run test:browser:all" 53 | }, 54 | "peerDependencies": { 55 | "react": "15.x || 16.x || 17.x || 18.x", 56 | "react-dom": "15.x || 16.x || 17.x || 18.x" 57 | }, 58 | "dependencies": { 59 | "@imgix/js-core": "^3.1.3", 60 | "prop-types": "^15.8.1", 61 | "react-measure": "^2.3.0", 62 | "shallowequal": "^1.1.0", 63 | "warning": "^4.0.1" 64 | }, 65 | "devDependencies": { 66 | "@babel/cli": "7.25.9", 67 | "@babel/core": "7.26.0", 68 | "@babel/plugin-proposal-class-properties": "7.18.6", 69 | "@babel/plugin-proposal-decorators": "7.25.9", 70 | "@babel/plugin-proposal-do-expressions": "7.25.9", 71 | "@babel/plugin-proposal-export-default-from": "7.25.9", 72 | "@babel/plugin-proposal-export-namespace-from": "7.18.9", 73 | "@babel/plugin-proposal-function-bind": "7.25.9", 74 | "@babel/plugin-proposal-function-sent": "7.25.9", 75 | "@babel/plugin-proposal-json-strings": "7.18.6", 76 | "@babel/plugin-proposal-logical-assignment-operators": "7.20.7", 77 | "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", 78 | "@babel/plugin-proposal-numeric-separator": "7.18.6", 79 | "@babel/plugin-proposal-optional-chaining": "7.21.0", 80 | "@babel/plugin-proposal-pipeline-operator": "7.25.9", 81 | "@babel/plugin-proposal-throw-expressions": "7.25.9", 82 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 83 | "@babel/plugin-syntax-import-meta": "7.10.4", 84 | "@babel/plugin-transform-object-assign": "7.25.9", 85 | "@babel/polyfill": "7.12.1", 86 | "@babel/preset-env": "7.26.0", 87 | "@babel/preset-react": "7.25.9", 88 | "@google/semantic-release-replace-plugin": "1.2.7", 89 | "@imgix/browserslist-config": "1.0.0", 90 | "@semantic-release/changelog": "6.0.3", 91 | "@semantic-release/git": "10.0.1", 92 | "babel-core": "7.0.0-bridge.0", 93 | "@babel/eslint-parser": "7.25.9", 94 | "babel-jest": "25.5.1", 95 | "babel-loader": "8.4.1", 96 | "babel-plugin-inline-package-json": "2.0.0", 97 | "babel-plugin-transform-es2017-object-entries": "0.0.5", 98 | "browserslist": "4.24.2", 99 | "chokidar": "^3.6.0", 100 | "common-tags": "1.8.2", 101 | "conventional-changelog-conventionalcommits": "7.0.2", 102 | "cross-env": "7.0.3", 103 | "enzyme": "3.11.0", 104 | "enzyme-adapter-react-16": "1.15.8", 105 | "expect": "25.5.0", 106 | "jest": "25.5.4", 107 | "jest-extended": "0.11.5", 108 | "jsuri": "1.3.1", 109 | "karma": "6.4.4", 110 | "karma-browserstack-launcher": "1.6.0", 111 | "karma-chrome-launcher": "3.2.0", 112 | "karma-firefox-launcher": "2.1.3", 113 | "karma-mocha": "2.0.1", 114 | "karma-mocha-reporter": "2.2.5", 115 | "karma-safari-launcher": "1.0.0", 116 | "karma-webpack": "4.0.2", 117 | "mocha": "10.8.2", 118 | "prettier": "3.2.5", 119 | "puppeteer": "22.6.5", 120 | "react": "16.14.0", 121 | "react-addons-test-utils": "15.6.2", 122 | "react-dom": "16.14.0", 123 | "react-test-renderer": "16.14.0", 124 | "read-pkg-up": "5.0.0", 125 | "rimraf": "5.0.10", 126 | "sinon": "17.0.1", 127 | "skin-deep": "1.2.0", 128 | "standard-version": "9.5.0", 129 | "webpack": "4.47.0", 130 | "webpack-cli": "4.10.0", 131 | "webpack-dev-server": "4.15.2" 132 | }, 133 | "jest": { 134 | "roots": [ 135 | "/src/", 136 | "/test/unit/" 137 | ], 138 | "moduleDirectories": [ 139 | "node_modules", 140 | "src" 141 | ], 142 | "setupFilesAfterEnv": [ 143 | "/test/setupUnit.js" 144 | ] 145 | }, 146 | "browserslist": [ 147 | "extends @imgix/browserslist-config" 148 | ], 149 | "release": { 150 | "branches": [ 151 | "main", 152 | { 153 | "name": "next", 154 | "prerelease": "rc" 155 | }, 156 | { 157 | "name": "beta", 158 | "prerelease": true 159 | }, 160 | { 161 | "name": "alpha", 162 | "prerelease": true 163 | } 164 | ], 165 | "plugins": [ 166 | [ 167 | "@semantic-release/commit-analyzer", 168 | { 169 | "releaseRules": [ 170 | { 171 | "type": "docs", 172 | "release": "patch" 173 | }, 174 | { 175 | "type": "deps", 176 | "scope": "deps", 177 | "release": "patch" 178 | } 179 | ] 180 | } 181 | ], 182 | [ 183 | "@semantic-release/release-notes-generator", 184 | { 185 | "preset": "conventionalcommits", 186 | "writerOpts": { 187 | "types": [ 188 | { 189 | "type": "feat", 190 | "section": "Features" 191 | }, 192 | { 193 | "type": "fix", 194 | "section": "Bug Fixes" 195 | }, 196 | { 197 | "type": "docs", 198 | "section": "Documentation", 199 | "hidden": false 200 | }, 201 | { 202 | "type": "deps", 203 | "section": "Dependency Updates", 204 | "hidden": false 205 | }, 206 | { 207 | "type": "chore", 208 | "hidden": true 209 | }, 210 | { 211 | "type": "style", 212 | "hidden": true 213 | }, 214 | { 215 | "type": "refactor", 216 | "hidden": true 217 | }, 218 | { 219 | "type": "perf", 220 | "hidden": true 221 | }, 222 | { 223 | "type": "test", 224 | "hidden": true 225 | } 226 | ] 227 | } 228 | } 229 | ], 230 | [ 231 | "@google/semantic-release-replace-plugin", 232 | { 233 | "replacements": [ 234 | { 235 | "files": [ 236 | "src/constants.js" 237 | ], 238 | "from": "const PACKAGE_VERSION = \".*\"", 239 | "to": "const PACKAGE_VERSION = \"${nextRelease.version}\"", 240 | "results": [ 241 | { 242 | "file": "src/constants.js", 243 | "hasChanged": true, 244 | "numMatches": 1, 245 | "numReplacements": 1 246 | } 247 | ], 248 | "countMatches": true 249 | } 250 | ] 251 | } 252 | ], 253 | "@semantic-release/changelog", 254 | "@semantic-release/npm", 255 | [ 256 | "@semantic-release/git", 257 | { 258 | "assets": [ 259 | "src/**", 260 | "package.json", 261 | "CHANGELOG.md" 262 | ], 263 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes} [skip ci]" 264 | } 265 | ], 266 | "@semantic-release/github" 267 | ] 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/HOCs/imgixProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, createContext } from "react"; 2 | 3 | const ImgixContext = createContext(); 4 | 5 | /** 6 | * `useImgixContext()` tries to invoke `React.useContext()`. If no context 7 | * is available, this function returns `undefined`. 8 | * @returns The context defined by the closest parent `ImgixProvider`. 9 | */ 10 | function useImgixContext() { 11 | return useContext(ImgixContext); 12 | } 13 | 14 | /** 15 | * Creates a Provider component that passes `reactImgixProps` as the Context 16 | * for child components who use the `useImgixContext()` custom hook or 17 | * `React.useContext()` API. 18 | * @param {React.Element } children 19 | * @param {Object} reactImgixProps 20 | * @returns React.Element 21 | */ 22 | function ImgixProvider({ children, ...reactImgixProps }) { 23 | const value = reactImgixProps; 24 | 25 | if (children == null || children.length < 1) { 26 | console.error("ImgixProvider must have at least one Imgix child component"); 27 | } 28 | return ( 29 | {children} 30 | ); 31 | } 32 | 33 | export { ImgixProvider, useImgixContext }; 34 | -------------------------------------------------------------------------------- /src/HOCs/index.js: -------------------------------------------------------------------------------- 1 | export * from "./shouldComponentUpdateHOC"; 2 | export * from "./imgixProvider"; 3 | -------------------------------------------------------------------------------- /src/HOCs/shouldComponentUpdateHOC.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { warning, shallowEqual } from "../common"; 4 | 5 | const ShouldComponentUpdateHOC = (WrappedComponent) => { 6 | class ShouldComponentUpdateHOC extends Component { 7 | shouldComponentUpdate = (nextProps) => { 8 | const props = this.props; 9 | warning( 10 | nextProps.onMounted == this.props.onMounted, 11 | "props.onMounted() is changing between renders. This is probably not intended. Ensure that a class method is being passed to Imgix rather than a function that is created every render. If this is intended, ignore this warning." 12 | ); 13 | 14 | const customizer = (oldProp, newProp, key) => { 15 | if (key === "children") { 16 | return shallowEqual(oldProp, newProp); 17 | } 18 | if (key === "imgixParams") { 19 | return shallowEqual(oldProp, newProp, (a, b) => { 20 | if (Array.isArray(a)) { 21 | return shallowEqual(a, b); 22 | } 23 | return undefined; 24 | }); 25 | } 26 | if (key === "htmlAttributes") { 27 | return shallowEqual(oldProp, newProp); 28 | } 29 | if (key === "attributeConfig") { 30 | return shallowEqual(oldProp, newProp); 31 | } 32 | return undefined; // handled by shallowEqual 33 | }; 34 | const propsAreEqual = shallowEqual(props, nextProps, customizer); 35 | return !propsAreEqual; 36 | }; 37 | render() { 38 | return ; 39 | } 40 | } 41 | ShouldComponentUpdateHOC.displayName = `ShouldComponentUpdateHOC(${WrappedComponent.displayName})`; 42 | return ShouldComponentUpdateHOC; 43 | }; 44 | 45 | export { ShouldComponentUpdateHOC }; 46 | -------------------------------------------------------------------------------- /src/HOFs/constants.js: -------------------------------------------------------------------------------- 1 | // @see https://www.imgix.com/docs/reference 2 | export const PARAMS_EXP_MAP = Object.freeze({ 3 | // Adjustment 4 | brightness: "bri", 5 | contrast: "con", 6 | exposure: "exp", 7 | gamma: "gam", 8 | highlights: "high", 9 | hue: "hue", 10 | invert: "invert", 11 | saturation: "sat", 12 | shaddows: "shad", 13 | shadows: "shad", 14 | sharpness: "sharp", 15 | "unsharp-mask": "usm", 16 | "unsharp-radius": "usmrad", 17 | vibrance: "vib", 18 | 19 | // Automatic 20 | "auto-features": "auto", 21 | 22 | // Background 23 | "background-color": "bg", 24 | 25 | // Blend 26 | blend: "blend", 27 | "blend-mode": "bm", 28 | "blend-align": "ba", 29 | "blend-alpha": "balph", 30 | "blend-padding": "bp", 31 | "blend-width": "bw", 32 | "blend-height": "bh", 33 | "blend-fit": "bf", 34 | "blend-crop": "bc", 35 | "blend-size": "bs", 36 | "blend-x": "bx", 37 | "blend-y": "by", 38 | 39 | // Border & Padding 40 | border: "border", 41 | padding: "pad", 42 | 43 | // Face Detection 44 | "face-index": "faceindex", 45 | "face-padding": "facepad", 46 | faces: "faces", 47 | 48 | // Format 49 | "chroma-subsampling": "chromasub", 50 | "color-quantization": "colorquant", 51 | download: "dl", 52 | DPI: "dpi", 53 | format: "fm", 54 | "lossless-compression": "lossless", 55 | quality: "q", 56 | 57 | // Mask 58 | "mask-image": "mask", 59 | 60 | // Mask 61 | "noise-blur": "nr", 62 | "noise-sharpen": "nrs", 63 | 64 | // Palette n/a 65 | // PDF n/a 66 | // Pixel Density n/a 67 | 68 | // Rotation 69 | "flip-direction": "flip", 70 | orientation: "or", 71 | "rotation-angle": "rot", 72 | 73 | // Size 74 | "crop-mode": "crop", 75 | "fit-mode": "fit", 76 | "image-height": "h", 77 | "image-width": "w", 78 | 79 | // Stylize 80 | blurring: "blur", 81 | halftone: "htn", 82 | monotone: "mono", 83 | pixelate: "px", 84 | "sepia-tone": "sepia", 85 | 86 | // Trim TODO 87 | // Watermark TODO 88 | 89 | // Extra 90 | height: "h", 91 | width: "w", 92 | }); 93 | -------------------------------------------------------------------------------- /src/HOFs/index.js: -------------------------------------------------------------------------------- 1 | export * from "./propMerger"; 2 | export * from "./propFormatter"; 3 | -------------------------------------------------------------------------------- /src/HOFs/propFormatter.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PARAMS_EXP_MAP } from "./constants"; 3 | 4 | /** 5 | * Creates a 1-step, or complete, URL from `domain` and `src` Strings. 6 | * 7 | * - First, the function checks if src has a defined `domain`. If it does, it 8 | * checks to see if `src` has a scheme, and prepends "http" or "https" as needed 9 | * - Otherwise, formatSrc formats `domain` and `src` Strings. 10 | * - First it strips the two strings of the leading and `/` or trailing `/` 11 | * slash characters. 12 | * - Then, it joins the two strings on a `/` character. IE, 13 | * `strippedDomain + "/" + strippedSrc`. 14 | * - If `domain` String argument `null` or `undefined`, the function returns 15 | * the original `src` String. 16 | * 17 | * @param {String} src - URL that is either 1-step or 2-step 18 | * @param {String} domain - Domain string, optional 19 | * @returns 1-step, or complete, URL String. Ex, _assets.ix.net/foo/bar.jpg_ 20 | */ 21 | export function formatSrc(src, domain, useHTTPS = true) { 22 | // ignore if already has protocol 23 | if (src.indexOf("://") !== -1) { 24 | return src; 25 | } else { 26 | // prepend domain if defined 27 | if (domain == null) { 28 | return src; 29 | } 30 | const strippedDomain = domain ? domain.replace(/^\/|\/$/g, "") : ""; 31 | const strippedSrc = src.replace(/^\/|\/$/g, ""); 32 | const prefix = useHTTPS ? "https://" : "http://"; 33 | return prefix + strippedDomain + "/" + strippedSrc; 34 | } 35 | } 36 | 37 | /** 38 | * A function that formats the following values in the props Object: 39 | * 40 | * - `width`: if undefined or negative gets set to `undefined`. 41 | * - `height`: if undefined or negative gets set to `undefined`. 42 | * - `src`: concatenated to `domain` if `src` defined and has no domain. 43 | * 44 | * @param {Object} props 45 | * @returns A formatted `props` Object. 46 | */ 47 | export const formatProps = (props) => { 48 | const width = !props.width || props.width <= 1 ? undefined : props.width; 49 | const height = !props.height || props.height <= 1 ? undefined : props.height; 50 | const src = props.src 51 | ? formatSrc(props.src, props.domain, props.useHttps) 52 | : undefined; 53 | 54 | return Object.assign({}, props, { width, height, src }); 55 | }; 56 | 57 | /** 58 | * Function that shortens params keys according to the imgix spec. 59 | * @param {Object} params - imgixParams object 60 | * @returns imgixParams object with shortened keys 61 | * @see https://www.imgix.com/docs/reference 62 | */ 63 | export const collapseImgixParams = (params) => { 64 | if (params == null) { 65 | return params; 66 | } 67 | const compactedParams = {}; 68 | for (const [k, v] of Object.entries(params)) { 69 | if (PARAMS_EXP_MAP[k]) { 70 | compactedParams[PARAMS_EXP_MAP[k]] = v; 71 | } else { 72 | compactedParams[k] = v; 73 | } 74 | } 75 | return compactedParams; 76 | }; 77 | 78 | /** 79 | * `processPropsHOF` takes a Component's props and formats them to adhere to the 80 | * ImgixClient's specifications. 81 | * 82 | * @param {React.Element} Component - A react component with 83 | * defined `props`. 84 | * @returns A React Component who's `props` have been formatted and 85 | * `imgixParams` have been collapsed. 86 | */ 87 | export const processPropsHOF = (Component) => (props) => { 88 | const formattedProps = formatProps(props); 89 | const formattedImgixParams = collapseImgixParams(formattedProps.imgixParams); 90 | 91 | return ; 92 | }; 93 | -------------------------------------------------------------------------------- /src/HOFs/propMerger.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useImgixContext } from "../HOCs"; 3 | 4 | /** 5 | * Merges the `src` object into the `destination` object. Destination values are 6 | * not overwritten by source values. Destination properties that resolve to 7 | * `undefined` or `null` are not overwritten if a destination value exists 8 | * unless destination key does not exist . It recursively merges the 9 | * `imgixParams` and `htmlAttributes` values. 10 | * 11 | * @param {Object} src - The Provider component's props object 12 | * @param {Object} destination - The child component's props object 13 | * @returns Object with the combined values from `src` & `destination` Objects 14 | * 15 | * @example 16 | * const src = { 17 | * width: 100, 18 | * height: 200, 19 | * imgixParams: { ar: "1:2", dpr: 2}, 20 | * htmlAttributes: { styles: "width: 50" } 21 | * } 22 | * const destination = { 23 | * width: 101, 24 | * height: 201, 25 | * imgixParams: { dpr: 1 }, 26 | * htmlAttributes: { styles: "width: 100" } 27 | * } 28 | * const result = mergeProps(src, destination); 29 | * 30 | * { 31 | * width: 101, 32 | * height: 201, 33 | * imgixParams: { ar: "1:2", dpr: 1 }, 34 | * htmlAttributes: { styles: "width: 100" } 35 | * } 36 | * 37 | */ 38 | export const mergeProps = (src, destination) => { 39 | if (src == null && destination !== null) { 40 | return destination; 41 | } 42 | if (src !== null && destination == null) { 43 | return src; 44 | } 45 | if (src == null && destination == null) { 46 | return {}; 47 | } 48 | 49 | const newProps = { ...destination }; 50 | const newPropKeys = Object.keys(newProps); 51 | 52 | for (const [k, v] of Object.entries(src)) { 53 | if (newPropKeys.indexOf(k) == -1 && v !== null) { 54 | newProps[k] = v; 55 | } 56 | // recursively merge imgixParams and htmlAttributes 57 | if (k === "imgixParams" || k === "htmlAttributes") { 58 | if (v !== null) { 59 | newProps[k] = mergeProps(src[k], newProps[k]); 60 | } 61 | } 62 | } 63 | return newProps; 64 | }; 65 | 66 | /** 67 | * `mergeComponentPropsHOF` tries to invoke `React.useContext()`. If context is 68 | * `undefined`, context is being accessed outside of an `ImgixContext` provider 69 | * and the Component is returned as is. 70 | * 71 | * Otherwise, it merges a Component's props with the `ImgixContext` props and 72 | * return a Component with the merged `props`. 73 | * @param {React.Element 77 | function mergeComponentPropsHOFInner(props) { 78 | const contextProps = useImgixContext(); 79 | if (contextProps == null) { 80 | return ; 81 | } 82 | 83 | const childProps = mergeProps(contextProps, props); 84 | return ; 85 | }; 86 | -------------------------------------------------------------------------------- /src/array-findindex.js: -------------------------------------------------------------------------------- 1 | // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex#Polyfill 2 | if (!Array.prototype.findIndex) { 3 | Object.defineProperty(Array.prototype, "findIndex", { 4 | value: function (predicate) { 5 | "use strict"; 6 | if (this == null) { 7 | throw new TypeError( 8 | "Array.prototype.findIndex called on null or undefined" 9 | ); 10 | } 11 | if (typeof predicate !== "function") { 12 | throw new TypeError("predicate must be a function"); 13 | } 14 | var list = Object(this); 15 | var length = list.length >>> 0; 16 | var thisArg = arguments[1]; 17 | var value; 18 | 19 | for (var i = 0; i < length; i++) { 20 | value = list[i]; 21 | if (predicate.call(thisArg, value, i, list)) { 22 | return i; 23 | } 24 | } 25 | return -1; 26 | }, 27 | enumerable: false, 28 | configurable: false, 29 | writable: false, 30 | }); 31 | } 32 | 33 | export default true; 34 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | export { default as warning } from "warning"; 2 | export { default as shallowEqual } from "shallowequal"; 3 | export { default as config } from "./config"; 4 | 5 | // Taken from https://github.com/reduxjs/redux/blob/v4.0.0/src/compose.js 6 | export function compose(...funcs) { 7 | if (funcs.length === 0) { 8 | return (arg) => arg; 9 | } 10 | 11 | if (funcs.length === 1) { 12 | return funcs[0]; 13 | } 14 | 15 | return funcs.reduce( 16 | (a, b) => 17 | (...args) => 18 | a(b(...args)) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | warnings: { 3 | fallbackImage: true, 4 | sizesAttribute: true, 5 | invalidARFormat: true, 6 | oversizeImage: true, 7 | lazyLCP: true 8 | }, 9 | }; 10 | 11 | const _setWarning = (name, value) => { 12 | if (!name || !(name in config.warnings)) { 13 | return; 14 | } 15 | config.warnings[name] = value; 16 | }; 17 | 18 | class PublicConfigAPI { 19 | static disableWarning(name) { 20 | _setWarning(name, false); 21 | } 22 | static enableWarning(name) { 23 | _setWarning(name, true); 24 | } 25 | } 26 | 27 | export default config; 28 | export { PublicConfigAPI }; 29 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const PACKAGE_VERSION = "9.10.0"; 2 | -------------------------------------------------------------------------------- /src/constructUrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2015 by Coursera 3 | Licensed under the Apache License 2.0, seen https://github.com/coursera/react-imgix/blob/master/LICENSE 4 | 5 | Minor syntax modifications have been made 6 | */ 7 | 8 | const PACKAGE_VERSION = require("../package.json").version; 9 | import ImgixClient from "@imgix/js-core"; 10 | import extractQueryParams from "./extractQueryParams"; 11 | 12 | // @see https://www.imgix.com/docs/reference 13 | var PARAM_EXPANSION = Object.freeze({ 14 | // Adjustment 15 | brightness: "bri", 16 | contrast: "con", 17 | exposure: "exp", 18 | gamma: "gam", 19 | highlights: "high", 20 | hue: "hue", 21 | invert: "invert", 22 | saturation: "sat", 23 | shaddows: "shad", 24 | shadows: "shad", 25 | sharpness: "sharp", 26 | "unsharp-mask": "usm", 27 | "unsharp-radius": "usmrad", 28 | vibrance: "vib", 29 | 30 | // Automatic 31 | "auto-features": "auto", 32 | 33 | // Background 34 | "background-color": "bg", 35 | 36 | // Blend 37 | blend: "blend", 38 | "blend-mode": "bm", 39 | "blend-align": "ba", 40 | "blend-alpha": "balph", 41 | "blend-padding": "bp", 42 | "blend-width": "bw", 43 | "blend-height": "bh", 44 | "blend-fit": "bf", 45 | "blend-crop": "bc", 46 | "blend-size": "bs", 47 | "blend-x": "bx", 48 | "blend-y": "by", 49 | 50 | // Border & Padding 51 | border: "border", 52 | padding: "pad", 53 | 54 | // Face Detection 55 | "face-index": "faceindex", 56 | "face-padding": "facepad", 57 | faces: "faces", 58 | 59 | // Format 60 | "chroma-subsampling": "chromasub", 61 | "color-quantization": "colorquant", 62 | download: "dl", 63 | DPI: "dpi", 64 | format: "fm", 65 | "lossless-compression": "lossless", 66 | quality: "q", 67 | 68 | // Mask 69 | "mask-image": "mask", 70 | 71 | // Mask 72 | "noise-blur": "nr", 73 | "noise-sharpen": "nrs", 74 | 75 | // Palette n/a 76 | // PDF n/a 77 | // Pixel Density n/a 78 | 79 | // Rotation 80 | "flip-direction": "flip", 81 | orientation: "or", 82 | "rotation-angle": "rot", 83 | 84 | // Size 85 | "crop-mode": "crop", 86 | "fit-mode": "fit", 87 | "image-height": "h", 88 | "image-width": "w", 89 | 90 | // Stylize 91 | blurring: "blur", 92 | halftone: "htn", 93 | monotone: "mono", 94 | pixelate: "px", 95 | "sepia-tone": "sepia", 96 | 97 | // Trim TODO 98 | // Watermark TODO 99 | 100 | // Extra 101 | height: "h", 102 | width: "w", 103 | }); 104 | 105 | var DEFAULT_OPTIONS = Object.freeze({ 106 | auto: "format", // http://www.imgix.com/docs/reference/automatic#param-auto 107 | }); 108 | 109 | /** 110 | * Construct a URL for an image with an Imgix proxy, expanding image options 111 | * per the [API reference docs](https://www.imgix.com/docs/reference). 112 | * @param {String} src src of raw image 113 | * @param {Object} longImgixParams map of image API options, in long or short form per expansion rules 114 | * @return {String} URL of image src transformed by Imgix 115 | */ 116 | function constructUrl(src, longImgixParams, srcOptions) { 117 | if (!src) { 118 | return ""; 119 | } 120 | const params = compactParamKeys(longImgixParams); 121 | const { client, pathComponents } = extractClientAndPathComponents(src); 122 | return client.buildURL(pathComponents.join("/"), params, srcOptions); 123 | } 124 | 125 | function compactParamKeys(longImgixParams) { 126 | const keys = Object.keys(longImgixParams); 127 | const keysLength = keys.length; 128 | const params = {}; 129 | for (let i = 0; i < keysLength; i++) { 130 | let key = keys[i]; 131 | if (PARAM_EXPANSION[key]) { 132 | params[PARAM_EXPANSION[key]] = longImgixParams[key]; 133 | } else { 134 | params[key] = longImgixParams[key]; 135 | } 136 | } 137 | 138 | return params; 139 | } 140 | 141 | function extractClientAndPathComponents(src) { 142 | const [scheme, rest] = src.split("://"); 143 | const [domain, ...pathComponents] = rest.split("/"); 144 | let useHTTPS = scheme == "https"; 145 | 146 | const client = new ImgixClient({ 147 | domain: domain, 148 | useHTTPS: useHTTPS, 149 | includeLibraryParam: false, 150 | }); 151 | 152 | return { client, pathComponents }; 153 | } 154 | 155 | function buildURLPublic(src, imgixParams = {}, options = {}) { 156 | const { disableLibraryParam, disablePathEncoding } = options; 157 | 158 | const [rawSrc, params] = extractQueryParams(src); 159 | 160 | return constructUrl( 161 | rawSrc, 162 | Object.assign( 163 | {}, 164 | params, 165 | imgixParams, 166 | disableLibraryParam ? {} : { ixlib: `react-${PACKAGE_VERSION}` } 167 | ), 168 | { disablePathEncoding } 169 | ); 170 | } 171 | 172 | export default constructUrl; 173 | 174 | export { buildURLPublic, compactParamKeys, extractClientAndPathComponents }; 175 | -------------------------------------------------------------------------------- /src/extractQueryParams.js: -------------------------------------------------------------------------------- 1 | export default function extractQueryParams(src) { 2 | const splitSrc = src.split("?"); 3 | const url = splitSrc[0]; 4 | const query = splitSrc[1]; 5 | if (!query) { 6 | return [url, {}]; 7 | } 8 | const paramsPairs = query.split("&"); 9 | const params = {}; 10 | const len = paramsPairs.length; 11 | for (let i = 0; i < len; i++) { 12 | const param = paramsPairs[i]; 13 | const splitParam = param.split("="); 14 | const key = splitParam[0]; 15 | const val = splitParam[1]; 16 | params[key] = decodeURIComponent(val); 17 | } 18 | return [url, params]; 19 | } 20 | -------------------------------------------------------------------------------- /src/findClosest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the closest value in the search array to the value provided using a binary search 3 | * If the value is in the middle of two candidate values, it chooses the higher value 4 | */ 5 | export default function findClosest(searchValue, arr) { 6 | if (searchValue < arr[0]) { 7 | return arr[0]; 8 | } 9 | if (searchValue > arr[arr.length - 1]) { 10 | return arr[arr.length - 1]; 11 | } 12 | var mid; 13 | var lo = 0; 14 | var hi = arr.length - 1; 15 | while (hi - lo > 1) { 16 | mid = Math.floor((lo + hi) / 2); 17 | if (arr[mid] < searchValue) { 18 | lo = mid; 19 | } else { 20 | hi = mid; 21 | } 22 | } 23 | if (searchValue - arr[lo] < arr[hi] - searchValue) { 24 | return arr[lo]; 25 | } 26 | return arr[hi]; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import ReactImgix, { Picture, Source } from "./react-imgix"; 4 | import { PublicConfigAPI } from "./config"; 5 | import { buildURLPublic as buildURL } from "./constructUrl"; 6 | import { ImgixProvider } from "./HOCs"; 7 | 8 | export { ImgixProvider }; 9 | export { buildURL }; 10 | export { Picture, Source, PublicConfigAPI }; 11 | export { Background } from "./react-imgix-bg"; 12 | export default ReactImgix; 13 | -------------------------------------------------------------------------------- /src/react-imgix-bg.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withContentRect } from "react-measure"; 3 | import { shallowEqual } from "./common"; 4 | import { PACKAGE_VERSION } from "./constants"; 5 | import constructUrl from "./constructUrl"; 6 | import extractQueryParams from "./extractQueryParams"; 7 | import findClosest from "./findClosest"; 8 | import targetWidths from "./targetWidths"; 9 | import { mergeComponentPropsHOF, processPropsHOF } from "./HOFs"; 10 | 11 | const findNearestWidth = (actualWidth) => 12 | findClosest(actualWidth, targetWidths); 13 | 14 | const toFixed = (dp, value) => +value.toFixed(dp); 15 | 16 | export const __shouldComponentUpdate = (props, nextProps) => { 17 | const contentRect = props.contentRect; 18 | const bounds = contentRect.bounds; 19 | const { width: prevWidth, height: prevHeight } = bounds; 20 | 21 | const nextContentRect = nextProps.contentRect; 22 | const nextBounds = nextContentRect.bounds; 23 | const { width: nextWidth, height: nextHeight } = nextBounds; 24 | 25 | // If neither of the previous nor next dimensions are present, 26 | // re-render. 27 | if (!nextWidth || !nextHeight || !prevWidth || !prevHeight) { 28 | return true; 29 | } 30 | 31 | // The component has been rendered at least twice by this point 32 | // and both the previous and next dimensions should be defined. 33 | // Only update if the nextWidth is greater than the prevWidth. 34 | if (prevWidth && nextWidth && nextWidth > prevWidth) { 35 | return true; 36 | } 37 | 38 | // Similarly, only update if the next height is greater than 39 | // the previous height. 40 | if (prevHeight && nextHeight && nextHeight > prevHeight) { 41 | return true; 42 | } 43 | 44 | const customizer = (oldProp, newProp, key) => { 45 | // these keys are ignored from prop checking process 46 | if (key === "contentRect" || key === "measure" || key === "measureRef") { 47 | return true; 48 | } 49 | 50 | if (key === "children") { 51 | return oldProp == newProp; 52 | } 53 | 54 | if (key === "imgixParams") { 55 | return shallowEqual(oldProp, newProp, (a, b) => { 56 | if (Array.isArray(a)) { 57 | return shallowEqual(a, b); 58 | } 59 | return undefined; 60 | }); 61 | } 62 | 63 | if (key === "htmlAttributes") { 64 | return shallowEqual(oldProp, newProp); 65 | } 66 | 67 | return undefined; // handled by shallowEqual 68 | }; 69 | 70 | // If we made it here, we need to check if the "top-level" 71 | // props have changed (e.g. disableLibraryParam). 72 | const propsEqual = shallowEqual(props, nextProps, customizer); 73 | 74 | return !propsEqual; 75 | }; 76 | 77 | class BackgroundImpl extends React.Component { 78 | constructor(props) { 79 | super(props); 80 | } 81 | 82 | shouldComponentUpdate(nextProps) { 83 | return __shouldComponentUpdate(this.props, nextProps); 84 | } 85 | 86 | render() { 87 | const { 88 | measureRef, 89 | contentRect, 90 | imgixParams = {}, 91 | onLoad, 92 | disableLibraryParam, 93 | disablePathEncoding, 94 | src, 95 | children, 96 | className = "", 97 | } = this.props; 98 | const { w: forcedWidth, h: forcedHeight } = imgixParams; 99 | const hasDOMDimensions = 100 | contentRect.bounds.width != null && contentRect.bounds.height != null; 101 | const htmlAttributes = this.props.htmlAttributes || {}; 102 | const dpr = toFixed(2, imgixParams.dpr || global.devicePixelRatio || 1); 103 | const ref = htmlAttributes.ref; 104 | const onRef = (el) => { 105 | measureRef(el); 106 | if (typeof ref === "function") { 107 | ref(el); 108 | } 109 | }; 110 | 111 | const { width, height } = (() => { 112 | const bothWidthAndHeightPassed = 113 | forcedWidth != null && forcedHeight != null; 114 | 115 | if (bothWidthAndHeightPassed) { 116 | return { width: forcedWidth, height: forcedHeight }; 117 | } 118 | 119 | if (!hasDOMDimensions) { 120 | return { width: undefined, height: undefined }; 121 | } 122 | const ar = contentRect.bounds.width / contentRect.bounds.height; 123 | 124 | const neitherWidthNorHeightPassed = 125 | forcedWidth == null && forcedHeight == null; 126 | if (neitherWidthNorHeightPassed) { 127 | const width = findNearestWidth(contentRect.bounds.width); 128 | const height = Math.ceil(width / ar); 129 | return { width, height }; 130 | } 131 | if (forcedWidth != null) { 132 | const height = Math.ceil(forcedWidth / ar); 133 | return { width: forcedWidth, height }; 134 | } else if (forcedHeight != null) { 135 | const width = Math.ceil(forcedHeight * ar); 136 | return { width, height: forcedHeight }; 137 | } 138 | })(); 139 | const isReady = width != null && height != null; 140 | 141 | const commonProps = { 142 | ...htmlAttributes, 143 | }; 144 | 145 | if (!isReady) { 146 | return ( 147 |
152 | {children} 153 |
154 | ); 155 | } 156 | 157 | const renderedSrc = (() => { 158 | const [rawSrc, params] = extractQueryParams(src); 159 | const longImgixParams = { 160 | ...params, 161 | fit: "crop", 162 | ...imgixParams, 163 | ...(disableLibraryParam ? {} : { ixlib: `react-${PACKAGE_VERSION}` }), 164 | width, 165 | height, 166 | dpr, 167 | }; 168 | 169 | const srcOptions = { 170 | disablePathEncoding, 171 | } 172 | 173 | return constructUrl(rawSrc, longImgixParams, srcOptions); 174 | })(); 175 | 176 | const style = { 177 | ...htmlAttributes.style, 178 | backgroundImage: `url(${renderedSrc})`, 179 | backgroundSize: 180 | (htmlAttributes.style || {}).backgroundSize !== undefined 181 | ? htmlAttributes.style.backgroundSize 182 | : "cover", 183 | }; 184 | 185 | return ( 186 |
187 | {children} 188 |
189 | ); 190 | } 191 | } 192 | 193 | const Background = mergeComponentPropsHOF( 194 | processPropsHOF(withContentRect("bounds")(BackgroundImpl)) 195 | ); 196 | 197 | export { Background, BackgroundImpl as __BackgroundImpl }; 198 | -------------------------------------------------------------------------------- /src/react-imgix.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React, { Component } from "react"; 3 | import "./array-findindex"; 4 | import { config } from "./common"; 5 | import { PACKAGE_VERSION } from "./constants"; 6 | import constructUrl, { 7 | compactParamKeys, 8 | extractClientAndPathComponents, 9 | } from "./constructUrl"; 10 | import extractQueryParams from "./extractQueryParams"; 11 | import { ShouldComponentUpdateHOC } from "./HOCs"; 12 | import { mergeComponentPropsHOF, processPropsHOF } from "./HOFs"; 13 | 14 | const NODE_ENV = process.env.NODE_ENV; 15 | 16 | const buildKey = (idx) => `react-imgix-${idx}`; 17 | 18 | const defaultImgixParams = { 19 | auto: ["format"], 20 | }; 21 | 22 | const defaultAttributeMap = { 23 | src: "src", 24 | srcSet: "srcSet", 25 | sizes: "sizes", 26 | }; 27 | 28 | const noop = () => {}; 29 | 30 | const COMMON_PROP_TYPES = { 31 | className: PropTypes.string, 32 | onMounted: PropTypes.func, 33 | htmlAttributes: PropTypes.object, 34 | alt: PropTypes.string, 35 | }; 36 | 37 | const SHARED_IMGIX_AND_SOURCE_PROP_TYPES = Object.assign( 38 | {}, 39 | COMMON_PROP_TYPES, 40 | { 41 | disableQualityByDPR: PropTypes.bool, 42 | disableSrcSet: PropTypes.bool, 43 | disableLibraryParam: PropTypes.bool, 44 | disablePathEncoding: PropTypes.bool, 45 | imgixParams: PropTypes.object, 46 | sizes: PropTypes.string, 47 | width: PropTypes.number, 48 | height: PropTypes.number, 49 | src: PropTypes.string.isRequired, 50 | srcSetOptions: PropTypes.shape({ 51 | widths: PropTypes.arrayOf(PropTypes.number), 52 | widthTolerance: PropTypes.number, 53 | minWidth: PropTypes.number, 54 | maxWidth: PropTypes.number, 55 | devicePixelRatios: PropTypes.arrayOf(PropTypes.number), 56 | }), 57 | } 58 | ); 59 | 60 | const REACT_IMGIX_PROP_TYPES = Object.assign( 61 | {}, 62 | SHARED_IMGIX_AND_SOURCE_PROP_TYPES, 63 | { 64 | alt: PropTypes.string, 65 | } 66 | ); 67 | 68 | const OVERSIZE_IMAGE_TOLERANCE = 500; 69 | 70 | let performanceObserver; 71 | 72 | /** 73 | * Validates that an aspect ratio is in the format w:h. If false is returned, the aspect ratio is in the wrong format. 74 | */ 75 | function aspectRatioIsValid(aspectRatio) { 76 | if (typeof aspectRatio !== "string") { 77 | return false; 78 | } 79 | 80 | return /^\d+(\.\d+)?:\d+(\.\d+)?$/.test(aspectRatio); 81 | } 82 | 83 | const setParentRef = (parentRef, el) => { 84 | if (!parentRef) { 85 | return; 86 | } 87 | 88 | // assign ref based on if it's a callback vs object 89 | if (typeof parentRef === "function") { 90 | parentRef(el); 91 | } else { 92 | parentRef.current = el; 93 | } 94 | }; 95 | 96 | function buildSrcSet(rawSrc, params = {}, options = {}) { 97 | const { client, pathComponents } = extractClientAndPathComponents(rawSrc); 98 | const compactedParams = compactParamKeys(params); 99 | return client.buildSrcSet(pathComponents.join("/"), compactedParams, options); 100 | } 101 | 102 | /** 103 | * Build a imgix source url with parameters from a raw url 104 | */ 105 | function buildSrc({ 106 | src: inputSrc, 107 | width, 108 | height, 109 | disableLibraryParam, 110 | disableSrcSet, 111 | disablePathEncoding, 112 | imgixParams, 113 | disableQualityByDPR, 114 | srcSetOptions, 115 | }) { 116 | const fixedSize = width != null || height != null; 117 | 118 | const [rawSrc, params] = extractQueryParams(inputSrc); 119 | 120 | const srcImgixParams = Object.assign( 121 | {}, 122 | params, 123 | imgixParams, 124 | disableLibraryParam ? {} : { ixlib: `react-${PACKAGE_VERSION}` }, 125 | fixedSize && height ? { height } : {}, 126 | fixedSize && width ? { width } : {} 127 | ); 128 | 129 | const srcOptions = { 130 | disablePathEncoding, 131 | }; 132 | 133 | const src = constructUrl(rawSrc, srcImgixParams, srcOptions); 134 | 135 | let srcSet; 136 | 137 | if (disableSrcSet) { 138 | srcSet = src; 139 | } else { 140 | const sharedSrcSetOptions = Object.assign({}, srcSetOptions, { 141 | disablePathEncoding, 142 | }); 143 | if (fixedSize) { 144 | const { width, w, height, h, q, ...urlParams } = srcImgixParams; 145 | if (q) { 146 | urlParams["q"] = q; 147 | } 148 | 149 | const finalWidth = width || w; 150 | const finalHeight = height || h; 151 | 152 | if (finalWidth) { 153 | urlParams["w"] = finalWidth; 154 | } 155 | 156 | if (finalHeight) { 157 | urlParams["h"] = finalHeight; 158 | } 159 | 160 | srcSet = buildSrcSet( 161 | rawSrc, 162 | urlParams, 163 | Object.assign( 164 | { disableVariableQuality: disableQualityByDPR }, 165 | sharedSrcSetOptions 166 | ) 167 | ); 168 | } else { 169 | const { width, w, height, h, ...urlParams } = srcImgixParams; 170 | 171 | const aspectRatio = imgixParams.ar; 172 | let showARWarning = 173 | aspectRatio != null && aspectRatioIsValid(aspectRatio) === false; 174 | 175 | srcSet = buildSrcSet(rawSrc, urlParams, sharedSrcSetOptions); 176 | 177 | if ( 178 | NODE_ENV !== "production" && 179 | showARWarning && 180 | config.warnings.invalidARFormat 181 | ) { 182 | console.warn( 183 | `[Imgix] The aspect ratio passed ("${aspectRatio}") is not in the correct format. The correct format is "W:H".` 184 | ); 185 | } 186 | } 187 | } 188 | 189 | return { 190 | src, 191 | srcSet, 192 | }; 193 | } 194 | 195 | /** 196 | * Use the PerfomanceObser API to warn if an LCP element is loaded lazily. 197 | */ 198 | function watchForLazyLCP(imgRef) { 199 | if ( 200 | !performanceObserver && 201 | typeof window !== 'undefined' && 202 | window.PerformanceObserver 203 | ) { 204 | performanceObserver = new PerformanceObserver((entryList) => { 205 | const entries = entryList.getEntries(); 206 | 207 | if (entries.length === 0) { 208 | return; 209 | } 210 | 211 | // The most recent LCP entry is the only one that can be the real LCP element. 212 | const lcpCandidate = entries[entries.length - 1]; 213 | if (lcpCandidate.element?.getAttribute("loading") === "lazy") { 214 | console.warn( 215 | `An image with URL ${imgRef.src} was detected as a possible LCP element (https://web.dev/lcp) ` + 216 | `and also has 'loading="lazy"'. This can have a significant negative impact on page loading performance. ` + 217 | `Lazy loading is not recommended for images which may render in the initial viewport.` ); 218 | } 219 | }); 220 | performanceObserver.observe({type: 'largest-contentful-paint', buffered: true}); 221 | } 222 | } 223 | 224 | /** 225 | * Once the image is loaded, warn if it's intrinsic size is much larger than its rendered size. 226 | */ 227 | function checkImageSize(imgRef) { 228 | const renderedWidth = imgRef.clientWidth; 229 | const renderedHeight = imgRef.clientHeight; 230 | const intrinsicWidth = imgRef.naturalWidth; 231 | const intrinsicHeight = imgRef.naturalHeight; 232 | 233 | if ( 234 | intrinsicWidth > renderedWidth + OVERSIZE_IMAGE_TOLERANCE || 235 | intrinsicHeight > renderedHeight + OVERSIZE_IMAGE_TOLERANCE 236 | ) { 237 | console.warn( 238 | `An image with URL ${imgRef.src} was rendered with dimensions significantly smaller than intrinsic size, ` + 239 | `which can slow down page loading. This may be caused by a missing or inaccurate "sizes" property. ` + 240 | `Rendered size: ${renderedWidth}x${renderedHeight}. Intrinsic size: ${intrinsicWidth}x${intrinsicHeight}.` 241 | ); 242 | } 243 | } 244 | 245 | /** 246 | * Initializes listeners for performance-related image warnings 247 | */ 248 | function doPerformanceChecksOnLoad(imgRef) { 249 | // Check image size on load 250 | if(config.warnings.oversizeImage) { 251 | if (imgRef.complete) { 252 | checkImageSize(imgRef); 253 | } else { 254 | imgRef.addEventListener('load', () => { 255 | checkImageSize(imgRef); 256 | }); 257 | } 258 | } 259 | if(config.warnings.lazyLCP) { 260 | watchForLazyLCP(imgRef); 261 | } 262 | } 263 | 264 | /** 265 | * Combines default imgix params with custom imgix params to make a imgix params config object 266 | */ 267 | function imgixParams(props) { 268 | const params = Object.assign({}, defaultImgixParams, props.imgixParams); 269 | return Object.assign({}, params); 270 | } 271 | 272 | /** 273 | * React component used to render elements with Imgix 274 | */ 275 | class ReactImgix extends Component { 276 | static propTypes = Object.assign({}, REACT_IMGIX_PROP_TYPES); 277 | static defaultProps = { 278 | disableSrcSet: false, 279 | onMounted: noop, 280 | }; 281 | 282 | constructor(props) { 283 | super(props); 284 | this.imgRef = null; 285 | } 286 | 287 | componentDidMount() { 288 | if (NODE_ENV === 'development' && this.imgRef) { 289 | doPerformanceChecksOnLoad(this.imgRef); 290 | } 291 | this.props.onMounted(this.imgRef); 292 | } 293 | 294 | render() { 295 | const { disableSrcSet, width, height } = this.props; 296 | 297 | // Pre-render checks 298 | if (NODE_ENV !== "production" && config.warnings.sizesAttribute) { 299 | if ( 300 | this.props.width == null && 301 | this.props.height == null && 302 | this.props.sizes == null && 303 | !this.props._inPicture 304 | ) { 305 | console.warn( 306 | "If width and height are not set, a sizes attribute should be passed." 307 | ); 308 | } 309 | } 310 | 311 | const { src, srcSet } = buildSrc( 312 | Object.assign({}, this.props, { 313 | type: "img", 314 | imgixParams: imgixParams(this.props), 315 | }) 316 | ); 317 | 318 | const attributeConfig = Object.assign( 319 | {}, 320 | defaultAttributeMap, 321 | this.props.attributeConfig 322 | ); 323 | 324 | const fixedSize = !!( 325 | (width || this.props.htmlAttributes?.width) && 326 | (height || this.props.htmlAttributes?.height) 327 | ); 328 | let adjustedSizes = this.props.sizes; 329 | if (this.props.sizes && this.props.htmlAttributes?.loading === "lazy" && !fixedSize) { 330 | adjustedSizes = "auto, " + adjustedSizes ?? ""; 331 | } 332 | 333 | const childProps = Object.assign({}, this.props.htmlAttributes, { 334 | [attributeConfig.sizes]: adjustedSizes, 335 | className: this.props.className, 336 | width: width <= 1 ? null : width ?? this.props.htmlAttributes?.width, 337 | height: height <= 1 ? null : height ?? this.props.htmlAttributes?.height, 338 | [attributeConfig.src]: src, 339 | ref: (el) => { 340 | this.imgRef = el; 341 | if ( 342 | this.props.htmlAttributes !== undefined && 343 | "ref" in this.props.htmlAttributes 344 | ) { 345 | setParentRef(this.props.htmlAttributes.ref, this.imgRef); 346 | } 347 | }, 348 | }); 349 | if (!disableSrcSet) { 350 | childProps[attributeConfig.srcSet] = srcSet; 351 | } 352 | if (this.props.alt) { 353 | childProps.alt = this.props.alt; 354 | } 355 | 356 | return ; 357 | } 358 | } 359 | ReactImgix.displayName = "ReactImgix"; 360 | 361 | /** 362 | * React component used to render elements with Imgix 363 | */ 364 | class PictureImpl extends Component { 365 | static propTypes = Object.assign({}, COMMON_PROP_TYPES, { 366 | children: PropTypes.any, 367 | }); 368 | static defaultProps = { 369 | onMounted: noop, 370 | }; 371 | 372 | constructor(props) { 373 | super(props); 374 | this.pictureRef = null; 375 | } 376 | 377 | componentDidMount() { 378 | this.props.onMounted(this.pictureRef); 379 | } 380 | 381 | render() { 382 | const { children } = this.props; 383 | 384 | // make sure all of our children have key set, otherwise we get react warnings 385 | let _children = 386 | React.Children.map(children, (child, idx) => { 387 | const childIsReactImgix = 388 | child.type?.name === "mergeComponentPropsHOFInner"; 389 | return React.cloneElement( 390 | child, 391 | Object.assign( 392 | { 393 | key: buildKey(idx), 394 | }, 395 | // This prevents props._inPicture being set on other children if 396 | // they're passed, such as an component, which will cause a 397 | // React error 398 | childIsReactImgix && { 399 | _inPicture: true, 400 | } 401 | ) 402 | ); 403 | }) || []; 404 | 405 | /* 406 | We need to make sure an or is the last child so we look for one in children 407 | a. if we find one, move it to the last entry if it's not already there 408 | b. if we don't find one, warn the user as they probably want to pass one. 409 | */ 410 | 411 | // look for an or - at the bare minimum we have to have a single element or else it will not work. 412 | let imgIdx = _children.findIndex( 413 | (c) => 414 | c.type === "img" || 415 | c.type === ReactImgix || 416 | c.type === ReactImgixWrapped 417 | ); 418 | 419 | if (imgIdx === -1 && config.warnings.fallbackImage) { 420 | console.warn( 421 | "No fallback or found in the children of a component. A fallback image should be passed to ensure the image renders correctly at all dimensions." 422 | ); 423 | } else if (imgIdx !== _children.length - 1) { 424 | // found one, need to move it to the end 425 | _children.push(_children.splice(imgIdx, 1)[0]); 426 | } 427 | 428 | return ( 429 | (this.pictureRef = el)} children={_children} /> 430 | ); 431 | } 432 | } 433 | PictureImpl.displayName = "ReactImgixPicture"; 434 | 435 | /** 436 | * React component used to render elements with Imgix 437 | */ 438 | class SourceImpl extends Component { 439 | static propTypes = Object.assign({}, SHARED_IMGIX_AND_SOURCE_PROP_TYPES); 440 | static defaultProps = { 441 | disableSrcSet: false, 442 | onMounted: noop, 443 | }; 444 | 445 | constructor(props) { 446 | super(props); 447 | this.sourceRef = null; 448 | } 449 | 450 | componentDidMount() { 451 | this.props.onMounted(this.sourceRef); 452 | } 453 | 454 | render() { 455 | const { disableSrcSet, width, height } = this.props; 456 | 457 | const { src, srcSet } = buildSrc( 458 | Object.assign({}, this.props, { 459 | type: "source", 460 | imgixParams: imgixParams(this.props), 461 | }) 462 | ); 463 | 464 | const attributeConfig = Object.assign( 465 | {}, 466 | defaultAttributeMap, 467 | this.props.attributeConfig 468 | ); 469 | const childProps = Object.assign({}, this.props.htmlAttributes, { 470 | [attributeConfig.sizes]: this.props.sizes, 471 | className: this.props.className, 472 | width: width <= 1 ? null : width ?? this.props.htmlAttributes?.width, 473 | height: height <= 1 ? null : height ?? this.props.htmlAttributes?.height, 474 | ref: (el) => { 475 | this.sourceRef = el; 476 | if ( 477 | this.props.htmlAttributes !== undefined && 478 | "ref" in this.props.htmlAttributes 479 | ) { 480 | setParentRef(this.props.htmlAttributes.ref, this.sourceRef); 481 | } 482 | }, 483 | }); 484 | 485 | // inside of a element a element ignores its src 486 | // attribute in favor of srcSet so we set that with either an actual 487 | // srcSet or a single src 488 | if (disableSrcSet) { 489 | childProps[attributeConfig.srcSet] = src; 490 | } else { 491 | childProps[attributeConfig.srcSet] = `${srcSet}`; 492 | } 493 | // for now we'll take media from htmlAttributes which isn't ideal because 494 | // a) this isn't an 495 | // b) passing objects as props means that react will always rerender 496 | // since objects dont respond correctly to === 497 | 498 | return ; 499 | } 500 | } 501 | SourceImpl.displayName = "ReactImgixSource"; 502 | 503 | const ReactImgixWrapped = mergeComponentPropsHOF( 504 | processPropsHOF(ShouldComponentUpdateHOC(ReactImgix)) 505 | ); 506 | const Picture = mergeComponentPropsHOF( 507 | processPropsHOF(ShouldComponentUpdateHOC(PictureImpl)) 508 | ); 509 | const Source = mergeComponentPropsHOF( 510 | processPropsHOF(ShouldComponentUpdateHOC(SourceImpl)) 511 | ); 512 | 513 | export default ReactImgixWrapped; 514 | export { 515 | ReactImgix as __ReactImgixImpl, 516 | Picture, 517 | Source, 518 | SourceImpl as __SourceImpl, 519 | PictureImpl as __PictureImpl, // for testing 520 | }; 521 | -------------------------------------------------------------------------------- /src/targetWidths.js: -------------------------------------------------------------------------------- 1 | import ImgixClient from "@imgix/js-core"; 2 | 3 | export default ImgixClient.targetWidths(); 4 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { shallowUntilTarget } from "./shallowUntilTarget"; 2 | -------------------------------------------------------------------------------- /test/helpers/shallowUntilTarget.js: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | Taken from https://github.com/mozilla/addons-frontend/blob/58d1315409f1ad6dc9b979440794df44c1128455/tests/unit/helpers.js#L276 7 | */ 8 | 9 | import { oneLine } from "common-tags"; 10 | import { shallow } from "enzyme"; 11 | 12 | /* 13 | * Repeatedly render a component tree using enzyme.shallow() until 14 | * finding and rendering TargetComponent. 15 | * 16 | * This is useful for testing a component wrapped in one or more 17 | * HOCs (higher order components). 18 | * 19 | * The `componentInstance` parameter is a React component instance. 20 | * Example: 21 | * 22 | * The `TargetComponent` parameter is the React class (or function) that 23 | * you want to retrieve from the component tree. 24 | */ 25 | export function shallowUntilTarget( 26 | componentInstance, 27 | TargetComponent, 28 | { maxTries = 10, shallowOptions, _shallow = shallow } = {} 29 | ) { 30 | if (!componentInstance) { 31 | throw new Error("componentInstance parameter is required"); 32 | } 33 | if (!TargetComponent) { 34 | throw new Error("TargetComponent parameter is required"); 35 | } 36 | 37 | let root = _shallow(componentInstance, shallowOptions); 38 | 39 | if (typeof root.type() === "string") { 40 | // If type() is a string then it's a DOM Node. 41 | // If it were wrapped, it would be a React component. 42 | throw new Error("Cannot unwrap this component because it is not wrapped"); 43 | } 44 | 45 | for (let tries = 1; tries <= maxTries; tries++) { 46 | if (root.is(TargetComponent)) { 47 | // Now that we found the target component, render it. 48 | return root.shallow(shallowOptions); 49 | } 50 | // Unwrap the next component in the hierarchy. 51 | root = root.dive(); 52 | } 53 | 54 | throw new Error(oneLine`Could not find ${TargetComponent} in rendered 55 | instance: ${componentInstance}; gave up after ${maxTries} tries`); 56 | } 57 | -------------------------------------------------------------------------------- /test/integration/react-imgix.test.jsx: -------------------------------------------------------------------------------- 1 | import Imgix from "react-imgix"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Background } from "react-imgix-bg"; 4 | import targetWidths from "targetWidths"; 5 | import Uri from "jsuri"; 6 | 7 | import React from "react"; 8 | import { mount, shallow } from "enzyme"; 9 | 10 | const isIE = (() => { 11 | const ua = window.navigator.userAgent; 12 | const isIE = /MSIE|Trident/.test(ua); 13 | return isIE; 14 | })(); 15 | 16 | const src = "https://assets.imgix.net/examples/pione.jpg"; 17 | 18 | const DELAY = 70; 19 | const findClosestWidthFromTargetWidths = (targetWidth) => 20 | targetWidths.reduce((acc, value, i) => { 21 | // <= ensures that the largest value is used 22 | if (Math.abs(value - targetWidth) <= Math.abs(acc - targetWidth)) { 23 | return value; 24 | } 25 | return acc; 26 | }, Number.MAX_VALUE); 27 | const findURIfromSUT = (sut) => { 28 | const container = sut.find(".bg-img").first(); 29 | 30 | if (!container) { 31 | throw new Error("Cannot find container."); 32 | } 33 | 34 | const bgImageStyle = container.getDOMNode().style; 35 | 36 | if (!bgImageStyle.backgroundImage) { 37 | throw new Error( 38 | "Cannot find style.background-image on background div. The element probably hasn't had time to measure the size of the DOM element." 39 | ); 40 | } 41 | 42 | const bgImageSrc = (() => { 43 | const bgImage = bgImageStyle.backgroundImage; 44 | // Mobile Safari trims speech marks from url('') styles, so this checks if they've been trimmed or not 45 | if (bgImage.startsWith('url("') || bgImage.startsWith("url('")) { 46 | return bgImageStyle.backgroundImage.slice(5, -2); 47 | } 48 | return bgImageStyle.backgroundImage.slice(4, -1); 49 | })(); 50 | 51 | const bgImageSrcURI = new Uri(bgImageSrc); 52 | return bgImageSrcURI; 53 | }; 54 | 55 | const renderBGAndWaitUntilLoaded = async (element) => { 56 | return new Promise((resolve, reject) => { 57 | let running; 58 | let waitUntilHasStyle = (maxTimes = 20, delay = 10, n = 0) => { 59 | if (!el) { 60 | return; 61 | } 62 | // Find the element which has the class "bg-img" 63 | const bgImageEl = (() => { 64 | if (el.getDOMNode().classList.contains("bg-img")) { 65 | return el.getDOMNode(); 66 | } 67 | if (el.getDOMNode().querySelector(".bg-img")) { 68 | return el.getDOMNode().querySelector(".bg-img"); 69 | } 70 | return undefined; 71 | })(); 72 | // Check if the element has loaded, which is shown by a truthy `background-image` 73 | if (bgImageEl.style.backgroundImage) { 74 | return resolve(el); 75 | } 76 | 77 | if (n >= maxTimes) { 78 | return reject("Tries exceeded to wait for component to be ready"); 79 | } 80 | setTimeout(() => waitUntilHasStyle(maxTimes, delay, n + 1), delay); 81 | }; 82 | const onRef = (ref) => { 83 | if (running) { 84 | return; 85 | } 86 | running = true; 87 | setTimeout(waitUntilHasStyle, 10); 88 | }; 89 | const addRef = (element) => 90 | React.cloneElement(element, { 91 | ...element.props, 92 | htmlAttributes: { 93 | ...element.props.htmlAttributes, 94 | ref: onRef, 95 | }, 96 | className: "bg-img " + (element.props.className || ""), 97 | }); 98 | const isRootBackground = element.type === Background; 99 | const elementWithRef = (() => { 100 | if (isRootBackground) { 101 | return addRef(element); 102 | } 103 | return React.cloneElement(element, { 104 | children: React.Children.map(element.props.children, (child) => { 105 | const isBackground = child.type === Background; 106 | if (!isBackground) { 107 | return child; 108 | } 109 | 110 | return addRef(child); 111 | }), 112 | }); 113 | })(); 114 | 115 | const el = renderIntoContainer(elementWithRef); 116 | }); 117 | }; 118 | 119 | let containerDiv; 120 | let sut; 121 | beforeEach(() => { 122 | containerDiv = global.document.createElement("div"); 123 | global.document.body.appendChild(containerDiv); 124 | }); 125 | 126 | afterEach(() => { 127 | global.document.body.removeChild(containerDiv); 128 | }); 129 | 130 | const fullRender = (markup) => { 131 | return ReactDOM.render(markup, containerDiv); 132 | }; 133 | 134 | const renderIntoContainer = (element) => { 135 | return mount(element, { attachTo: containerDiv }); 136 | }; 137 | 138 | const renderAndWaitForImageLoad = async (element) => { 139 | return new Promise((resolve, reject) => { 140 | let renderedEl; 141 | const elementWithOnMounted = React.cloneElement(element, { 142 | onMounted: () => {}, 143 | htmlAttributes: { 144 | ...(element.props.htmlAttributes || {}), 145 | onLoad: () => { 146 | element.props.htmlAttributes && 147 | element.props.htmlAttributes.onLoad && 148 | element.props.htmlAttributes.onLoad(); 149 | setImmediate(() => resolve(renderedEl)); 150 | }, 151 | onError: () => { 152 | element.props.htmlAttributes && 153 | element.props.htmlAttributes.onError && 154 | element.props.htmlAttributes.onError(); 155 | setImmediate(() => resolve(renderedEl)); 156 | }, 157 | }, 158 | }); 159 | renderedEl = renderIntoContainer(elementWithOnMounted); 160 | }); 161 | }; 162 | 163 | describe("Image Warnings", () => { 164 | const imageUndersizedWarning = "was rendered with dimensions significantly smaller than intrinsic size"; 165 | const lcpWarning = "was detected as a possible LCP element"; 166 | 167 | let realConsoleWarn; 168 | let warnings = []; 169 | 170 | beforeEach(() => { 171 | realConsoleWarn = window.console.warn; 172 | window.console.warn = (str) => { 173 | warnings.push(str); 174 | } 175 | }); 176 | afterEach(() => { 177 | window.console.warn = realConsoleWarn; 178 | warnings = []; 179 | }); 180 | 181 | it("should log a warning if LCP image is lazy-loaded", async () => { 182 | const renderedImage = await renderAndWaitForImageLoad( 183 | 190 | ); 191 | 192 | await new Promise((resolve) => { 193 | setTimeout(resolve, 500); 194 | }); 195 | expect(warnings.find(warning => warning.includes(lcpWarning))).toBeTruthy(); 196 | }); 197 | 198 | it("should not log a warning if non-LCP image is lazy-loaded", async () => { 199 | renderIntoContainer( 200 |
201 |
202 | 209 |
210 | ); 211 | 212 | await new Promise((resolve) => { 213 | setTimeout(resolve, 1000); 214 | }); 215 | expect(warnings.find(warning => warning.includes(lcpWarning))).not.toBeTruthy(); 216 | }); 217 | 218 | it("should log a warning if intrinsic dimensions are significantly larger than rendered size", async () => { 219 | 220 | const sut = await renderAndWaitForImageLoad( 221 | 228 | ); 229 | 230 | expect(warnings.find(warning => warning.includes(imageUndersizedWarning))).toBeTruthy(); 231 | }); 232 | 233 | it("should not log a warning if intrinsic dimensions are within the threshold of rendered size", async () => { 234 | 235 | const sut = await renderAndWaitForImageLoad( 236 | 243 | ); 244 | 245 | expect(warnings.find(warning => warning.includes(imageUndersizedWarning))).not.toBeTruthy(); 246 | }); 247 | 248 | }); 249 | 250 | describe("When in default mode", () => { 251 | const renderImage = () => 252 | renderIntoContainer(); 253 | 254 | it("an should be rendered", () => { 255 | expect(renderImage().find("img")).toHaveLength(1); 256 | }); 257 | it("the rendered element's src should be set", () => { 258 | expect(renderImage().find("img").props().src).toContain(src); 259 | }); 260 | 261 | context("htmlAttributes", () => { 262 | it("'onLoad' calls the callback", async () => { 263 | let onLoadCalled = false; 264 | 265 | await renderAndWaitForImageLoad( 266 | { 272 | onLoadCalled = true; 273 | }, 274 | }} 275 | /> 276 | ); 277 | 278 | expect(onLoadCalled).toBe(true); 279 | }); 280 | it("'onError' calls the callback", async () => { 281 | let onErrorCalled = false; 282 | 283 | await renderAndWaitForImageLoad( 284 | { 288 | onErrorCalled = true; 289 | }, 290 | }} 291 | /> 292 | ); 293 | 294 | expect(onErrorCalled).toBe(true); 295 | }); 296 | }); 297 | }); 298 | 299 | describe("Background Mode", () => { 300 | /////////////////////// 301 | // Common test cases 302 | const shouldRenderNoBGImage = (element) => { 303 | const sut = renderIntoContainer(element); 304 | 305 | const container = sut.find(".bg-img").first(); 306 | 307 | const bgImage = container.getDOMNode().style.backgroundImage; 308 | 309 | expect(bgImage).toEqual(expect.not.stringContaining("url")); 310 | }; 311 | const shouldBehaveLikeBg = function (size = "cover") { 312 | it("the element should have backgroundImage and backgroundSize set", () => { 313 | const style = sut.find(".bg-img").first().getDOMNode().style; 314 | expect({ 315 | backgroundImage: style.backgroundImage, 316 | backgroundSize: style.backgroundSize, 317 | }).toMatchObject({ 318 | backgroundImage: expect.stringContaining(src), 319 | backgroundSize: size, 320 | }); 321 | }); 322 | }; 323 | const shouldHaveDimensions = async ( 324 | { width: expectedWidth, height: expectedHeight }, 325 | element 326 | ) => { 327 | const sut = await new Promise((resolve, reject) => { 328 | const renderedEl = renderIntoContainer(element); 329 | setTimeout(() => resolve(renderedEl), DELAY); 330 | }); 331 | 332 | const bgImageSrcURL = findURIfromSUT(sut); 333 | 334 | expect(bgImageSrcURL.getQueryParamValue("w")).toBe("" + expectedWidth); 335 | expect(bgImageSrcURL.getQueryParamValue("h")).toBe("" + expectedHeight); 336 | }; 337 | 338 | ////////////////////////////// 339 | // Tests 340 | it("renders a div", () => { 341 | const sut = renderIntoContainer(); 342 | 343 | expect(sut.getDOMNode().tagName).toBe("DIV"); 344 | }); 345 | 346 | describe("when neither width nor height are passed", () => { 347 | it("renders nothing at first", () => { 348 | shouldRenderNoBGImage( 349 | 350 |
Content
351 |
352 | ); 353 | }); 354 | 355 | it("sets the size of the background image to the size of the containing element", async () => { 356 | const targetWidth = 105; 357 | const targetHeight = 110; 358 | const aspectRatio = targetWidth / targetHeight; 359 | const sut = await renderBGAndWaitUntilLoaded( 360 |
361 | 362 | 363 |
Content
364 |
365 |
366 | ); 367 | 368 | const bgImageSrcURL = findURIfromSUT(sut); 369 | 370 | const expectedWidth = findClosestWidthFromTargetWidths(targetWidth); 371 | const expectedHeight = Math.round(expectedWidth / aspectRatio); 372 | 373 | expect(bgImageSrcURL.getQueryParamValue("w")).toBe(`${expectedWidth}`); 374 | expect(bgImageSrcURL.getQueryParamValue("h")).toBe(`${expectedHeight}`); 375 | expect(bgImageSrcURL.getQueryParamValue("fit")).toBe("crop"); 376 | }); 377 | }); 378 | describe("when both width and height provided", () => { 379 | it("renders immediately when both width and height provided", () => { 380 | const sut = renderIntoContainer( 381 | 389 |
Content
390 |
391 | ); 392 | 393 | const bgImageSrcURL = findURIfromSUT(sut); 394 | 395 | expect(bgImageSrcURL.getQueryParamValue("w")).toBe("300"); 396 | expect(bgImageSrcURL.getQueryParamValue("h")).toBe("350"); 397 | }); 398 | it("sets width and height to values passed", async () => { 399 | const sut = await renderBGAndWaitUntilLoaded( 400 |
401 | 402 | 410 |
Content
411 |
412 |
413 | ); 414 | 415 | const bgImageSrcURL = findURIfromSUT(sut); 416 | 417 | expect(bgImageSrcURL.getQueryParamValue("w")).toBe("300"); 418 | expect(bgImageSrcURL.getQueryParamValue("h")).toBe("350"); 419 | }); 420 | }); 421 | 422 | describe("when only width is passed", () => { 423 | it("renders nothing at first", () => { 424 | shouldRenderNoBGImage( 425 | 432 |
Content
433 |
434 | ); 435 | }); 436 | it("sets height dynamically", async () => { 437 | await shouldHaveDimensions( 438 | { width: 200, height: 210 }, 439 |
440 | 441 | 448 |
Content
449 |
450 |
451 | ); 452 | }); 453 | }); 454 | describe("when only height is passed", () => { 455 | it("renders nothing at first", () => { 456 | shouldRenderNoBGImage( 457 | 464 |
Content
465 |
466 | ); 467 | }); 468 | it("sets width dynamically", async () => { 469 | await shouldHaveDimensions( 470 | { width: 200, height: 210 }, 471 |
472 | 473 | 480 |
Content
481 |
482 |
483 | ); 484 | }); 485 | }); 486 | 487 | describe("when both w and h provided", () => { 488 | it("does not duplicate query params for height and width", () => { 489 | const sut = renderIntoContainer( 490 | 498 | ); 499 | 500 | const bgImageSrcURL = findURIfromSUT(sut); 501 | const seen = new Set(); 502 | for (const [k, _v] of bgImageSrcURL.queryPairs) { 503 | if (seen.has(k)) { 504 | throw new Error( 505 | `duplicate keys for '${k}' found in query parameters` 506 | ); 507 | } else { 508 | seen.add(k); 509 | } 510 | } 511 | }); 512 | }); 513 | 514 | describe("without the backgroundSize prop set", () => { 515 | beforeEach(async () => { 516 | sut = await renderBGAndWaitUntilLoaded( 517 |
518 | 519 | 524 |
525 | ); 526 | }); 527 | shouldBehaveLikeBg(""); 528 | }); 529 | 530 | describe("with the backgroundSize prop set to 'contain'", () => { 531 | beforeEach(async () => { 532 | sut = await renderBGAndWaitUntilLoaded( 533 |
534 | 535 | 539 |
540 | ); 541 | }); 542 | shouldBehaveLikeBg("contain"); 543 | }); 544 | it("respects className", () => { 545 | const sut = renderIntoContainer( 546 | 547 | ); 548 | 549 | expect(sut.getDOMNode().classList.contains("custom-class-name")).toBe(true); 550 | }); 551 | it("can disable library param", async () => { 552 | const sut = await renderBGAndWaitUntilLoaded( 553 | 554 | ); 555 | 556 | expect(sut.getDOMNode().style.backgroundImage).not.toContain("ixlib="); 557 | }); 558 | describe("can override html properties", () => { 559 | it("before loading", () => { 560 | const sut = renderIntoContainer( 561 | 562 | ); 563 | 564 | expect(sut.getDOMNode().getAttribute("alt")).toBe("Alt tag"); 565 | }); 566 | it("after loaded", async () => { 567 | const sut = await renderBGAndWaitUntilLoaded( 568 | 569 | ); 570 | 571 | expect(sut.getDOMNode().getAttribute("alt")).toBe("Alt tag"); 572 | }); 573 | }); 574 | 575 | it("scales the background image by the devices dpr", async () => { 576 | // window.devicePixelRatio is not allowed in IE. 577 | if (isIE) { 578 | return; 579 | } 580 | const oldDPR = global.devicePixelRatio; 581 | global.devicePixelRatio = 2; 582 | 583 | const targetWidth = 105; 584 | const targetHeight = 110; 585 | const sut = await renderBGAndWaitUntilLoaded( 586 |
587 | 588 | 589 |
Content
590 |
591 |
592 | ); 593 | 594 | const bgImageSrcURL = findURIfromSUT(sut); 595 | 596 | expect(bgImageSrcURL.getQueryParamValue("dpr")).toBe("2"); 597 | 598 | global.devicePixelRatio = oldDPR; 599 | }); 600 | it("the dpr can be overriden", async () => { 601 | // IE doesn't allow us to override window.devicePixelRatio 602 | if (isIE) { 603 | return; 604 | } 605 | const oldDPR = window.devicePixelRatio; 606 | window.devicePixelRatio = 2; 607 | 608 | const targetWidth = 105; 609 | const targetHeight = 110; 610 | const sut = await renderBGAndWaitUntilLoaded( 611 |
612 | 613 | 621 |
Content
622 |
623 |
624 | ); 625 | 626 | const bgImageSrcURL = findURIfromSUT(sut); 627 | 628 | expect(bgImageSrcURL.getQueryParamValue("dpr")).toBe("3"); 629 | 630 | window.devicePixelRatio = oldDPR; 631 | }); 632 | it("the dpr is rounded to 2dp", async () => { 633 | const targetWidth = 105; 634 | const targetHeight = 110; 635 | const sut = await renderBGAndWaitUntilLoaded( 636 |
637 | 638 | 646 |
Content
647 |
648 |
649 | ); 650 | 651 | const bgImageSrcURL = findURIfromSUT(sut); 652 | 653 | expect(bgImageSrcURL.getQueryParamValue("dpr")).toBe("3.44"); 654 | }); 655 | 656 | it("window resize smaller", async () => { 657 | const sut = await renderBGAndWaitUntilLoaded( 658 |
659 | 660 | 667 |
Content
668 |
669 |
670 | ); 671 | 672 | const fakeWindowEl = document.querySelector(".fake-window"); 673 | 674 | // Simulate browser resize 675 | const BROWSER_WIDTH = 500; 676 | fakeWindowEl.style.width = BROWSER_WIDTH + "px"; 677 | 678 | await new Promise((resolve) => { 679 | setTimeout(resolve, 1000); 680 | }); 681 | 682 | const bgImageSrcURL = findURIfromSUT(sut); 683 | 684 | const expectedWidth = BROWSER_WIDTH / 2; 685 | // We're only resizing if a wider image is required. 686 | // If the browser width shrinks, then the "w" we're 687 | // getting should be greater than or equal to the 688 | // expectedWidth (in this case "w" doesn't change at 689 | // all). 690 | expect( 691 | parseInt(bgImageSrcURL.getQueryParamValue("w")) 692 | ).toBeGreaterThanOrEqual(expectedWidth); 693 | }); 694 | 695 | it("window resize larger", async () => { 696 | const sut = await renderBGAndWaitUntilLoaded( 697 |
698 | 699 | 706 |
Content
707 |
708 |
709 | ); 710 | 711 | // Before resize: 712 | const startWidth = 500; 713 | const bgImageSrcURLBefore = findURIfromSUT(sut); 714 | expect(parseInt(bgImageSrcURLBefore.getQueryParamValue("w"))).toEqual( 715 | startWidth 716 | ); 717 | 718 | const fakeWindowEl = document.querySelector(".fake-window"); 719 | 720 | // Simulate browser resize 721 | const BROWSER_WIDTH = 1080; 722 | fakeWindowEl.style.width = BROWSER_WIDTH + "px"; 723 | 724 | await new Promise((resolve) => { 725 | setTimeout(resolve, 1000); 726 | }); 727 | 728 | const bgImageSrcURL = findURIfromSUT(sut); 729 | 730 | // After resize: 731 | const expectedWidth = BROWSER_WIDTH / 2; 732 | 733 | // We've resized, the "w" should be strictly greater than the startWidth. 734 | expect(parseInt(bgImageSrcURL.getQueryParamValue("w"))).toBeGreaterThan( 735 | startWidth 736 | ); 737 | expect(parseInt(bgImageSrcURL.getQueryParamValue("w"))).toEqual( 738 | expectedWidth 739 | ); 740 | }); 741 | 742 | it("can pass ref to component", async () => { 743 | let ref = false; 744 | const onRef = (el) => { 745 | ref = el; 746 | }; 747 | const sut = await new Promise((resolve, reject) => { 748 | const el = ( 749 |
750 | 751 | 756 |
Content
757 |
758 |
759 | ); 760 | const renderedEl = renderIntoContainer(el); 761 | setTimeout(() => resolve(renderedEl), DELAY); 762 | }); 763 | 764 | expect(ref).toBeTruthy(); 765 | expect(ref instanceof HTMLElement).toBe(true); 766 | expect(findURIfromSUT(sut).getQueryParamValue("w")).toBe("100"); 767 | }); 768 | it("the fit parameter defaults to 'crop'", async () => { 769 | const sut = await renderBGAndWaitUntilLoaded( 770 |
771 | 772 |
Content
773 |
774 |
775 | ); 776 | 777 | const bgImageSrcURL = findURIfromSUT(sut); 778 | expect(bgImageSrcURL.getQueryParamValue("fit")).toBe("crop"); 779 | }); 780 | it("the fit parameter can be overriden", async () => { 781 | const sut = await renderBGAndWaitUntilLoaded( 782 |
783 | 784 |
Content
785 |
786 |
787 | ); 788 | 789 | const bgImageSrcURL = findURIfromSUT(sut); 790 | expect(bgImageSrcURL.getQueryParamValue("fit")).toBe("clip"); 791 | }); 792 | }); 793 | 794 | function injectScript(src) { 795 | return new Promise((resolve, reject) => { 796 | const script = document.createElement("script"); 797 | script.async = true; 798 | script.src = src; 799 | script.addEventListener("load", () => resolve(script)); 800 | script.addEventListener("error", () => reject("Error loading script.")); 801 | script.addEventListener("abort", () => reject("Script loading aborted.")); 802 | document.head.appendChild(script); 803 | }); 804 | } 805 | 806 | describe("Lazysizes support", () => { 807 | let script; 808 | beforeEach(async () => { 809 | script = await injectScript( 810 | "https://cdnjs.cloudflare.com/ajax/libs/lazysizes/4.1.2/lazysizes.min.js" 811 | ); 812 | }); 813 | afterEach(async () => { 814 | document.head.removeChild(script); 815 | script = null; 816 | }); 817 | it("lazy loading", async () => { 818 | const component = ( 819 | 830 | ); 831 | 832 | const renderedImage = renderIntoContainer(component); 833 | const renderedImageElement = renderedImage.getDOMNode(); 834 | lazySizes.loader.unveil(renderedImageElement); 835 | await new Promise((resolve) => setTimeout(resolve, 1)); // Timeout allows DOM to update 836 | 837 | const actualSrc = renderedImageElement.getAttribute("src"); 838 | const actualSrcSet = renderedImageElement.getAttribute("srcset"); 839 | 840 | expect(actualSrc).toContain(src); 841 | expect(actualSrcSet).toContain(src); 842 | }); 843 | 844 | it("LQIP", async () => { 845 | const lqipSrc = `${src}?w=10&h=10`; 846 | const component = ( 847 | 861 | ); 862 | 863 | const renderedImage = renderIntoContainer(component); 864 | const renderedImageElement = renderedImage.getDOMNode(); 865 | await new Promise((resolve, reject) => { 866 | const mutationObserver = new MutationObserver(function (mutations) { 867 | actualSrc = renderedImageElement.getAttribute("src"); 868 | const actualSrcSet = renderedImageElement.getAttribute("srcset"); 869 | 870 | expect(actualSrc).toContain(src); 871 | expect(actualSrcSet).toContain(src); 872 | resolve(); 873 | }); 874 | 875 | mutationObserver.observe(renderedImageElement, { 876 | attributes: true, 877 | }); 878 | 879 | let actualSrc = renderedImageElement.src; 880 | expect(actualSrc).toBe(lqipSrc); 881 | 882 | lazySizes.loader.unveil(renderedImageElement); 883 | }); 884 | }).timeout(10000); 885 | }); 886 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.expect = require("expect"); 2 | -------------------------------------------------------------------------------- /test/setupIntegration.js: -------------------------------------------------------------------------------- 1 | require("./setup"); 2 | 3 | import "@babel/polyfill"; 4 | 5 | import { configure } from "enzyme"; 6 | import Adapter from "enzyme-adapter-react-16"; 7 | const configureEnzymeWithAdapter = () => { 8 | configure({ adapter: new Adapter() }); 9 | }; 10 | 11 | configureEnzymeWithAdapter(); 12 | -------------------------------------------------------------------------------- /test/setupUnit.js: -------------------------------------------------------------------------------- 1 | require("./setup"); 2 | import "@babel/polyfill"; 3 | import { configure } from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-16"; 5 | import { PublicConfigAPI } from "../src"; 6 | 7 | // TODO: Only initialise jest-extended for unit tests until https://github.com/jest-community/jest-extended/pull/140 is merged. 8 | const addExtraJestMatchers = () => require("jest-extended"); 9 | 10 | addExtraJestMatchers(); 11 | 12 | const configureEnzymeWithAdapter = () => { 13 | configure({ adapter: new Adapter() }); 14 | }; 15 | 16 | configureEnzymeWithAdapter(); 17 | 18 | PublicConfigAPI.disableWarning("fallbackImage"); 19 | PublicConfigAPI.disableWarning("sizesAttribute"); 20 | -------------------------------------------------------------------------------- /test/tests.webpack.js: -------------------------------------------------------------------------------- 1 | require("./setupIntegration"); 2 | 3 | var context = require.context("./integration", true, /\.jsx?$/); 4 | context.keys().forEach(context); 5 | -------------------------------------------------------------------------------- /test/unit/attributes.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from "enzyme"; 2 | import React from "react"; 3 | import ReactImgix from "../../src/index"; 4 | 5 | const imageProps = { 6 | src: "https://assets.imgix.net/examples/pione.jpg", 7 | sizes: "50vw", 8 | disableSrcSet: true, 9 | disableLibraryParam: true, 10 | }; 11 | 12 | describe("ReactImgix", () => { 13 | test("should set alt prop on the react component", () => { 14 | const component = ; 15 | 16 | const expectedImageTagProps = { 17 | alt: "a cute dog", 18 | sizes: "50vw", 19 | className: undefined, 20 | width: undefined, 21 | height: undefined, 22 | src: "https://assets.imgix.net/examples/pione.jpg?auto=format", 23 | }; 24 | 25 | const renderedComponent = mount(component); 26 | const renderedImageTagProps = renderedComponent.find("img").props(); 27 | 28 | expect(renderedImageTagProps).toEqual(expectedImageTagProps); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/unit/build-url.test.js: -------------------------------------------------------------------------------- 1 | import { buildURL } from "index"; 2 | 3 | test("buildURL builds a url correctly", () => { 4 | const actual = buildURL("https://demo.imgix.net/abc.png", { w: 450, h: 100 }); 5 | 6 | const actualURL = new URL(actual); 7 | expect(actualURL.searchParams.get("w")).toBe("450"); 8 | expect(actualURL.searchParams.get("h")).toBe("100"); 9 | }); 10 | 11 | test("buildURL includes an ixlib parameter", () => { 12 | const actual = buildURL("https://demo.imgix.net/abc.png", { w: 450, h: 100 }); 13 | 14 | const actualURL = new URL(actual); 15 | expect(actualURL.searchParams.get("ixlib")).toEqual( 16 | createIxLibParamMatcher() 17 | ); 18 | }); 19 | 20 | test("ixlib addition can be disabled with disableLibraryParam", () => { 21 | const actual = buildURL( 22 | "https://demo.imgix.net/abc.png", 23 | { w: 450, h: 100 }, 24 | { disableLibraryParam: true } 25 | ); 26 | 27 | const actualURL = new URL(actual); 28 | expect(actualURL.searchParams.has("ixlib")).toBe(false); 29 | }); 30 | 31 | test("parameters that already exist in the url are overriden", () => { 32 | const actual = buildURL("https://demo.imgix.net/abc.png?w=50&h=50", { 33 | w: 100, 34 | h: 100, 35 | }); 36 | 37 | const actualURL = new URL(actual); 38 | expect(actualURL.searchParams.get("w")).toBe("100"); 39 | expect(actualURL.searchParams.get("h")).toBe("100"); 40 | }); 41 | 42 | const createIxLibParamMatcher = () => { 43 | const expectedVersion = require("read-pkg-up").sync().pkg.version; 44 | const expectedParam = `react-${expectedVersion}`; 45 | 46 | return expect.stringContaining(expectedParam); 47 | }; 48 | -------------------------------------------------------------------------------- /test/unit/collapse-params.test.js: -------------------------------------------------------------------------------- 1 | import { collapseImgixParams } from "../../src/HOFs/propFormatter"; 2 | 3 | describe("collapseImgixParams()", () => { 4 | it("should shorten params keys", () => { 5 | // TODO(luis): break these up into individual tests? 6 | const params = { 7 | brightness: 1, 8 | contrast: 1, 9 | exposure: 1, 10 | gamma: 1, 11 | highlights: 1, 12 | hue: 1, 13 | invert: 1, 14 | saturation: 1, 15 | shaddows: 1, 16 | shadows: 1, 17 | sharpness: 1, 18 | "unsharp-mask": 1, 19 | "unsharp-radius": 1, 20 | vibrance: 1, 21 | "auto-features": 1, 22 | "background-color": 1, 23 | blend: 1, 24 | "blend-mode": 1, 25 | "blend-align": 1, 26 | "blend-alpha": 1, 27 | "blend-padding": 1, 28 | "blend-width": 1, 29 | "blend-height": 1, 30 | "blend-fit": 1, 31 | "blend-crop": 1, 32 | "blend-size": 1, 33 | "blend-x": 1, 34 | "blend-y": 1, 35 | border: 1, 36 | padding: 1, 37 | "face-index": 1, 38 | "face-padding": 1, 39 | faces: 1, 40 | "chroma-subsampling": 1, 41 | "color-quantization": 1, 42 | download: 1, 43 | DPI: 1, 44 | format: 1, 45 | "lossless-compression": 1, 46 | quality: 1, 47 | "mask-image": 1, 48 | "noise-blur": 1, 49 | "noise-sharpen": 1, 50 | "flip-direction": 1, 51 | orientation: 1, 52 | "rotation-angle": 1, 53 | "crop-mode": 1, 54 | "fit-mode": 1, 55 | "image-height": 1, 56 | "image-width": 1, 57 | blurring: 1, 58 | halftone: 1, 59 | monotone: 1, 60 | pixelate: 1, 61 | "sepia-tone": 1, 62 | height: 1, 63 | width: 1, 64 | }; 65 | const expected = { 66 | bri: 1, 67 | con: 1, 68 | exp: 1, 69 | gam: 1, 70 | high: 1, 71 | hue: 1, 72 | invert: 1, 73 | sat: 1, 74 | shad: 1, 75 | sharp: 1, 76 | usm: 1, 77 | usmrad: 1, 78 | vib: 1, 79 | auto: 1, 80 | bg: 1, 81 | blend: 1, 82 | bm: 1, 83 | ba: 1, 84 | balph: 1, 85 | bp: 1, 86 | bw: 1, 87 | bh: 1, 88 | bf: 1, 89 | bc: 1, 90 | bs: 1, 91 | bx: 1, 92 | by: 1, 93 | border: 1, 94 | pad: 1, 95 | faceindex: 1, 96 | facepad: 1, 97 | faces: 1, 98 | chromasub: 1, 99 | colorquant: 1, 100 | dl: 1, 101 | dpi: 1, 102 | fm: 1, 103 | lossless: 1, 104 | q: 1, 105 | mask: 1, 106 | nr: 1, 107 | nrs: 1, 108 | flip: 1, 109 | or: 1, 110 | rot: 1, 111 | crop: 1, 112 | fit: 1, 113 | h: 1, 114 | w: 1, 115 | blur: 1, 116 | htn: 1, 117 | mono: 1, 118 | px: 1, 119 | sepia: 1, 120 | h: 1, 121 | w: 1, 122 | }; 123 | 124 | const result = collapseImgixParams(params); 125 | expect(result).toEqual(expected); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/unit/config.test.js: -------------------------------------------------------------------------------- 1 | import config, { PublicConfigAPI } from "config"; 2 | 3 | test("warnings can be disabled", () => { 4 | const warnings = Object.keys(config.warnings); 5 | if (warnings.length < 1) { 6 | fail("No warnings to configure"); 7 | } 8 | const warning = warnings[0]; 9 | // Set warning to true 10 | const oldValue = config.warnings[warning]; 11 | config.warnings[warning] = true; 12 | 13 | PublicConfigAPI.disableWarning(warning); 14 | 15 | expect(config.warnings[warning]).toBeFalse(); 16 | 17 | config.warnings[warning] = oldValue; 18 | }); 19 | 20 | test("warnings can be disabled", () => { 21 | const warnings = Object.keys(config.warnings); 22 | if (warnings.length < 1) { 23 | fail("No warnings to configure"); 24 | } 25 | const warning = warnings[0]; 26 | // Set warning to false 27 | const oldValue = config.warnings[warning]; 28 | config.warnings[warning] = false; 29 | 30 | PublicConfigAPI.enableWarning(warning); 31 | 32 | expect(config.warnings[warning]).toBeTrue(); 33 | 34 | config.warnings[warning] = oldValue; 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/encoding.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from "enzyme"; 2 | import React from "react"; 3 | import ReactImgix, { ImgixProvider, Source, Background } from "../../src/index"; 4 | 5 | const imageProps = { 6 | src: "https://assets.imgix.net/examples/space bridge.jpg", 7 | }; 8 | 9 | describe("ReactImgix", () => { 10 | test("should encode src path", () => { 11 | const component = ; 12 | 13 | const expectedSrc = "https://assets.imgix.net/examples/space%20bridge.jpg"; 14 | 15 | const renderedComponent = mount(component); 16 | const renderedImageTagProps = renderedComponent.find("img").props(); 17 | 18 | expect(renderedImageTagProps.src).toMatch(expectedSrc); 19 | }); 20 | test("should not encode the src path if disablePathEncoding is set to true", () => { 21 | const component = ; 22 | 23 | const expectedSrc = "https://assets.imgix.net/examples/space bridge.jpg"; 24 | 25 | const renderedComponent = mount(component); 26 | const renderedImageTagProps = renderedComponent.find("img").props(); 27 | 28 | expect(renderedImageTagProps.src).toMatch(expectedSrc); 29 | }); 30 | 31 | test("Background should not encode the src path if disablePathEncoding is set to true", () => { 32 | const encodedSrc = 33 | "https://sdk-test.imgix.net/%26%24%2B%2C%3A%3B%3D%3F%40%23.jpg"; 34 | // Force BG to render immediately by giving it w,h params, contentRect bounds 35 | // and a measureRef that points to null 36 | const component = ( 37 | null} 43 | imgixParams={{ w: 100, h: 100 }} 44 | contentRect={{ bounds: { top: 10, width: 100, height: 100 } }} 45 | > 46 |
Content
47 |
48 | ); 49 | const expectedBgUrl = `url(${encodedSrc}?fit=crop&w=100&h=100&dpr=1)`; 50 | 51 | const renderedComponent = mount(component); 52 | const renderedBg = renderedComponent.find("div.bg-test").props(); 53 | 54 | expect(renderedBg.style.backgroundImage).toMatch(expectedBgUrl); 55 | }); 56 | 57 | test("should encode the srcset", () => { 58 | const component = ; 59 | 60 | const expectedSrc = "https://assets.imgix.net/examples/space%20bridge.jpg"; 61 | 62 | const renderedComponent = mount(component); 63 | const renderedImageTagProps = renderedComponent.find("img").props(); 64 | 65 | expect(renderedImageTagProps.srcSet).toMatch(expectedSrc); 66 | }); 67 | test("should not encode the srcset if disablePathEncoding is set to true", () => { 68 | const component = ; 69 | 70 | const expectedSrc = "https://assets.imgix.net/examples/space bridge.jpg"; 71 | 72 | const renderedComponent = mount(component); 73 | const renderedImageTagProps = renderedComponent.find("img").props(); 74 | 75 | expect(renderedImageTagProps.srcSet).toMatch(expectedSrc); 76 | }); 77 | it("should not encode the src or srcset path if disablePathEncoding is set on ", () => { 78 | const providerProps = { 79 | disablePathEncoding: true, 80 | }; 81 | const wrappedComponent = ( 82 | 83 | 84 | 85 | ); 86 | 87 | const expectedSrc = "https://assets.imgix.net/examples/space bridge.jpg"; 88 | 89 | const renderedComponent = mount(wrappedComponent); 90 | const renderedImageTagProps = renderedComponent.find("img").props(); 91 | expect(renderedImageTagProps.src).toMatch(expectedSrc); 92 | expect(renderedImageTagProps.srcSet).toMatch(expectedSrc); 93 | }); 94 | }); 95 | describe("Source", () => { 96 | test("should encode srcSet path", () => { 97 | const component = ; 98 | 99 | const expectedSrc = "https://assets.imgix.net/examples/space%20bridge.jpg"; 100 | 101 | const renderedComponent = mount(component); 102 | const renderedSourceTagProps = renderedComponent.find("source").props(); 103 | 104 | expect(renderedSourceTagProps.srcSet).toMatch(expectedSrc); 105 | }); 106 | test("should not encode the srcSet path if disablePathEncoding is set to true", () => { 107 | const component = ; 108 | 109 | const expectedSrc = "https://assets.imgix.net/examples/space bridge.jpg"; 110 | 111 | const renderedComponent = mount(component); 112 | const renderedSourceTagProps = renderedComponent.find("source").props(); 113 | 114 | expect(renderedSourceTagProps.srcSet).toMatch(expectedSrc); 115 | }); 116 | test("should encode the srcset path if disableSrcSet is true", () => { 117 | const component = ; 118 | 119 | const expectedSrc = "https://assets.imgix.net/examples/space%20bridge.jpg"; 120 | 121 | const renderedComponent = mount(component); 122 | const renderedSourceTagProps = renderedComponent.find("source").props(); 123 | 124 | expect(renderedSourceTagProps.srcSet).toMatch(expectedSrc); 125 | }); 126 | test("should not encode the srcset path if disablePathEncoding and disableSrcSet are set to true", () => { 127 | const component = ( 128 | 129 | ); 130 | 131 | const expectedSrc = "https://assets.imgix.net/examples/space bridge.jpg"; 132 | 133 | const renderedComponent = mount(component); 134 | const renderedSourceTagProps = renderedComponent.find("source").props(); 135 | 136 | expect(renderedSourceTagProps.srcSet).toMatch(expectedSrc); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/unit/extract-query-params.test.js: -------------------------------------------------------------------------------- 1 | import extractQueryParams from "extractQueryParams"; 2 | 3 | test("query param extraction", () => { 4 | const [src, params] = extractQueryParams( 5 | "https://demo.imgix.net/abc.png?w=100&auto=format,enhance" 6 | ); 7 | expect(src).toEqual("https://demo.imgix.net/abc.png"); 8 | expect(params).toEqual({ w: "100", auto: "format,enhance" }); 9 | }); 10 | 11 | test("returns empty object when no query parameters are present", () => { 12 | const [, params] = extractQueryParams("https://demo.imgix.net/abc.png"); 13 | expect(params).toEqual({}); 14 | }); 15 | 16 | test("decodes encoded query parameters", () => { 17 | const [src, params] = extractQueryParams( 18 | "https://demo.imgix.net/abc.png?auto=format%2Cenhance&foo=%2Ffoo%22%20bar" 19 | ); 20 | expect(params).toEqual({ auto: "format,enhance", foo: '/foo" bar' }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/find-closest.test.js: -------------------------------------------------------------------------------- 1 | import findClosest from "findClosest"; 2 | 3 | test("when closest value is below the search value", () => 4 | expect(findClosest(101, [100, 200])).toBe(100)); 5 | test("when closest value is above the search value", () => 6 | expect(findClosest(151, [100, 200])).toBe(200)); 7 | test("when closest value is the median of two nearest values, the largest value is returned", () => 8 | expect(findClosest(150, [100, 200])).toBe(200)); 9 | 10 | describe("Edge cases", () => { 11 | test("when value is smaller than the entire array", () => 12 | expect(findClosest(80, [100, 200])).toBe(100)); 13 | test("when value is larger than the entire array", () => 14 | expect(findClosest(205, [100, 200])).toBe(200)); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/format-props.test.js: -------------------------------------------------------------------------------- 1 | import { formatProps } from "../../src/HOFs/propFormatter"; 2 | 3 | describe("formatProps()", () => { 4 | it("should set width to undefined if negative", () => { 5 | const props = { height: 100, src: "/pione.jpg", width: -100 }; 6 | const result = formatProps(props); 7 | const expected = { height: 100, src: "/pione.jpg", width: undefined }; 8 | 9 | expect(result).toEqual(expected); 10 | }); 11 | 12 | it("should set height to undefined if negative", () => { 13 | const props = { height: -100, src: "/pione.jpg", width: 100 }; 14 | const result = formatProps(props); 15 | const expected = { height: undefined, src: "/pione.jpg", width: 100 }; 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | 20 | it("should set src to 1-step URL if domain prop present", () => { 21 | const props = { 22 | domain: "assets.imgix.net", 23 | src: "/pione.jpg", 24 | width: 100, 25 | }; 26 | const result = formatProps(props); 27 | const expected = { 28 | height: undefined, 29 | domain: "assets.imgix.net", 30 | src: "https://assets.imgix.net/pione.jpg", 31 | width: 100, 32 | }; 33 | 34 | expect(result).toEqual(expected); 35 | }); 36 | 37 | it("should set src to insecure 1-step URL if useHttp prop false", () => { 38 | const props = { 39 | domain: "assets.imgix.net", 40 | src: "/pione.jpg", 41 | width: 100, 42 | useHttps: false, 43 | }; 44 | const result = formatProps(props); 45 | const expected = { 46 | height: undefined, 47 | domain: "assets.imgix.net", 48 | src: "http://assets.imgix.net/pione.jpg", 49 | width: 100, 50 | useHttps: false, 51 | }; 52 | 53 | expect(result).toEqual(expected); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/unit/format-src.test.js: -------------------------------------------------------------------------------- 1 | import { formatSrc } from "../../src/HOFs/propFormatter"; 2 | 3 | describe("formatSrc()", () => { 4 | it("should create a 1-step URL from a 2-step URL", () => { 5 | const domain = "sdk-test.imgix.net"; 6 | const src = "pione.jpg"; 7 | const url = formatSrc(src, domain); 8 | const expected = "https://sdk-test.imgix.net/pione.jpg"; 9 | expect(url).toBe(expected); 10 | }); 11 | 12 | it("should correctly interpret trailing slashes", () => { 13 | const domain = "sdk-test.imgix.net/"; 14 | const src = "pione.jpg/"; 15 | const url = formatSrc(src, domain); 16 | const expected = "https://sdk-test.imgix.net/pione.jpg"; 17 | expect(url).toBe(expected); 18 | }); 19 | 20 | it("should correctly interpret leading slashes", () => { 21 | const domain = "/sdk-test.imgix.net"; 22 | const src = "/pione.jpg"; 23 | const url = formatSrc(src, domain); 24 | const expected = "https://sdk-test.imgix.net/pione.jpg"; 25 | expect(url).toBe(expected); 26 | }); 27 | 28 | it("should do nothing to a 2-step URL", () => { 29 | const domain = "assets.imgix.net"; 30 | const src = "https://sdk-test.imgix.net/pione.jpg"; 31 | const url = formatSrc(src, domain); 32 | const expected = "https://sdk-test.imgix.net/pione.jpg"; 33 | expect(url).toBe(expected); 34 | }); 35 | 36 | it("should do nothing if domain is undefined", () => { 37 | const domain = undefined; 38 | const src = "/pione.jpg"; 39 | const url = formatSrc(src, domain); 40 | const expected = "/pione.jpg"; 41 | expect(url).toBe(expected); 42 | }); 43 | 44 | it("should do nothing if domain is null", () => { 45 | const domain = null; 46 | const src = "/pione.jpg"; 47 | const url = formatSrc(src, domain); 48 | const expected = "/pione.jpg"; 49 | expect(url).toBe(expected); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/helpers/shallowUntilTarget.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | Taken from https://github.com/mozilla/addons-frontend/blob/58d1315409f1ad6dc9b979440794df44c1128455/tests/unit/testHelpers.js 7 | */ 8 | 9 | import { shallow } from "enzyme"; 10 | import React, { Component } from "react"; 11 | import sinon from "sinon"; 12 | import { compose } from "../../../src/common"; 13 | 14 | import { shallowUntilTarget } from "../../helpers"; 15 | 16 | describe("helpers", () => { 17 | describe("shallowUntilTarget", () => { 18 | function ExampleBase() { 19 | return
Example component
; 20 | } 21 | 22 | function wrapper() { 23 | return function Wrapper(WrappedComponent) { 24 | return function InnerWrapper(props) { 25 | return ; 26 | }; 27 | }; 28 | } 29 | 30 | it("requires a componentInstance", () => { 31 | expect(() => shallowUntilTarget(undefined, ExampleBase)).toThrow( 32 | "componentInstance parameter is required" 33 | ); 34 | }); 35 | 36 | it("requires a valid component instance", () => { 37 | expect(() => { 38 | shallowUntilTarget({ notAComponent: true }, ExampleBase); 39 | }).toThrow(/ShallowWrapper can only wrap valid elements/); 40 | }); 41 | 42 | it("requires a TargetComponent", () => { 43 | const Example = compose(wrapper())(ExampleBase); 44 | 45 | expect(() => shallowUntilTarget(, undefined)).toThrow( 46 | "TargetComponent parameter is required" 47 | ); 48 | }); 49 | 50 | it("lets you unwrap a component one level", () => { 51 | const Example = compose(wrapper())(ExampleBase); 52 | 53 | const root = shallowUntilTarget(, ExampleBase); 54 | expect(root.text()).toEqual("Example component"); 55 | }); 56 | 57 | it("lets you unwrap a component two levels", () => { 58 | const Example = compose(wrapper(), wrapper())(ExampleBase); 59 | 60 | const root = shallowUntilTarget(, ExampleBase); 61 | expect(root.text()).toEqual("Example component"); 62 | }); 63 | 64 | it("lets you unwrap a React class based component", () => { 65 | class ReactExampleBase extends Component { 66 | render() { 67 | return
example of class based component
; 68 | } 69 | } 70 | 71 | const Example = compose(wrapper())(ReactExampleBase); 72 | 73 | const root = shallowUntilTarget(, ReactExampleBase); 74 | expect(root.instance()).toBeInstanceOf(ReactExampleBase); 75 | }); 76 | 77 | it("does not let you unwrap a component that is not wrapped", () => { 78 | expect(() => { 79 | shallowUntilTarget(, ExampleBase); 80 | }).toThrow(/Cannot unwrap this component because it is not wrapped/); 81 | }); 82 | 83 | it("gives up trying to unwrap component after maxTries", () => { 84 | const Example = compose(wrapper(), wrapper(), wrapper())(ExampleBase); 85 | 86 | expect(() => { 87 | shallowUntilTarget(, ExampleBase, { 88 | maxTries: 2, 89 | }); 90 | }).toThrow(/Could not find .*gave up after 2 tries/); 91 | }); 92 | 93 | it("lets you pass options to shallow()", () => { 94 | const shallowStub = sinon.spy(shallow); 95 | 96 | const Example = compose(wrapper())(ExampleBase); 97 | 98 | const shallowOptions = { 99 | lifecycleExperimental: true, 100 | }; 101 | const instance = ; 102 | shallowUntilTarget(instance, ExampleBase, { 103 | shallowOptions, 104 | _shallow: shallowStub, 105 | }); 106 | 107 | sinon.assert.calledWith(shallowStub, instance, shallowOptions); 108 | }); 109 | 110 | it("lets you pass options to the final shallow()", () => { 111 | const componentDidUpdate = sinon.stub(); 112 | 113 | class LifecyleExample extends Component { 114 | componentDidUpdate() { 115 | componentDidUpdate(); 116 | } 117 | 118 | render() { 119 | return
example of using lifecycle methods
; 120 | } 121 | } 122 | 123 | const Example = compose(wrapper())(LifecyleExample); 124 | 125 | const root = shallowUntilTarget(, LifecyleExample, { 126 | shallowOptions: { 127 | lifecycleExperimental: true, 128 | }, 129 | }); 130 | root.setProps({ something: "else" }); 131 | 132 | sinon.assert.called(componentDidUpdate); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/unit/imgix-provider.test.jsx: -------------------------------------------------------------------------------- 1 | import { mount, shallow } from "enzyme"; 2 | import React from "react"; 3 | import ReactImgix, { ImgixProvider, Background } from "../../src/index"; 4 | 5 | const providerProps = { 6 | domain: "sdk-test.imgix.net", 7 | sizes: "100vw", 8 | }; 9 | 10 | const imageProps = { 11 | src: "https://assets.imgix.net/examples/pione.jpg", 12 | sizes: "50vw", 13 | }; 14 | 15 | describe("ImgixProvider", () => { 16 | test("should not have context value defined if Provider has no props", () => { 17 | const wrappedComponent = ( 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | const renderedComponent = shallow(wrappedComponent); 25 | 26 | // Inspect the rendered children directly 27 | const renderedChildren = renderedComponent.children(); 28 | 29 | expect(renderedChildren.length).toBe(2); // Assuming you have two children 30 | expect(renderedChildren.at(0).props()).toEqual({ 31 | src: 'https://assets.imgix.net/examples/pione.jpg', 32 | sizes: imageProps.sizes, 33 | }); 34 | 35 | expect(renderedChildren.at(1).props()).toEqual({ 36 | src: 'https://assets.imgix.net/examples/pione.jpg', 37 | sizes: imageProps.sizes, 38 | }); 39 | 40 | expect(renderedComponent.prop('value')).toEqual({}); 41 | }); 42 | 43 | test("should set the context value to the Provider props", () => { 44 | const wrappedComponent = ( 45 | 46 | 47 | 48 | 49 | ); 50 | 51 | const renderedComponent = shallow(wrappedComponent); 52 | 53 | // Inspect the rendered children directly 54 | const renderedChildren = renderedComponent.children(); 55 | 56 | const expectedProps = { 57 | value: { domain: "sdk-test.imgix.net", sizes: "100vw" }, 58 | }; 59 | 60 | expect(renderedChildren.length).toBe(2); // Assuming you have two children 61 | expect(renderedChildren.at(0).props()).toEqual({ 62 | src: 'https://assets.imgix.net/examples/pione.jpg', 63 | sizes: '50vw', 64 | }); 65 | expect(renderedChildren.at(1).props()).toEqual({ 66 | src: 'https://assets.imgix.net/examples/pione.jpg', 67 | sizes: '50vw', 68 | }); 69 | 70 | expect(renderedComponent.prop('value')).toEqual(expectedProps.value); 71 | }); 72 | 73 | test("should merge the Provider and Child props", () => { 74 | const modifiedProps = { 75 | ...imageProps, 76 | src: "examples/pione.jpg", 77 | sizes: null, 78 | }; 79 | 80 | const wrappedComponent = ( 81 | 82 | 83 | 84 | 85 | ); 86 | 87 | // ensure Provider and Child props are merged as intended 88 | const expectedProps = { 89 | domain: "sdk-test.imgix.net", 90 | height: undefined, 91 | imgixParams: undefined, 92 | onMounted: undefined, 93 | sizes: null, 94 | src: "https://sdk-test.imgix.net/examples/pione.jpg", 95 | width: undefined, 96 | }; 97 | 98 | const expectedReactImgixProps = { 99 | ...expectedProps, 100 | disableSrcSet: false, 101 | }; 102 | 103 | const expectedBgProps = { 104 | ...expectedProps, 105 | }; 106 | 107 | // The order of the childAt() needs to update if number of HOCs change. 108 | const renderedComponent = mount(wrappedComponent); //ImgixProvider 109 | 110 | const renderedProps = renderedComponent 111 | .childAt(0) // mergePropsHOF 112 | .childAt(0) // processPropsHOF 113 | .childAt(0) // shouldComponentUpdateHOC 114 | .childAt(0) // ChildComponent 115 | .props(); 116 | 117 | const renderedBackgroundProps = renderedComponent 118 | .childAt(1) // mergePropsHOF 119 | .childAt(0) // processPropsHOF 120 | .childAt(0) // withContentRect 121 | .props(); 122 | 123 | // remove noop function that breaks tests 124 | renderedProps.onMounted = undefined; 125 | 126 | expect(renderedProps).toEqual(expectedReactImgixProps); 127 | expect(renderedBackgroundProps).toEqual(expectedBgProps); 128 | }); 129 | 130 | test("should log error when has no consumers", () => { 131 | jest.spyOn(global.console, "error").mockImplementation(() => {}); 132 | 133 | const wrappedComponent = ; 134 | 135 | const expectedProps = { 136 | children: undefined, 137 | value: providerProps, 138 | }; 139 | 140 | const renderedComponent = shallow(wrappedComponent); 141 | 142 | expect(renderedComponent.props()).toEqual(expectedProps); 143 | expect(console.error).toHaveBeenCalledWith( 144 | expect.stringContaining( 145 | "ImgixProvider must have at least one Imgix child component" 146 | ) 147 | ); 148 | 149 | jest.clearAllMocks(); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/unit/propMerger.test.js: -------------------------------------------------------------------------------- 1 | import { mergeProps } from "../../src/HOFs/propMerger"; 2 | 3 | describe("mergeProps()", () => { 4 | it("should merge the `src` object into the `destination` object", () => { 5 | const src = { width: 100, height: 200, imgixParams: { ar: "1:2", dpr: 2 } }; 6 | const destination = {}; 7 | const result = mergeProps(src, destination); 8 | 9 | expect(result).toEqual({ 10 | width: 100, 11 | height: 200, 12 | imgixParams: { ar: "1:2", dpr: 2 }, 13 | }); 14 | }); 15 | 16 | it("should not overwrite destination values with source values", () => { 17 | const src = { width: 100 }; 18 | const destination = { width: 101 }; 19 | const result = mergeProps(src, destination); 20 | 21 | expect(result).toEqual({ width: 101 }); 22 | }); 23 | 24 | it("should not overwrite destination resolving to `null` if source exists", () => { 25 | const src = { domain: "foo.bar", height: 100 }; 26 | const destination = { height: null }; 27 | const result = mergeProps(src, destination); 28 | 29 | expect(result).toEqual({ domain: "foo.bar", height: null }); 30 | }); 31 | 32 | it("should not overwrite destination resolving to `undefined` if source exists", () => { 33 | const src = { domain: "foo.bar", height: 100 }; 34 | const destination = { height: undefined }; 35 | const result = mergeProps(src, destination); 36 | 37 | expect(result).toEqual({ domain: "foo.bar", height: undefined }); 38 | }); 39 | 40 | it("should recursively merge imgixParams and htmlAttributes", () => { 41 | const src = { 42 | imgixParams: { ar: "1:2", dpr: 2 }, 43 | htmlAttributes: { styles: "width: 50", alt: "src" }, 44 | }; 45 | const destination = { 46 | imgixParams: { dpr: 1, fit: "fill" }, 47 | htmlAttributes: { styles: "width: 100", className: "destination" }, 48 | }; 49 | const result = mergeProps(src, destination); 50 | 51 | expect(result).toEqual({ 52 | imgixParams: { ar: "1:2", dpr: 1, fit: "fill" }, 53 | htmlAttributes: { 54 | styles: "width: 100", 55 | className: "destination", 56 | alt: "src", 57 | }, 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/unit/react-imgix.test.jsx: -------------------------------------------------------------------------------- 1 | import { mount } from "enzyme"; 2 | import { PublicConfigAPI } from "index"; 3 | import React from "react"; 4 | import Imgix, { 5 | Picture, 6 | Source, 7 | __PictureImpl, 8 | __ReactImgixImpl, 9 | __SourceImpl, 10 | } from "react-imgix"; 11 | import { __BackgroundImpl } from "react-imgix-bg"; 12 | import sinon from "sinon"; 13 | import targetWidths from "targetWidths"; 14 | import { shallowUntilTarget } from "../helpers"; 15 | 16 | const DPR_QUALITY = { 17 | q_dpr1: 75, 18 | q_dpr2: 50, 19 | q_dpr3: 35, 20 | q_dpr4: 23, 21 | q_dpr5: 20, 22 | }; 23 | 24 | function shallow(element, target = __ReactImgixImpl, shallowOptions) { 25 | return shallowUntilTarget(element, target, { 26 | shallowOptions: shallowOptions || { 27 | disableLifecycleMethods: true, 28 | }, 29 | }); 30 | } 31 | const shallowSource = (element) => shallow(element, __SourceImpl); 32 | const shallowPicture = (element) => shallow(element, __PictureImpl); 33 | 34 | const makeBackgroundWithBounds = (bounds) => (props) => 35 | ( 36 | <__BackgroundImpl 37 | measureRef={() => null} 38 | contentRect={{ bounds }} 39 | {...props} 40 | /> 41 | ); 42 | 43 | const src = "http://domain.imgix.net/image.jpg"; 44 | let sut; 45 | 46 | describe("When in default mode", () => { 47 | beforeEach(() => { 48 | jest.spyOn(global.console, "warn").mockImplementation((msg) => { 49 | console.log(msg); 50 | }); 51 | }); 52 | afterAll(() => { 53 | jest.clearAllMocks(); 54 | }); 55 | it("the rendered element's type should be img", () => { 56 | const sut = shallow(); 57 | expect(sut.type()).toBe("img"); 58 | }); 59 | describe("srcset", () => { 60 | it("the rendered element should have a srcSet set correctly", async () => { 61 | const sut = shallow(); 62 | const srcset = sut.props().srcSet; 63 | expect(srcset).not.toBeUndefined(); 64 | expect(srcset.split(",\n")[0].split(" ")).toHaveLength(2); 65 | const aSrcFromSrcSet = srcset.split(",\n")[0].split(" ")[0]; 66 | expect(aSrcFromSrcSet).toContain(src); 67 | const aWidthFromSrcSet = srcset.split(",\n")[0].split(" ")[1]; 68 | expect(aWidthFromSrcSet).toMatch(/^\d+w$/); 69 | }); 70 | it("returns the expected number of `url widthDescriptor` pairs", function () { 71 | const sut = shallow(); 72 | const srcset = sut.props().srcSet; 73 | 74 | expect(srcset.split(",").length).toEqual(targetWidths.length); 75 | }); 76 | 77 | it("should not exceed the bounds of [100, 8192]", () => { 78 | const sut = shallow(); 79 | const srcset = sut.props().srcSet; 80 | 81 | const srcsetWidths = srcset 82 | .split(", ") 83 | .map((srcset) => srcset.split(" ")[1]) 84 | .map((width) => width.slice(0, -1)) 85 | .map(Number.parseFloat); 86 | 87 | const min = Math.min(...srcsetWidths); 88 | const max = Math.max(...srcsetWidths); 89 | 90 | expect(min).not.toBeLessThan(100); 91 | expect(max).not.toBeGreaterThan(8192); 92 | }); 93 | 94 | // 18% used to allow +-1% for rounding 95 | it("should not increase more than 18% every iteration", () => { 96 | const INCREMENT_ALLOWED = 0.18; 97 | 98 | const sut = shallow(); 99 | const srcset = sut.props().srcSet; 100 | 101 | const srcsetWidths = srcset 102 | .split(", ") 103 | .map((srcset) => srcset.split(" ")[1]) 104 | .map((width) => width.slice(0, -1)) 105 | .map(Number.parseFloat); 106 | 107 | let prev = srcsetWidths[0]; 108 | 109 | for (let index = 1; index < srcsetWidths.length; index++) { 110 | const element = srcsetWidths[index]; 111 | expect(element / prev).toBeLessThan(1 + INCREMENT_ALLOWED); 112 | prev = element; 113 | } 114 | }); 115 | 116 | describe("supports varying q to dpr matching when rendering a fixed-size image", () => { 117 | it("generates predefined q and dpr pairs", async () => { 118 | const sut = shallow(); 119 | const srcset = sut.props().srcSet.split(",\n"); 120 | 121 | expect(srcset[0].split(" ")[0]).toContain("q=" + DPR_QUALITY.q_dpr1); 122 | expect(srcset[1].split(" ")[0]).toContain("q=" + DPR_QUALITY.q_dpr2); 123 | expect(srcset[2].split(" ")[0]).toContain("q=" + DPR_QUALITY.q_dpr3); 124 | expect(srcset[3].split(" ")[0]).toContain("q=" + DPR_QUALITY.q_dpr4); 125 | expect(srcset[4].split(" ")[0]).toContain("q=" + DPR_QUALITY.q_dpr5); 126 | }); 127 | it("allows q to dpr matching to be disabled", async () => { 128 | const sut = shallow( 129 | 130 | ); 131 | const srcset = sut.props().srcSet.split(",\n"); 132 | 133 | expect(srcset[0].split(" ")[0]).not.toContain( 134 | "q=" + DPR_QUALITY.q_dpr1 135 | ); 136 | expect(srcset[1].split(" ")[0]).not.toContain( 137 | "q=" + DPR_QUALITY.q_dpr2 138 | ); 139 | expect(srcset[2].split(" ")[0]).not.toContain( 140 | "q=" + DPR_QUALITY.q_dpr3 141 | ); 142 | expect(srcset[3].split(" ")[0]).not.toContain( 143 | "q=" + DPR_QUALITY.q_dpr4 144 | ); 145 | expect(srcset[4].split(" ")[0]).not.toContain( 146 | "q=" + DPR_QUALITY.q_dpr5 147 | ); 148 | }); 149 | it("allows the q parameter to be overriden when explicitly passed in", async () => { 150 | const q_override = 100; 151 | const sut = shallow( 152 | 153 | ); 154 | const srcset = sut.props().srcSet.split(",\n"); 155 | 156 | expect(srcset[0].split(" ")[0]).toContain("q=" + q_override); 157 | expect(srcset[1].split(" ")[0]).toContain("q=" + q_override); 158 | expect(srcset[2].split(" ")[0]).toContain("q=" + q_override); 159 | expect(srcset[3].split(" ")[0]).toContain("q=" + q_override); 160 | expect(srcset[4].split(" ")[0]).toContain("q=" + q_override); 161 | }); 162 | }); 163 | 164 | describe("supports custom srcSet options", () => { 165 | it('allows the default "widths" option to be overriden', () => { 166 | const sut = shallow( 167 | 173 | ); 174 | const srcset = sut.props().srcSet; 175 | 176 | const srcsetWidths = srcset 177 | .replace(/[\n\s]+/g, " ") 178 | .split(", ") 179 | .map((srcset) => srcset.split(" ")[1]) 180 | .map((width) => width.slice(0, -1)) 181 | .map(Number.parseFloat); 182 | 183 | expect(srcsetWidths).toEqual([100, 200, 300]); 184 | }); 185 | 186 | it('allows the default "widthTolerance" option to be overriden', () => { 187 | const sut = shallow( 188 | 194 | ); 195 | const srcset = sut.props().srcSet; 196 | 197 | const srcsetWidths = srcset 198 | .replace(/[\n\s]+/g, " ") 199 | .split(", ") 200 | .map((srcset) => srcset.split(" ")[1]) 201 | .map((width) => width.slice(0, -1)) 202 | .map(Number.parseFloat); 203 | 204 | expect(srcsetWidths).toEqual([ 205 | 100, 140, 196, 274, 384, 538, 753, 1054, 1476, 2066, 2893, 4050, 5669, 206 | 7937, 8192, 207 | ]); 208 | }); 209 | 210 | it('allows the default "minWidth" and "maxWidth" options to be overriden', () => { 211 | const sut = shallow( 212 | 219 | ); 220 | const srcset = sut.props().srcSet; 221 | 222 | const srcsetWidths = srcset 223 | .replace(/[\n\s]+/g, " ") 224 | .split(", ") 225 | .map((srcset) => srcset.split(" ")[1]) 226 | .map((width) => width.slice(0, -1)) 227 | .map(Number.parseFloat); 228 | 229 | expect(srcsetWidths).toEqual([ 230 | 200, 232, 269, 312, 362, 420, 487, 565, 656, 761, 882, 1000, 231 | ]); 232 | }); 233 | }); 234 | }); 235 | 236 | describe("using the htmlAttributes prop", () => { 237 | it("passes any attributes via htmlAttributes to the rendered element", () => { 238 | const htmlAttributes = { 239 | "data-src": "https://mysource.imgix.net/demo.png", 240 | width: "200", 241 | height: "100", 242 | loading: "lazy" 243 | }; 244 | sut = shallow( 245 | 250 | ); 251 | expect(sut.props()["data-src"]).toEqual(htmlAttributes["data-src"]); 252 | expect(sut.props()["width"]).toEqual(htmlAttributes["width"]); 253 | expect(sut.props()["height"]).toEqual(htmlAttributes["height"]); 254 | expect(sut.props()["loading"]).toEqual(htmlAttributes["loading"]); 255 | }); 256 | it("prepends 'auto, ' to the sizes prop if loading is lazy and not fixed size", () => { 257 | const htmlAttributes = { 258 | "data-src": "https://mysource.imgix.net/demo.png", 259 | width: "200", 260 | loading: "lazy", 261 | }; 262 | sut = shallow( 263 | 268 | ); 269 | expect(sut.props()["sizes"]).toEqual("auto, 100vw"); 270 | }); 271 | 272 | it("does not prepend 'auto, ' to the sizes prop if loading is not lazy", () => { 273 | const htmlAttributes = { 274 | "data-src": "https://mysource.imgix.net/demo.png", 275 | width: "200", 276 | height: "100", 277 | loading: "eager", // Not lazy loading 278 | }; 279 | sut = shallow( 280 | 285 | ); 286 | expect(sut.props()["sizes"]).toEqual("100vw"); 287 | }); 288 | 289 | it("does not prepend 'auto, ' to the sizes prop if loading is omitted", () => { 290 | const htmlAttributes = { 291 | "data-src": "https://mysource.imgix.net/demo.png", 292 | width: "200", 293 | height: "100", 294 | }; 295 | sut = shallow( 296 | 301 | ); 302 | expect(sut.props()["sizes"]).toEqual("100vw"); 303 | }); 304 | 305 | it("does not prepend 'auto, ' to the sizes prop if both width and height are present (htmlAttributes)", () => { 306 | const htmlAttributes = { 307 | "data-src": "https://mysource.imgix.net/demo.png", 308 | width: "200", 309 | height: "100", 310 | loading: "lazy", 311 | }; 312 | sut = shallow( 313 | 318 | ); 319 | expect(sut.props()["sizes"]).toEqual("100vw"); 320 | }); 321 | 322 | it("does not prepend 'auto, ' to the sizes prop if both width and height are present (element attributes)", () => { 323 | const htmlAttributes = { 324 | "data-src": "https://mysource.imgix.net/demo.png", 325 | loading: "lazy", 326 | }; 327 | sut = shallow( 328 | 335 | ); 336 | expect(sut.props()["sizes"]).toEqual("100vw"); 337 | }); 338 | }); 339 | }); 340 | 341 | describe("When in image mode", () => { 342 | it("a callback passed through the onMounted prop should be called", () => { 343 | const onMountedSpy = sinon.spy(); 344 | const sut = mount( 345 | 350 | ); 351 | 352 | expect(onMountedSpy.callCount).toEqual(1); 353 | const onMountArg = onMountedSpy.lastCall.args[0]; 354 | expect(onMountArg).toBeInstanceOf(HTMLImageElement); 355 | }); 356 | }); 357 | 358 | describe("When in mode", () => { 359 | const sizes = 360 | "(max-width: 30em) 100vw, (max-width: 50em) 50vw, calc(33vw - 100px)"; 361 | const htmlAttributes = { 362 | media: "(min-width: 1200px)", 363 | type: "image/webp", 364 | }; 365 | const shouldBehaveLikeSource = function (renderImage) { 366 | it("a component should be rendered", () => { 367 | expect(renderImage().type()).toBe("source"); 368 | }); 369 | 370 | it("srcSet prop should exist", () => { 371 | expect(renderImage().props().srcSet).not.toBeUndefined(); 372 | }); 373 | 374 | it("props.sizes should be defined and equal to the image's props", () => 375 | expect(renderImage().props().sizes).toEqual(sizes)); 376 | 377 | Object.keys(htmlAttributes) 378 | .filter((k) => k !== "alt") 379 | .forEach((k) => { 380 | it(`props.${k} should be defined and equal to the image's props`, () => { 381 | expect(renderImage().props()[k]).toBe(htmlAttributes[k]); 382 | }); 383 | }); 384 | it("an ixlib param should be added to the src", () => { 385 | renderImage() 386 | .props() 387 | .srcSet.split(",") 388 | .forEach((srcSet) => expectUrlToContainIxLibParam(srcSet)); 389 | }); 390 | }; 391 | 392 | describe("by default", () => { 393 | const renderImage = () => { 394 | return shallowSource( 395 | 396 | ); 397 | }; 398 | 399 | shouldBehaveLikeSource(renderImage); 400 | it("props.srcSet should be set to a valid src", () => { 401 | expect(renderImage().props().srcSet).toContain(src); 402 | }); 403 | 404 | it("should have a srcSet set correctly", async () => { 405 | const srcset = renderImage().props().srcSet; 406 | expect(srcset).not.toBeUndefined(); 407 | expect(srcset.split(",\n")[0].split(" ")).toHaveLength(2); 408 | const aSrcFromSrcSet = srcset.split(",\n")[0].split(" ")[0]; 409 | expect(aSrcFromSrcSet).toContain(src); 410 | const aWidthFromSrcSet = srcset.split(",\n")[0].split(" ")[1]; 411 | expect(aWidthFromSrcSet).toMatch(/^\d+w$/); 412 | }); 413 | 414 | it("returns the expected number of `url widthDescriptor` pairs", function () { 415 | const srcset = renderImage().props().srcSet; 416 | 417 | expect(srcset.split(",").length).toEqual(targetWidths.length); 418 | }); 419 | 420 | it("should not exceed the bounds of [100, 8192]", () => { 421 | const srcset = renderImage().props().srcSet; 422 | 423 | const srcsetWidths = srcset 424 | .replace(/[\n\s]+/g, " ") 425 | .split(", ") 426 | .map((srcset) => srcset.split(" ")[1]) 427 | .map((width) => width.slice(0, -1)) 428 | .map(Number.parseFloat); 429 | 430 | const min = Math.min(...srcsetWidths); 431 | const max = Math.max(...srcsetWidths); 432 | 433 | expect(min).not.toBeLessThan(100); 434 | expect(max).not.toBeGreaterThan(8192); 435 | }); 436 | }); 437 | 438 | describe("in fixed width mode", () => { 439 | const renderImage = () => { 440 | return shallowSource( 441 | 448 | ); 449 | }; 450 | 451 | it("srcSet should be in the form src 1x, src 2x, src 3x, src 4x, src 5x", () => { 452 | const srcSet = renderImage().props().srcSet; 453 | 454 | const srcSets = srcSet.split(",\n"); 455 | expect(srcSets).toHaveLength(5); 456 | srcSets.forEach((srcSet) => { 457 | expect(srcSet).toContain(src); 458 | }); 459 | expect(srcSets[0].split(" ")[1]).toBe("1x"); 460 | expect(srcSets[1].split(" ")[1]).toBe("2x"); 461 | expect(srcSets[2].split(" ")[1]).toBe("3x"); 462 | expect(srcSets[3].split(" ")[1]).toBe("4x"); 463 | expect(srcSets[4].split(" ")[1]).toBe("5x"); 464 | }); 465 | 466 | it("width and height should be passed through to the img element", () => { 467 | const { width, height } = renderImage().props(); 468 | expect(width).toBe(100); 469 | expect(height).toBe(100); 470 | }); 471 | }); 472 | 473 | describe("with disableSrcSet prop", () => { 474 | const renderImage = () => 475 | shallowSource( 476 | 482 | ); 483 | 484 | shouldBehaveLikeSource(renderImage); 485 | it("props.srcSet should include the specified src passed as props", () => { 486 | expect(renderImage().props().srcSet).toMatch(new RegExp(`^${src}`)); 487 | }); 488 | }); 489 | it("a callback passed through the onMounted prop should be called", () => { 490 | const onMountedSpy = sinon.spy(); 491 | const sut = mount( 492 | 497 | ); 498 | 499 | expect(onMountedSpy.callCount).toEqual(1); 500 | const onMountArg = onMountedSpy.lastCall.args[0]; 501 | expect(onMountArg).toBeInstanceOf(HTMLSourceElement); 502 | }); 503 | 504 | describe("using the htmlAttributes prop", () => { 505 | it("assigns an alt attribute given htmlAttributes.alt", async () => { 506 | const htmlAttributes = { 507 | alt: "Example alt attribute", 508 | }; 509 | 510 | sut = mount( 511 | 516 | ); 517 | 518 | expect(sut.props()["htmlAttributes"].alt).toEqual(htmlAttributes.alt); 519 | }); 520 | 521 | it("passes any attributes via htmlAttributes to the rendered element", () => { 522 | const htmlAttributes = { 523 | "data-src": "https://mysource.imgix.net/demo.png", 524 | width: "200", 525 | height: "100", 526 | }; 527 | sut = mount( 528 | 533 | ); 534 | 535 | expect(sut.props()["htmlAttributes"]["data-src"]).toEqual( 536 | htmlAttributes["data-src"] 537 | ); 538 | expect(sut.props()["htmlAttributes"]["width"]).toEqual( 539 | htmlAttributes["width"] 540 | ); 541 | expect(sut.props()["htmlAttributes"]["height"]).toEqual( 542 | htmlAttributes["height"] 543 | ); 544 | }); 545 | 546 | it("attaches a ref via htmlAttributes", () => { 547 | const callback = sinon.spy(); 548 | 549 | sut = mount( 550 | 555 | ); 556 | 557 | expect(callback.callCount).toEqual(1); 558 | }); 559 | 560 | it("stills calls onMounted if a ref is passed via htmlAttributes", () => { 561 | const htmlAttrCallback = sinon.spy(); 562 | const onMountedCallback = sinon.spy(); 563 | 564 | sut = mount( 565 | 571 | ); 572 | 573 | expect(htmlAttrCallback.callCount).toEqual(1); 574 | expect(onMountedCallback.callCount).toEqual(1); 575 | }); 576 | }); 577 | }); 578 | 579 | describe("When in picture mode", () => { 580 | let children, lastChild; 581 | const parentAlt = "parent alt"; 582 | const childAlt = "child alt"; 583 | 584 | const shouldBehaveLikePicture = function () { 585 | it("every child should have a key", () => { 586 | expect(children.everyWhere((c) => c.key() !== undefined)).toBe(true); 587 | }); 588 | 589 | it("a picture should be rendered", () => { 590 | expect(sut.type()).toBe("picture"); 591 | }); 592 | 593 | it("alt tag should not exist", () => { 594 | expect(sut.props().alt).toBe(undefined); 595 | }); 596 | 597 | it("an or a should be the last child", () => { 598 | // If the number of HOCs for ReactImgix is changed, there may need to be a change in the number of .first().shallow() calls 599 | // hack from https://github.com/airbnb/enzyme/issues/539#issuecomment-239497107 until a better solution is implemented 600 | const lastChildElement = lastChild 601 | .first() 602 | .shallow() 603 | .first() 604 | .shallow() 605 | .first() 606 | .shallow(); 607 | if (lastChildElement.type().hasOwnProperty("name")) { 608 | expect(lastChildElement.name()).toBe(__ReactImgixImpl.displayName); 609 | expect( 610 | lastChildElement.shallow({ disableLifecycleMethods: true }).type() 611 | ).toBe("img"); 612 | } else { 613 | expect(lastChildElement.type()).toBe("img"); 614 | } 615 | }); 616 | }; 617 | 618 | it("should throw an error when no children passed", () => { 619 | PublicConfigAPI.enableWarning("fallbackImage"); 620 | jest.spyOn(global.console, "warn").mockImplementation((msg) => { 621 | // Enable when debugging 622 | }); 623 | 624 | shallowPicture(); 625 | 626 | expect(console.warn).toHaveBeenCalledWith( 627 | expect.stringContaining("No fallback or found") 628 | ); 629 | PublicConfigAPI.disableWarning("fallbackImage"); 630 | }); 631 | 632 | describe("with a passed as a child", () => { 633 | beforeEach(() => { 634 | sut = shallowPicture( 635 | 641 | 642 | 643 | ); 644 | children = sut.children(); 645 | lastChild = children.last(); 646 | }); 647 | 648 | shouldBehaveLikePicture(); 649 | it("only one child should exist", () => { 650 | expect(children).toHaveLength(1); 651 | }); 652 | }); 653 | 654 | describe("with an passed as a child", () => { 655 | beforeEach(() => { 656 | sut = shallowPicture( 657 | 662 | {childAlt} 663 | 664 | ); 665 | children = sut.children(); 666 | lastChild = children.last(); 667 | }); 668 | 669 | shouldBehaveLikePicture(); 670 | it("only one child should exist", () => { 671 | expect(children).toHaveLength(1); 672 | }); 673 | it("props should not be passed down to children", () => { 674 | const lastChildProps = lastChild.props(); 675 | expect(lastChildProps).toMatchObject({ 676 | alt: childAlt, 677 | }); 678 | }); 679 | }); 680 | 681 | it("a callback passed through the onMounted prop should be called", () => { 682 | const onMountedSpy = sinon.spy(); 683 | sut = mount( 684 | 685 | 686 | 687 | ); 688 | 689 | expect(onMountedSpy.callCount).toEqual(1); 690 | const onMountArg = onMountedSpy.lastCall.args[0]; 691 | expect(onMountArg).toBeInstanceOf(HTMLPictureElement); 692 | }); 693 | 694 | it("should not pass _inPicture to a element", () => { 695 | const sut = mount( 696 | 697 | 698 | 699 | ); 700 | 701 | expect(sut.find("img").props()._inPicture).toBe(undefined); 702 | }); 703 | }); 704 | 705 | describe("When in background mode", () => { 706 | it("should be loading when there is a width but no height", () => { 707 | const Background = makeBackgroundWithBounds({ top: 10, width: 100 }); 708 | 709 | sut = mount( 710 | 715 |
Content
716 |
717 | ); 718 | 719 | expect(sut.find("div.bg-img").hasClass("react-imgix-bg-loading")).toBe( 720 | true 721 | ); 722 | }); 723 | 724 | it("should not be loading when a width and height are available", () => { 725 | const Background = makeBackgroundWithBounds({ 726 | top: 10, 727 | width: 100, 728 | height: 100, 729 | }); 730 | 731 | sut = mount( 732 | 733 |
Content
734 |
735 | ); 736 | 737 | expect(sut.find("div.bg-img").hasClass("react-imgix-bg-loading")).toBe( 738 | false 739 | ); 740 | }); 741 | }); 742 | 743 | describe("When using the component", () => { 744 | let className = "img--enabled"; 745 | beforeEach(() => { 746 | sut = shallow( 747 | 753 | ); 754 | }); 755 | it("the auto param should alter the url correctly", () => { 756 | expectSrcsToContain(sut, "auto=format%2Cenhance"); 757 | }); 758 | it("the rendered element should contain the class name provided", () => { 759 | expect(sut.props().className).toContain(className); 760 | }); 761 | it("the fit param should alter the fit query pararmeter correctly", () => { 762 | expectSrcsTo(sut, expect.not.stringContaining("fit=crop")); 763 | }); 764 | it("the keys of custom url parameters should be url encoded", () => { 765 | const helloWorldKey = "hello world"; 766 | const expectedKey = "hello%20world"; 767 | sut = shallow( 768 | 775 | ); 776 | 777 | expectSrcsToContain(sut, `${expectedKey}=interesting`); 778 | expectSrcsTo( 779 | sut, 780 | expect.not.stringContaining(`${helloWorldKey}=interesting`) 781 | ); 782 | }); 783 | it("the values of custom url parameters should be url encoded", () => { 784 | const helloWorldValue = '/foo"> <'; 785 | const expectedValue = 786 | "%2Ffoo%22%3E%20%3Cscript%3Ealert(%22hacked%22)%3C%2Fscript%3E%3C"; 787 | sut = shallow( 788 | 795 | ); 796 | 797 | expectSrcsToContain(sut, `hello_world=${expectedValue}`); 798 | expectSrcsTo( 799 | sut, 800 | expect.not.stringContaining(`hello_world=${helloWorldValue}`) 801 | ); 802 | }); 803 | it("the base64 custom parameter values should be base64 encoded", () => { 804 | const txt64Value = "I cannøt belîév∑ it wors! 😱"; 805 | const expectedValue = "SSBjYW5uw7h0IGJlbMOuw6l24oiRIGl0IHdvcu-jv3MhIPCfmLE"; 806 | sut = shallow( 807 | 814 | ); 815 | 816 | expectSrcsToContain(sut, `txt64=${expectedValue}`); 817 | expectSrcsTo(sut, expect.not.stringContaining(`txt64=${txt64Value}`)); 818 | }); 819 | it("a custom height should alter the height query parameter correctly", () => { 820 | const height = 300; 821 | sut = shallow( 822 | 823 | ); 824 | 825 | expectSrcsToContain(sut, `h=${height}`); 826 | }); 827 | 828 | it("a height prop between 0 and 1 should not be passed as a prop to the child element rendered", () => { 829 | const height = 0.5; 830 | sut = shallow( 831 | 832 | ); 833 | 834 | expect(sut.props().height).toBeFalsy(); 835 | }); 836 | 837 | it("a height prop greater than 1 should be passed to the the child element rendered", () => { 838 | const height = 300; 839 | sut = shallow( 840 | 841 | ); 842 | 843 | expect(sut.props().height).toEqual(height); 844 | }); 845 | 846 | it("the width prop should alter the width query parameter correctly", () => { 847 | const width = 300; 848 | sut = shallow( 849 | 850 | ); 851 | 852 | expectSrcsToContain(sut, `w=${width}`); 853 | }); 854 | 855 | it("responsive width should overwrite the width query parameter correctly", () => { 856 | sut = shallow( 857 | 858 | ); 859 | 860 | expect(sut.props().src).toContain("w=333"); 861 | expect(sut.props().srcSet).not.toContain("w=333"); 862 | }); 863 | 864 | it("a width prop between 0 and 1 should not be passed as a prop to the child element rendered", () => { 865 | const width = 0.5; 866 | sut = shallow( 867 | 868 | ); 869 | 870 | expect(sut.props().width).toBeFalsy(); 871 | }); 872 | 873 | it("a width prop greater than 1 should be passed to the the child element rendered", () => { 874 | const width = 300; 875 | sut = shallow( 876 | 877 | ); 878 | 879 | expect(sut.props().width).toEqual(width); 880 | }); 881 | 882 | describe("aspectRatio", () => { 883 | describe("valid AR", () => { 884 | const testValidAR = ({ ar }) => { 885 | it(`a valid ar prop (${ar}) should generate an ar query parameter`, () => { 886 | const parseParam = (url, param) => { 887 | const matched = url.match("[?&]" + param + "=([^&]+)"); 888 | if (!matched) return undefined; 889 | return matched[1]; 890 | }; 891 | const removeFallbackSrcSet = (srcSets) => srcSets.slice(0, -1); 892 | 893 | sut = shallow( 894 | 899 | ); 900 | 901 | const srcSet = sut.props().srcSet; 902 | const srcSets = srcSet.split(",").map((v) => v.trim()); 903 | const srcSetUrls = srcSets.map((srcSet) => srcSet.split(" ")[0]); 904 | removeFallbackSrcSet(srcSetUrls).forEach((srcSetUrl) => { 905 | const ar = parseParam(srcSetUrl, "ar"); 906 | expect(ar).toBeTruthy(); 907 | }); 908 | }); 909 | }; 910 | [ 911 | ["1:1"], 912 | ["1.1:1"], 913 | ["1.12:1"], 914 | ["1.123:1"], 915 | ["1:1.1"], 916 | ["1:1.12"], 917 | ["1.1:1.1"], 918 | ["1.123:1.123"], 919 | ["11.123:11.123"], 920 | ].forEach(([validAR, validArDecimal]) => 921 | testValidAR({ 922 | ar: validAR, 923 | }) 924 | ); 925 | }); 926 | 927 | describe("invalid AR", () => { 928 | beforeAll(() => { 929 | PublicConfigAPI.disableWarning("invalidARFormat"); 930 | }); 931 | afterAll(() => { 932 | PublicConfigAPI.enableWarning("invalidARFormat"); 933 | }); 934 | const testInvalidAR = (ar) => { 935 | it(`an invalid ar prop (${ar}) will still generate an ar query parameter`, () => { 936 | const parseParam = (url, param) => { 937 | const matched = url.match("[?&]" + param + "=([^&]+)"); 938 | if (!matched) return undefined; 939 | return matched[1]; 940 | }; 941 | const removeFallbackSrcSet = (srcSets) => srcSets.slice(0, -1); 942 | 943 | sut = shallow( 944 | 949 | ); 950 | 951 | const srcSet = sut.props().srcSet; 952 | const srcSets = srcSet.split(",").map((v) => v.trim()); 953 | const srcSetUrls = srcSets.map((srcSet) => srcSet.split(" ")[0]); 954 | removeFallbackSrcSet(srcSetUrls).forEach((srcSetUrl) => { 955 | const w = parseParam(srcSetUrl, "w"); 956 | const ar = parseParam(srcSetUrl, "ar"); 957 | 958 | expect(w).toBeTruthy(); 959 | expect(ar).toBeTruthy(); 960 | }); 961 | }); 962 | }; 963 | 964 | [ 965 | "4x3", 966 | "4:", 967 | , 968 | "blah:1:1", 969 | "blah1:1", 970 | "1x1", 971 | "1:1blah", 972 | "1:blah1", 973 | 0.145, 974 | true, 975 | ].forEach((invalidAR) => testInvalidAR(invalidAR)); 976 | }); 977 | 978 | it("srcsets should not have an ar parameter when aspectRatio is not set", () => { 979 | sut = shallow( 980 | 984 | ); 985 | const srcSet = sut.props().srcSet; 986 | const srcSets = srcSet.split(",").map((v) => v.trim()); 987 | const srcSetUrls = srcSets.map((srcSet) => srcSet.split(" ")[0]); 988 | const parseParam = (str, param) => { 989 | const matched = str.match("[?&]" + param + "=([^&]+)"); 990 | if (!matched) return null; 991 | return matched[1]; 992 | }; 993 | srcSetUrls.forEach((srcSetUrl) => { 994 | const ar = parseParam(srcSetUrl, "ar"); 995 | expect(ar).toBeFalsy(); 996 | }); 997 | }); 998 | 999 | it("the generated src should have an ar parameter included", () => { 1000 | sut = shallow( 1001 | 1006 | ); 1007 | 1008 | expectSrcsTo(sut, expect.stringContaining("ar=")); 1009 | }); 1010 | }); 1011 | 1012 | describe("using the htmlAttributes prop", () => { 1013 | it("assigns an alt attribute given htmlAttributes.alt", async () => { 1014 | const htmlAttributes = { 1015 | alt: "Example alt attribute", 1016 | }; 1017 | sut = shallow( 1018 | 1023 | ); 1024 | expect(sut.props().alt).toEqual(htmlAttributes.alt); 1025 | }); 1026 | 1027 | it("passes any attributes via htmlAttributes to the rendered element", () => { 1028 | const htmlAttributes = { 1029 | "data-src": "https://mysource.imgix.net/demo.png", 1030 | }; 1031 | sut = shallow( 1032 | 1037 | ); 1038 | 1039 | expect(sut.props()["data-src"]).toEqual(htmlAttributes["data-src"]); 1040 | }); 1041 | 1042 | it("attaches a ref via htmlAttributes", () => { 1043 | const callback = sinon.spy(); 1044 | 1045 | sut = mount( 1046 | 1051 | ); 1052 | 1053 | expect(callback.callCount).toEqual(1); 1054 | }); 1055 | 1056 | it("stills calls onMounted if a ref is passed via htmlAttributes", () => { 1057 | const htmlAttrCallback = sinon.spy(); 1058 | const onMountedCallback = sinon.spy(); 1059 | 1060 | sut = mount( 1061 | 1067 | ); 1068 | 1069 | expect(htmlAttrCallback.callCount).toEqual(1); 1070 | expect(onMountedCallback.callCount).toEqual(1); 1071 | }); 1072 | }); 1073 | 1074 | it("an ixlib parameter should be included by default in the computed src and srcSet", () => { 1075 | sut = shallow( 1076 | 1077 | ); 1078 | expectSrcsTo(sut, createIxLibParamMatcher()); 1079 | }); 1080 | it("the addition of the ixlib parameter to the url can be disabled", () => { 1081 | sut = shallow( 1082 | 1087 | ); 1088 | 1089 | expectSrcsTo(sut, expect.not.stringContaining(`ixlib=`)); 1090 | }); 1091 | }); 1092 | 1093 | describe("Attribute config", () => { 1094 | describe("", () => { 1095 | const ATTRIBUTES = ["src", "srcSet", "sizes"]; 1096 | ATTRIBUTES.forEach((ATTRIBUTE) => { 1097 | it(`${ATTRIBUTE} can be configured to use data-${ATTRIBUTE}`, () => { 1098 | sut = shallow( 1099 | 1106 | ); 1107 | 1108 | expect(sut.props()[`data-${ATTRIBUTE}`]).not.toBeUndefined(); 1109 | expect(sut.props()[ATTRIBUTE]).toBeUndefined(); 1110 | }); 1111 | }); 1112 | }); 1113 | describe("", () => { 1114 | const ATTRIBUTES = ["srcSet", "sizes"]; 1115 | ATTRIBUTES.forEach((ATTRIBUTE) => { 1116 | it(`${ATTRIBUTE} can be configured to use data-${ATTRIBUTE}`, () => { 1117 | sut = shallowSource( 1118 | 1125 | ); 1126 | 1127 | expect(sut.props()[`data-${ATTRIBUTE}`]).not.toBeUndefined(); 1128 | expect(sut.props()[ATTRIBUTE]).toBeUndefined(); 1129 | }); 1130 | }); 1131 | }); 1132 | }); 1133 | 1134 | const expectSrcsTo = (sut, matcher) => { 1135 | const src = sut.props().src; 1136 | expect(src).toEqual(matcher); // Use jest matchers as param, e.g. jest.stringContaining() 1137 | 1138 | const srcSet = sut.props().srcSet; 1139 | if (!srcSet) { 1140 | fail("No srcSet"); 1141 | } 1142 | const srcSets = srcSet.split(",").map((v) => v.trim()); 1143 | const srcSetUrls = srcSets.map((srcSet) => srcSet.split(" ")[0]); 1144 | srcSetUrls.forEach((srcSetUrl) => { 1145 | expect(srcSetUrl).toEqual(matcher); 1146 | }); 1147 | }; 1148 | 1149 | const expectSrcsToContain = (sut, shouldContainString) => 1150 | expectSrcsTo(sut, expect.stringContaining(shouldContainString)); 1151 | 1152 | const expectUrlToContainIxLibParam = (url) => { 1153 | expect(url).toEqual(createIxLibParamMatcher()); 1154 | }; 1155 | 1156 | const createIxLibParamMatcher = () => { 1157 | const expectedVersion = require("read-pkg-up").sync().pkg.version; 1158 | const expectedParam = `ixlib=react-${expectedVersion}`; 1159 | 1160 | return expect.stringContaining(expectedParam); 1161 | }; 1162 | -------------------------------------------------------------------------------- /test/unit/should-component-update.test.js: -------------------------------------------------------------------------------- 1 | import { __shouldComponentUpdate } from "../../src/react-imgix-bg"; 2 | 3 | test("shouldComponentUpdate should return true when children change", () => { 4 | const contentRect = { bounds: { width: 100, height: 100 } }; 5 | const props = { children: 0, contentRect }; 6 | const nextProps = { children: 1, contentRect }; 7 | 8 | expect(__shouldComponentUpdate(props, nextProps)).toBe(true); 9 | }); 10 | 11 | test("shouldComponentUpdate should return false when imgix-params don't change", () => { 12 | const contentRect = { bounds: { width: 100, height: 100 } }; 13 | const props = { contentRect, imgixParams: { ar: "1:2" } }; 14 | const nextProps = { contentRect, imgixParams: { ar: "1:2" } }; 15 | 16 | expect(__shouldComponentUpdate(props, nextProps)).toBe(false); 17 | }); 18 | --------------------------------------------------------------------------------