├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── commit-msg ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ ├── fragment.test.js.snap │ ├── misc.test.js.snap │ └── misc.test.tsx.snap ├── api │ ├── __snapshots__ │ │ ├── base.test.tsx.snap │ │ ├── client.test.tsx.snap │ │ ├── link.test.tsx.snap │ │ ├── meta.test.tsx.snap │ │ ├── noscript.test.tsx.snap │ │ ├── script.test.tsx.snap │ │ ├── style.test.tsx.snap │ │ └── title.test.tsx.snap │ ├── base.test.tsx │ ├── bodyAttributes.test.tsx │ ├── client.test.tsx │ ├── htmlAttributes.test.tsx │ ├── link.test.tsx │ ├── meta.test.tsx │ ├── noscript.test.tsx │ ├── script.test.tsx │ ├── style.test.tsx │ ├── title.test.tsx │ └── titleAttributes.test.tsx ├── deferred.test.tsx ├── fragment.test.tsx ├── misc.test.tsx ├── server │ ├── __snapshots__ │ │ ├── base.test.tsx.snap │ │ ├── bodyAttributes.test.tsx.snap │ │ ├── helmetData.test.tsx.snap │ │ ├── htmlAttributes.test.tsx.snap │ │ ├── link.test.tsx.snap │ │ ├── meta.test.tsx.snap │ │ ├── noscript.test.tsx.snap │ │ ├── script.test.tsx.snap │ │ ├── server.test.tsx.snap │ │ ├── style.test.tsx.snap │ │ └── title.test.tsx.snap │ ├── base.test.tsx │ ├── bodyAttributes.test.tsx │ ├── helmetData.test.tsx │ ├── htmlAttributes.test.tsx │ ├── link.test.tsx │ ├── meta.test.tsx │ ├── noscript.test.tsx │ ├── script.test.tsx │ ├── server.test.tsx │ ├── style.test.tsx │ └── title.test.tsx ├── setup-test-env.ts ├── utils.tsx └── window.ts ├── build.ts ├── commitlint.config.js ├── package.json ├── src ├── Dispatcher.tsx ├── HelmetData.ts ├── Provider.tsx ├── client.ts ├── constants.ts ├── index.tsx ├── server.ts ├── types.ts └── utils.ts ├── tsconfig.json ├── vitest.config.ts └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:18.17.1 6 | 7 | steps: 8 | - checkout 9 | 10 | - restore_cache: 11 | keys: 12 | - deps-{{ checksum "package.json" }} 13 | 14 | - run: yarn 15 | 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: deps-{{ checksum "package.json" }} 20 | 21 | - run: 22 | name: Tests 23 | command: yarn test 24 | 25 | - run: 26 | name: Type checks 27 | command: yarn tsc 28 | 29 | - run: 30 | name: ESLint 31 | command: yarn lint 32 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /node_modules 3 | package.json 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').Linter.BaseConfig} 3 | */ 4 | module.exports = { 5 | extends: [ 6 | '@remix-run/eslint-config', 7 | '@remix-run/eslint-config/node', 8 | '@remix-run/eslint-config/jest-testing-library', 9 | 'prettier', 10 | ], 11 | plugins: ['prettier'], 12 | rules: { 13 | 'import/order': [ 14 | 'error', 15 | { 16 | 'newlines-between': 'always', 17 | }, 18 | ], 19 | 'prettier/prettier': [ 20 | 'error', 21 | { 22 | singleQuote: true, 23 | trailingComma: 'es5', 24 | useTabs: false, 25 | tabWidth: 2, 26 | printWidth: 100, 27 | }, 28 | ], 29 | 'testing-library/render-result-naming-convention': 'off', 30 | }, 31 | settings: { 32 | jest: { 33 | version: 27, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | jest.setup.js 3 | *.config.js 4 | node_modules 5 | src 6 | __tests__ 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /es 3 | /coverage 4 | /node_modules 5 | package.json 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "avoid", 5 | "printWidth": 100, 6 | "useTabs": false, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 The New York Times Company 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-helmet-async 2 | 3 | [![CircleCI](https://circleci.com/gh/staylor/react-helmet-async.svg?style=svg)](https://circleci.com/gh/staylor/react-helmet-async) 4 | 5 | [Announcement post on Times Open blog](https://open.nytimes.com/the-future-of-meta-tag-management-for-modern-react-development-ec26a7dc9183) 6 | 7 | This package is a fork of [React Helmet](https://github.com/nfl/react-helmet). 8 | `` usage is synonymous, but server and client now requires `` to encapsulate state per request. 9 | 10 | `react-helmet` relies on `react-side-effect`, which is not thread-safe. If you are doing anything asynchronous on the server, you need Helmet to encapsulate data on a per-request basis, this package does just that. 11 | 12 | ## Usage 13 | 14 | **New is 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'` 15 | 16 | The main way that this package differs from `react-helmet` is that it requires using a Provider to encapsulate Helmet state for your React tree. If you use libraries like Redux or Apollo, you are already familiar with this paradigm: 17 | 18 | ```javascript 19 | import React from 'react'; 20 | import ReactDOM from 'react-dom'; 21 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 22 | 23 | const app = ( 24 | 25 | 26 | 27 | Hello World 28 | 29 | 30 |

Hello World

31 |
32 |
33 | ); 34 | 35 | ReactDOM.hydrate( 36 | app, 37 | document.getElementById(‘app’) 38 | ); 39 | ``` 40 | 41 | On the server, we will no longer use static methods to extract state. `react-side-effect` 42 | exposed a `.rewind()` method, which Helmet used when calling `Helmet.renderStatic()`. Instead, we are going 43 | to pass a `context` prop to `HelmetProvider`, which will hold our state specific to each request. 44 | 45 | ```javascript 46 | import React from 'react'; 47 | import { renderToString } from 'react-dom/server'; 48 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 49 | 50 | const helmetContext = {}; 51 | 52 | const app = ( 53 | 54 | 55 | 56 | Hello World 57 | 58 | 59 |

Hello World

60 |
61 |
62 | ); 63 | 64 | const html = renderToString(app); 65 | 66 | const { helmet } = helmetContext; 67 | 68 | // helmet.title.toString() etc… 69 | ``` 70 | 71 | ## Streams 72 | 73 | This package only works with streaming if your `` data is output outside of `renderToNodeStream()`. 74 | This is possible if your data hydration method already parses your React tree. Example: 75 | 76 | ```javascript 77 | import through from 'through'; 78 | import { renderToNodeStream } from 'react-dom/server'; 79 | import { getDataFromTree } from 'react-apollo'; 80 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 81 | import template from 'server/template'; 82 | 83 | const helmetContext = {}; 84 | 85 | const app = ( 86 | 87 | 88 | 89 | Hello World 90 | 91 | 92 |

Hello World

93 |
94 |
95 | ); 96 | 97 | await getDataFromTree(app); 98 | 99 | const [header, footer] = template({ 100 | helmet: helmetContext.helmet, 101 | }); 102 | 103 | res.status(200); 104 | res.write(header); 105 | renderToNodeStream(app) 106 | .pipe( 107 | through( 108 | function write(data) { 109 | this.queue(data); 110 | }, 111 | function end() { 112 | this.queue(footer); 113 | this.queue(null); 114 | } 115 | ) 116 | ) 117 | .pipe(res); 118 | ``` 119 | 120 | ## Usage in Jest 121 | While testing in using jest, if there is a need to emulate SSR, the following string is required to have the test behave the way they are expected to. 122 | 123 | ```javascript 124 | import { HelmetProvider } from 'react-helmet-async'; 125 | 126 | HelmetProvider.canUseDOM = false; 127 | ``` 128 | 129 | ## Prioritizing tags for SEO 130 | 131 | It is understood that in some cases for SEO, certain tags should appear earlier in the HEAD. Using the `prioritizeSeoTags` flag on any `` component allows the server render of react-helmet-async to expose a method for prioritizing relevant SEO tags. 132 | 133 | In the component: 134 | ```javascript 135 | 136 | A fancy webpage 137 | 138 | 139 | 140 | 141 | 142 | ``` 143 | 144 | In your server template: 145 | 146 | ```javascript 147 | 148 | 149 | ${helmet.title.toString()} 150 | ${helmet.priority.toString()} 151 | ${helmet.meta.toString()} 152 | ${helmet.link.toString()} 153 | ${helmet.script.toString()} 154 | 155 | ... 156 | 157 | ``` 158 | 159 | Will result in: 160 | 161 | ```html 162 | 163 | 164 | A fancy webpage 165 | 166 | 167 | 168 | 169 | 170 | ... 171 | 172 | ``` 173 | 174 | A list of prioritized tags and attributes can be found in [constants.ts](./src/constants.ts). 175 | 176 | ## Usage without Context 177 | You can optionally use `` outside a context by manually creating a stateful `HelmetData` instance, and passing that stateful object to each `` instance: 178 | 179 | 180 | ```js 181 | import React from 'react'; 182 | import { renderToString } from 'react-dom/server'; 183 | import { Helmet, HelmetProvider, HelmetData } from 'react-helmet-async'; 184 | 185 | const helmetData = new HelmetData({}); 186 | 187 | const app = ( 188 | 189 | 190 | Hello World 191 | 192 | 193 |

Hello World

194 |
195 | ); 196 | 197 | const html = renderToString(app); 198 | 199 | const { helmet } = helmetData.context; 200 | ``` 201 | 202 | ## License 203 | 204 | Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor 205 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/fragment.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`fragments parses Fragments 1`] = `"Hello"`; 4 | 5 | exports[`fragments parses nested Fragments 1`] = `"Baz"`; 6 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/misc.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`misc API encodes special characters 1`] = `""`; 4 | 5 | exports[`misc API only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; 6 | 7 | exports[`misc API only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; 8 | 9 | exports[`misc API only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; 10 | 11 | exports[`misc API only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; 12 | 13 | exports[`misc API only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; 14 | 15 | exports[`misc API recognizes valid tags regardless of attribute ordering 1`] = `""`; 16 | 17 | exports[`misc Declarative API encodes special characters 1`] = `""`; 18 | 19 | exports[`misc Declarative API only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; 20 | 21 | exports[`misc Declarative API only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; 22 | 23 | exports[`misc Declarative API only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; 24 | 25 | exports[`misc Declarative API only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; 26 | 27 | exports[`misc Declarative API only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; 28 | 29 | exports[`misc Declarative API recognizes valid tags regardless of attribute ordering 1`] = `""`; 30 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/misc.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`misc > API > encodes special characters 1`] = `""`; 4 | 5 | exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; 6 | 7 | exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; 8 | 9 | exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; 10 | 11 | exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; 12 | 13 | exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; 14 | 15 | exports[`misc > API > recognizes valid tags regardless of attribute ordering 1`] = `""`; 16 | 17 | exports[`misc > Declarative API > encodes special characters 1`] = `""`; 18 | 19 | exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; 20 | 21 | exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; 22 | 23 | exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; 24 | 25 | exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; 26 | 27 | exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; 28 | 29 | exports[`misc > Declarative API > recognizes valid tags regardless of attribute ordering 1`] = `""`; 30 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/base.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`base tag > API > sets base tag based on deepest nested component 1`] = `""`; 4 | 5 | exports[`base tag > Declarative API > sets base tag based on deepest nested component 1`] = `""`; 6 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/client.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; 4 | 5 | exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; 6 | 7 | exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; 8 | 9 | exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; 10 | 11 | exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; 12 | 13 | exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; 14 | 15 | exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; 16 | 17 | exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; 18 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/link.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`link tags > API > allows duplicate link tags if specified in the same component 1`] = `""`; 4 | 5 | exports[`link tags > API > allows duplicate link tags if specified in the same component 2`] = `""`; 6 | 7 | exports[`link tags > API > does not render tag when primary attribute is null 1`] = `""`; 8 | 9 | exports[`link tags > API > overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; 10 | 11 | exports[`link tags > API > overrides single link tag with duplicate link tags in a nested component 1`] = `""`; 12 | 13 | exports[`link tags > API > overrides single link tag with duplicate link tags in a nested component 2`] = `""`; 14 | 15 | exports[`link tags > API > sets link tags based on deepest nested component 1`] = `""`; 16 | 17 | exports[`link tags > API > sets link tags based on deepest nested component 2`] = `""`; 18 | 19 | exports[`link tags > API > sets link tags based on deepest nested component 3`] = `""`; 20 | 21 | exports[`link tags > API > tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; 22 | 23 | exports[`link tags > API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; 24 | 25 | exports[`link tags > API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; 26 | 27 | exports[`link tags > Declarative API > allows duplicate link tags if specified in the same component 1`] = `""`; 28 | 29 | exports[`link tags > Declarative API > allows duplicate link tags if specified in the same component 2`] = `""`; 30 | 31 | exports[`link tags > Declarative API > does not render tag when primary attribute is null 1`] = `""`; 32 | 33 | exports[`link tags > Declarative API > overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; 34 | 35 | exports[`link tags > Declarative API > overrides single link tag with duplicate link tags in a nested component 1`] = `""`; 36 | 37 | exports[`link tags > Declarative API > overrides single link tag with duplicate link tags in a nested component 2`] = `""`; 38 | 39 | exports[`link tags > Declarative API > sets link tags based on deepest nested component 1`] = `""`; 40 | 41 | exports[`link tags > Declarative API > sets link tags based on deepest nested component 2`] = `""`; 42 | 43 | exports[`link tags > Declarative API > sets link tags based on deepest nested component 3`] = `""`; 44 | 45 | exports[`link tags > Declarative API > tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; 46 | 47 | exports[`link tags > Declarative API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; 48 | 49 | exports[`link tags > Declarative API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; 50 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/meta.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`meta tags > API > allows duplicate meta tags if specified in the same component 1`] = `""`; 4 | 5 | exports[`meta tags > API > allows duplicate meta tags if specified in the same component 2`] = `""`; 6 | 7 | exports[`meta tags > API > fails gracefully when meta is wrong shape 1`] = `"Helmet: meta should be of type \\"Array\\". Instead found type \\"object\\""`; 8 | 9 | exports[`meta tags > API > overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; 10 | 11 | exports[`meta tags > API > overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; 12 | 13 | exports[`meta tags > API > overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; 14 | 15 | exports[`meta tags > API > sets meta tags based on deepest nested component 1`] = `""`; 16 | 17 | exports[`meta tags > API > sets meta tags based on deepest nested component 2`] = `""`; 18 | 19 | exports[`meta tags > API > sets meta tags based on deepest nested component 3`] = `""`; 20 | 21 | exports[`meta tags > Declarative API > allows duplicate meta tags if specified in the same component 1`] = `""`; 22 | 23 | exports[`meta tags > Declarative API > allows duplicate meta tags if specified in the same component 2`] = `""`; 24 | 25 | exports[`meta tags > Declarative API > overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; 26 | 27 | exports[`meta tags > Declarative API > overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; 28 | 29 | exports[`meta tags > Declarative API > overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; 30 | 31 | exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 1`] = `""`; 32 | 33 | exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 2`] = `""`; 34 | 35 | exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 3`] = `""`; 36 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/noscript.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`noscript tags > API > updates noscript tags 1`] = `""`; 4 | 5 | exports[`noscript tags > Declarative API > updates noscript tags 1`] = `""`; 6 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/script.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`script tags > API > sets script tags based on deepest nested component 1`] = `""`; 4 | 5 | exports[`script tags > API > sets script tags based on deepest nested component 2`] = `""`; 6 | 7 | exports[`script tags > API > sets undefined attribute values to empty strings 1`] = `""`; 8 | 9 | exports[`script tags > Declarative API > sets script tags based on deepest nested component 1`] = `""`; 10 | 11 | exports[`script tags > Declarative API > sets script tags based on deepest nested component 2`] = `""`; 12 | 13 | exports[`script tags > Declarative API > sets undefined attribute values to empty strings 1`] = `""`; 14 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/style.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Declarative API > updates style tags 1`] = ` 4 | "" 9 | `; 10 | 11 | exports[`Declarative API > updates style tags 2`] = ` 12 | "" 17 | `; 18 | 19 | exports[`style tags > updates style tags 1`] = ` 20 | "" 25 | `; 26 | 27 | exports[`style tags > updates style tags 2`] = ` 28 | "" 33 | `; 34 | -------------------------------------------------------------------------------- /__tests__/api/__snapshots__/title.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`title > API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; 4 | 5 | exports[`title > API > merges deepest component title with nearest upstream titleTemplate 1`] = `"This is a Second Test of the titleTemplate feature"`; 6 | 7 | exports[`title > API > page title with prop itemprop 1`] = `"Test Title with itemProp"`; 8 | 9 | exports[`title > API > renders dollar characters in a title correctly when titleTemplate present 1`] = `"This is a te$t te$$t te$$$t te$$$$t"`; 10 | 11 | exports[`title > API > replaces multiple title strings in titleTemplate 1`] = `"This is a Test of the titleTemplate feature. Another Test."`; 12 | 13 | exports[`title > API > sets title based on deepest nested component 1`] = `"Nested Title"`; 14 | 15 | exports[`title > API > sets title using deepest nested component with a defined title 1`] = `"Main Title"`; 16 | 17 | exports[`title > API > updates page title 1`] = `"Test Title"`; 18 | 19 | exports[`title > API > updates page title with multiple children 1`] = `"Child Two Title"`; 20 | 21 | exports[`title > API > uses a titleTemplate based on deepest nested component 1`] = `"A Second Test using nested titleTemplate attributes"`; 22 | 23 | exports[`title > API > uses a titleTemplate if defined 1`] = `"This is a Test of the titleTemplate feature"`; 24 | 25 | exports[`title > API > uses defaultTitle if no title is defined 1`] = `"Fallback"`; 26 | 27 | exports[`title > Declarative API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; 28 | 29 | exports[`title > Declarative API > merges deepest component title with nearest upstream titleTemplate 1`] = `"This is a Second Test of the titleTemplate feature"`; 30 | 31 | exports[`title > Declarative API > page title with prop itemProp 1`] = `"Test Title with itemProp"`; 32 | 33 | exports[`title > Declarative API > renders dollar characters in a title correctly when titleTemplate present 1`] = `"This is a te$t te$$t te$$$t te$$$$t"`; 34 | 35 | exports[`title > Declarative API > replaces multiple title strings in titleTemplate 1`] = `"This is a Test of the titleTemplate feature. Another Test."`; 36 | 37 | exports[`title > Declarative API > retains existing title tag when no title tag is defined 1`] = `"Existing Title"`; 38 | 39 | exports[`title > Declarative API > sets title based on deepest nested component 1`] = `"Nested Title"`; 40 | 41 | exports[`title > Declarative API > sets title using deepest nested component with a defined title 1`] = `"Main Title"`; 42 | 43 | exports[`title > Declarative API > updates page title 1`] = `"Test Title"`; 44 | 45 | exports[`title > Declarative API > updates page title and allows children containing expressions 1`] = `"Title: Some Great Title"`; 46 | 47 | exports[`title > Declarative API > updates page title with multiple children 1`] = `"Child Two Title"`; 48 | 49 | exports[`title > Declarative API > uses a titleTemplate based on deepest nested component 1`] = `"A Second Test using nested titleTemplate attributes"`; 50 | 51 | exports[`title > Declarative API > uses a titleTemplate if defined 1`] = `"This is a Test of the titleTemplate feature"`; 52 | 53 | exports[`title > Declarative API > uses defaultTitle if no title is defined 1`] = `"Fallback"`; 54 | -------------------------------------------------------------------------------- /__tests__/api/base.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Helmet } from '../../src'; 4 | import { HELMET_ATTRIBUTE } from '../../src/constants'; 5 | import { render } from '../utils'; 6 | 7 | Helmet.defaultProps.defer = false; 8 | 9 | describe('base tag', () => { 10 | describe('API', () => { 11 | it('updates base tag', () => { 12 | render(); 13 | 14 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 15 | 16 | expect(existingTags).toBeDefined(); 17 | 18 | const filteredTags = existingTags.filter( 19 | tag => tag.getAttribute('href') === 'http://mysite.com/' 20 | ); 21 | 22 | expect(filteredTags).toHaveLength(1); 23 | }); 24 | 25 | it('clears the base tag if one is not specified', () => { 26 | render(); 27 | render(); 28 | 29 | const existingTags = document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`); 30 | 31 | expect(existingTags).toBeDefined(); 32 | expect(existingTags).toHaveLength(0); 33 | }); 34 | 35 | it("tags without 'href' are not accepted", () => { 36 | render(); 37 | const existingTags = document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`); 38 | 39 | expect(existingTags).toBeDefined(); 40 | expect(existingTags).toHaveLength(0); 41 | }); 42 | 43 | it('sets base tag based on deepest nested component', () => { 44 | render( 45 |
46 | 47 | 48 |
49 | ); 50 | 51 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 52 | const [firstTag] = existingTags; 53 | 54 | expect(existingTags).toBeDefined(); 55 | expect(existingTags).toHaveLength(1); 56 | 57 | expect(firstTag).toBeInstanceOf(Element); 58 | expect(firstTag.getAttribute).toBeDefined(); 59 | expect(firstTag).toHaveAttribute('href', 'http://mysite.com/public'); 60 | expect(firstTag.outerHTML).toMatchSnapshot(); 61 | }); 62 | 63 | it('does not render tag when primary attribute is null', () => { 64 | // @ts-ignore 65 | render(); 66 | 67 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 68 | 69 | expect(existingTags).toHaveLength(0); 70 | }); 71 | }); 72 | 73 | describe('Declarative API', () => { 74 | it('updates base tag', () => { 75 | render( 76 | 77 | 78 | 79 | ); 80 | 81 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 82 | 83 | expect(existingTags).toBeDefined(); 84 | 85 | const filteredTags = existingTags.filter( 86 | tag => tag.getAttribute('href') === 'http://mysite.com/' 87 | ); 88 | 89 | expect(filteredTags).toHaveLength(1); 90 | }); 91 | 92 | it('clears the base tag if one is not specified', () => { 93 | render(); 94 | render(); 95 | 96 | const existingTags = document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`); 97 | 98 | expect(existingTags).toBeDefined(); 99 | expect(existingTags).toHaveLength(0); 100 | }); 101 | 102 | it("tags without 'href' are not accepted", () => { 103 | render( 104 | 105 | 106 | 107 | ); 108 | 109 | const existingTags = document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`); 110 | 111 | expect(existingTags).toBeDefined(); 112 | expect(existingTags).toHaveLength(0); 113 | }); 114 | 115 | it('sets base tag based on deepest nested component', () => { 116 | render( 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | ); 126 | 127 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 128 | const [firstTag] = existingTags; 129 | 130 | expect(existingTags).toBeDefined(); 131 | expect(existingTags).toHaveLength(1); 132 | 133 | expect(firstTag).toBeInstanceOf(Element); 134 | expect(firstTag.getAttribute).toBeDefined(); 135 | expect(firstTag).toHaveAttribute('href', 'http://mysite.com/public'); 136 | expect(firstTag.outerHTML).toMatchSnapshot(); 137 | }); 138 | 139 | it('does not render tag when primary attribute is null', () => { 140 | render( 141 | 142 | 143 | 144 | ); 145 | 146 | const tagNodes = document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`); 147 | const existingTags = [].slice.call(tagNodes); 148 | 149 | expect(existingTags).toHaveLength(0); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /__tests__/api/bodyAttributes.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type { BodyProps } from '../../src'; 4 | import { Helmet } from '../../src'; 5 | import { HELMET_ATTRIBUTE, HTML_TAG_MAP } from '../../src/constants'; 6 | import { render } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | describe('body attributes', () => { 11 | describe('valid attributes', () => { 12 | const attributeList: BodyProps = { 13 | accessKey: 'c', 14 | className: 'test', 15 | contentEditable: 'true', 16 | contextMenu: 'mymenu', 17 | 'data-animal-type': 'lion', 18 | dir: 'rtl', 19 | draggable: 'true', 20 | dropzone: 'copy', 21 | // @ts-ignore 22 | hidden: 'true', 23 | id: 'test', 24 | lang: 'fr', 25 | spellcheck: 'true', 26 | // @ts-ignore 27 | style: 'color: green', 28 | // @ts-ignore 29 | tabIndex: '-1', 30 | title: 'test', 31 | translate: 'no', 32 | }; 33 | 34 | Object.keys(attributeList).forEach(attribute => { 35 | it(`${attribute}`, () => { 36 | const attrValue = attributeList[attribute]; 37 | 38 | const attr = { 39 | [attribute]: attrValue, 40 | }; 41 | 42 | render( 43 | 44 | 45 | 46 | ); 47 | 48 | const bodyTag = document.body; 49 | 50 | const reactCompatAttr = HTML_TAG_MAP[attribute] || attribute; 51 | 52 | expect(bodyTag).toHaveAttribute(reactCompatAttr, attrValue); 53 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, reactCompatAttr); 54 | }); 55 | }); 56 | }); 57 | 58 | it('updates multiple body attributes', () => { 59 | render( 60 | 61 | 62 | 63 | ); 64 | 65 | const bodyTag = document.body; 66 | 67 | expect(bodyTag).toHaveAttribute('class', 'myClassName'); 68 | expect(bodyTag).toHaveAttribute('tabindex', '-1'); 69 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'class,tabindex'); 70 | }); 71 | 72 | it('sets attributes based on the deepest nested component', () => { 73 | render( 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 | ); 83 | 84 | const bodyTag = document.body; 85 | 86 | expect(bodyTag).toHaveAttribute('lang', 'ja'); 87 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang'); 88 | }); 89 | 90 | it('handles valueless attributes', () => { 91 | render( 92 | 93 | 94 | 95 | ); 96 | 97 | const bodyTag = document.body; 98 | 99 | expect(bodyTag).toHaveAttribute('hidden', 'true'); 100 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'hidden'); 101 | }); 102 | 103 | it('clears body attributes that are handled within helmet', () => { 104 | render( 105 | 106 | 107 | 108 | ); 109 | 110 | render(); 111 | 112 | const bodyTag = document.body; 113 | 114 | expect(bodyTag).not.toHaveAttribute('lang'); 115 | expect(bodyTag).not.toHaveAttribute('hidden'); 116 | expect(bodyTag).not.toHaveAttribute(HELMET_ATTRIBUTE); 117 | }); 118 | 119 | it('updates with multiple additions and removals - overwrite and new', () => { 120 | render( 121 | 122 | 123 | 124 | ); 125 | 126 | render( 127 | 128 | 129 | 130 | ); 131 | 132 | const bodyTag = document.body; 133 | 134 | expect(bodyTag).not.toHaveAttribute('hidden'); 135 | expect(bodyTag).toHaveAttribute('lang', 'ja'); 136 | expect(bodyTag).toHaveAttribute('id', 'body-tag'); 137 | expect(bodyTag).toHaveAttribute('title', 'body tag'); 138 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang,id,title'); 139 | }); 140 | 141 | it('updates with multiple additions and removals - all new', () => { 142 | render( 143 | 144 | 145 | 146 | ); 147 | 148 | render( 149 | 150 | 151 | 152 | ); 153 | 154 | const bodyTag = document.body; 155 | 156 | expect(bodyTag).not.toHaveAttribute('hidden'); 157 | expect(bodyTag).not.toHaveAttribute('lang'); 158 | expect(bodyTag).toHaveAttribute('id', 'body-tag'); 159 | expect(bodyTag).toHaveAttribute('title', 'body tag'); 160 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'id,title'); 161 | }); 162 | 163 | describe('initialized outside of helmet', () => { 164 | beforeEach(() => { 165 | const bodyTag = document.body; 166 | bodyTag.setAttribute('test', 'test'); 167 | }); 168 | 169 | it('attributes are not cleared', () => { 170 | render(); 171 | 172 | const bodyTag = document.body; 173 | 174 | expect(bodyTag).toHaveAttribute('test', 'test'); 175 | expect(bodyTag).not.toHaveAttribute(HELMET_ATTRIBUTE); 176 | }); 177 | 178 | it('attributes are overwritten if specified in helmet', () => { 179 | render( 180 | 181 | 185 | 186 | ); 187 | 188 | const bodyTag = document.body; 189 | 190 | expect(bodyTag).toHaveAttribute('test', 'helmet-attr'); 191 | expect(bodyTag).toHaveAttribute(HELMET_ATTRIBUTE, 'test'); 192 | }); 193 | 194 | it('attributes are cleared once managed in helmet', () => { 195 | render( 196 | 197 | 201 | 202 | ); 203 | 204 | render(); 205 | 206 | const bodyTag = document.body; 207 | 208 | expect(bodyTag).not.toHaveAttribute('test'); 209 | expect(bodyTag).not.toHaveAttribute(HELMET_ATTRIBUTE); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /__tests__/api/client.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Helmet } from '../../src'; 4 | import { render } from '../utils'; 5 | 6 | Helmet.defaultProps.defer = false; 7 | 8 | describe('onChangeClientState', () => { 9 | describe('API', () => { 10 | it('when handling client state change, calls the function with new state, addedTags and removedTags', () => { 11 | const onChange = vi.fn(); 12 | render( 13 |
14 | 36 |
37 | ); 38 | 39 | expect(onChange).toHaveBeenCalled(); 40 | 41 | const newState = onChange.mock.calls[0][0]; 42 | const addedTags = onChange.mock.calls[0][1]; 43 | const removedTags = onChange.mock.calls[0][2]; 44 | 45 | expect(newState).toEqual(expect.objectContaining({ title: 'Main Title' })); 46 | expect(newState.baseTag[0]).toEqual( 47 | expect.objectContaining({ 48 | href: 'http://mysite.com/', 49 | }) 50 | ); 51 | expect(newState.metaTags[0]).toEqual(expect.objectContaining({ charset: 'utf-8' })); 52 | expect(newState.linkTags[0]).toEqual( 53 | expect.objectContaining({ 54 | href: 'http://localhost/helmet', 55 | rel: 'canonical', 56 | }) 57 | ); 58 | expect(newState.scriptTags[0]).toEqual( 59 | expect.objectContaining({ 60 | src: 'http://localhost/test.js', 61 | type: 'text/javascript', 62 | }) 63 | ); 64 | 65 | expect(addedTags.baseTag).toBeDefined(); 66 | expect(addedTags.baseTag[0]).toBeDefined(); 67 | expect(addedTags.baseTag[0].outerHTML).toMatchSnapshot(); 68 | 69 | expect(addedTags.metaTags).toBeDefined(); 70 | expect(addedTags.metaTags[0]).toBeDefined(); 71 | expect(addedTags.metaTags[0].outerHTML).toMatchSnapshot(); 72 | 73 | expect(addedTags.linkTags).toBeDefined(); 74 | expect(addedTags.linkTags[0]).toBeDefined(); 75 | expect(addedTags.linkTags[0].outerHTML).toMatchSnapshot(); 76 | 77 | expect(addedTags.scriptTags).toBeDefined(); 78 | expect(addedTags.scriptTags[0]).toBeDefined(); 79 | expect(addedTags.scriptTags[0].outerHTML).toMatchSnapshot(); 80 | 81 | expect(removedTags).toEqual({}); 82 | }); 83 | 84 | // it('calls the deepest defined callback with the deepest state', () => { 85 | // const onChange = vi.fn(); 86 | // render( 87 | //
88 | // 89 | // 90 | //
91 | // ); 92 | // 93 | // expect(onChange).toBeCalled(); 94 | // expect(onChange.mock.calls).toHaveLength(1); 95 | // expect(onChange.mock.calls[0][0]).toEqual( 96 | // expect.objectContaining({ 97 | // title: 'Deeper Title', 98 | // }) 99 | // ); 100 | // }); 101 | }); 102 | 103 | describe('Declarative API', () => { 104 | it('when handling client state change, calls the function with new state, addedTags and removedTags', () => { 105 | const onChange = vi.fn(); 106 | render( 107 |
108 | 109 | 110 | 111 | 112 | 195 | 196 | ); 197 | 198 | const existingTags = [...document.head.getElementsByTagName('script')]; 199 | 200 | expect(existingTags).toBeDefined(); 201 | 202 | const filteredTags = existingTags.filter( 203 | tag => 204 | (tag.getAttribute('src') === 'http://localhost/test.js' && 205 | tag.getAttribute('type') === 'text/javascript') || 206 | (tag.getAttribute('src') === 'http://localhost/test2.js' && 207 | tag.getAttribute('type') === 'text/javascript') || 208 | (tag.getAttribute('type') === 'application/ld+json' && tag.innerHTML === scriptInnerHTML) 209 | ); 210 | 211 | expect(filteredTags.length).toBeGreaterThanOrEqual(3); 212 | }); 213 | 214 | it('clears all scripts tags if none are specified', () => { 215 | render( 216 | 217 | 74 | 75 | 76 | 77 | 78 |
79 | ); 80 | 81 | expect(window.__spy__).toHaveBeenCalledTimes(1); 82 | 83 | await vi.waitFor( 84 | () => 85 | new Promise(resolve => { 86 | requestAnimationFrame(() => { 87 | expect(window.__spy__).toHaveBeenCalledTimes(2); 88 | expect(window.__spy__.mock.calls).toStrictEqual([[1], [2]]); 89 | 90 | resolve(true); 91 | }); 92 | }) 93 | ); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /__tests__/fragment.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Helmet } from '../src'; 4 | 5 | import { render } from './utils'; 6 | 7 | // TODO: This is confusing 8 | Helmet.defaultProps.defer = false; 9 | 10 | describe('fragments', () => { 11 | it('parses Fragments', () => { 12 | const title = 'Hello'; 13 | render( 14 | 15 | <> 16 | {title} 17 | 18 | 19 | 20 | ); 21 | 22 | expect(document.title).toBe(title); 23 | }); 24 | 25 | it('parses nested Fragments', () => { 26 | const title = 'Baz'; 27 | render( 28 | 29 | <> 30 | Foo 31 | <> 32 | Bar 33 | {title} 34 | 35 | 36 | 37 | ); 38 | 39 | expect(document.title).toBe(title); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/base.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders base tag as React component 1`] = `""`; 4 | 5 | exports[`server > API > renders base tags as string 1`] = `""`; 6 | 7 | exports[`server > Declarative API > renders base tag as React component 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders base tags as string 1`] = `""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/bodyAttributes.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > Declarative API > renders body attributes as component 1`] = `""`; 4 | 5 | exports[`server > Declarative API > renders body attributes as string 1`] = `"lang=\\"ga\\" class=\\"myClassName\\""`; 6 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/helmetData.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Helmet Data > browser > renders declarative without context 1`] = `""`; 4 | 5 | exports[`Helmet Data > browser > renders without context 1`] = `""`; 6 | 7 | exports[`Helmet Data > browser > sets base tag based on deepest nested component 1`] = `""`; 8 | 9 | exports[`Helmet Data > server > renders declarative without context 1`] = `""`; 10 | 11 | exports[`Helmet Data > server > renders without context 1`] = `""`; 12 | 13 | exports[`Helmet Data > server > sets base tag based on deepest nested component 1`] = `""`; 14 | 15 | exports[`Helmet Data > server > works with the same context object but separate HelmetData instances 1`] = `""`; 16 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/htmlAttributes.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders html attributes as component 1`] = `""`; 4 | 5 | exports[`server > API > renders html attributes as string 1`] = `"lang=\\"ga\\" class=\\"myClassName\\""`; 6 | 7 | exports[`server > Declarative API > renders html attributes as component 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders html attributes as string 1`] = `"lang=\\"ga\\" class=\\"myClassName\\""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/link.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders link tags as React components 1`] = `""`; 4 | 5 | exports[`server > API > renders link tags as string 1`] = `""`; 6 | 7 | exports[`server > Declarative API > renders link tags as React components 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders link tags as string 1`] = `""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/meta.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders meta tags as React components 1`] = `""`; 4 | 5 | exports[`server > API > renders meta tags as string 1`] = `""`; 6 | 7 | exports[`server > Declarative API > renders meta tags as React components 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders meta tags as string 1`] = `""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/noscript.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders noscript tags as React components 1`] = `""`; 4 | 5 | exports[`server > Declarative API > renders noscript tags as React components 1`] = `""`; 6 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/script.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders script tags as React components 1`] = `""`; 4 | 5 | exports[`server > API > renders script tags as string 1`] = `""`; 6 | 7 | exports[`server > Declarative API > renders script tags as React components 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders script tags as string 1`] = `""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/server.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > does not render undefined attribute values 1`] = `""`; 4 | 5 | exports[`server > API > rewind() provides a fallback object for empty Helmet state 1`] = `""`; 6 | 7 | exports[`server > API > rewind() provides a fallback object for empty Helmet state 2`] = `""`; 8 | 9 | exports[`server > Declarative API > does not render undefined attribute values 1`] = `""`; 10 | 11 | exports[`server > Declarative API > rewind() provides a fallback object for empty Helmet state 1`] = `""`; 12 | 13 | exports[`server > Declarative API > rewind() provides a fallback object for empty Helmet state 2`] = `""`; 14 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/style.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > renders style tags as React components 1`] = `""`; 4 | 5 | exports[`server > API > renders style tags as string 1`] = `""`; 6 | 7 | exports[`server > Declarative API > renders style tags as React components 1`] = `""`; 8 | 9 | exports[`server > Declarative API > renders style tags as string 1`] = `""`; 10 | -------------------------------------------------------------------------------- /__tests__/server/__snapshots__/title.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`server > API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; 4 | 5 | exports[`server > API > encodes special characters in title 1`] = `"Dangerous <script> include"`; 6 | 7 | exports[`server > API > opts out of string encoding 1`] = `"This is text and & and '."`; 8 | 9 | exports[`server > API > renders title as React component 1`] = `"Dangerous <script> include"`; 10 | 11 | exports[`server > API > renders title tag as string 1`] = `"Dangerous <script> include"`; 12 | 13 | exports[`server > API > renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; 14 | 15 | exports[`server > API > renders title with itemprop name as string 1`] = `"Title with Itemprop"`; 16 | 17 | exports[`server > Declarative API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; 18 | 19 | exports[`server > Declarative API > encodes special characters in title 1`] = `"Dangerous <script> include"`; 20 | 21 | exports[`server > Declarative API > opts out of string encoding 1`] = `"This is text and & and '."`; 22 | 23 | exports[`server > Declarative API > renders title and allows children containing expressions 1`] = `"Title: Some Great Title"`; 24 | 25 | exports[`server > Declarative API > renders title as React component 1`] = `"Dangerous <script> include"`; 26 | 27 | exports[`server > Declarative API > renders title tag as string 1`] = `"Dangerous <script> include"`; 28 | 29 | exports[`server > Declarative API > renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; 30 | 31 | exports[`server > Declarative API > renders title with itemprop name as string 1`] = `"Title with Itemprop"`; 32 | 33 | exports[`server > renderStatic > does html encode title 1`] = `"Dangerous <script> include"`; 34 | 35 | exports[`server > renderStatic > renders title as React component 1`] = `"Dangerous <script> include"`; 36 | -------------------------------------------------------------------------------- /__tests__/server/base.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext, isArray } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders base tag as React component', () => { 21 | const head = renderContext(); 22 | 23 | expect(head.base).toBeDefined(); 24 | expect(head.base.toComponent).toBeDefined(); 25 | 26 | const baseComponent = head.base.toComponent(); 27 | 28 | expect(baseComponent).toEqual(isArray); 29 | expect(baseComponent).toHaveLength(1); 30 | 31 | baseComponent.forEach((base: Element) => { 32 | expect(base).toEqual(expect.objectContaining({ type: 'base' })); 33 | }); 34 | 35 | const markup = ReactServer.renderToStaticMarkup(baseComponent); 36 | 37 | expect(markup).toMatchSnapshot(); 38 | }); 39 | 40 | it('renders base tags as string', () => { 41 | const head = renderContext(); 42 | expect(head.base).toBeDefined(); 43 | expect(head.base.toString).toBeDefined(); 44 | expect(head.base.toString()).toMatchSnapshot(); 45 | }); 46 | }); 47 | 48 | describe('Declarative API', () => { 49 | it('renders base tag as React component', () => { 50 | const head = renderContext( 51 | 52 | 53 | 54 | ); 55 | 56 | expect(head.base).toBeDefined(); 57 | expect(head.base.toComponent).toBeDefined(); 58 | 59 | const baseComponent = head.base.toComponent(); 60 | 61 | expect(baseComponent).toEqual(isArray); 62 | expect(baseComponent).toHaveLength(1); 63 | 64 | baseComponent.forEach((base: Element) => { 65 | expect(base).toEqual(expect.objectContaining({ type: 'base' })); 66 | }); 67 | 68 | const markup = ReactServer.renderToStaticMarkup(baseComponent); 69 | 70 | expect(markup).toMatchSnapshot(); 71 | }); 72 | 73 | it('renders base tags as string', () => { 74 | const head = renderContext( 75 | 76 | 77 | 78 | ); 79 | 80 | expect(head.base).toBeDefined(); 81 | expect(head.base.toString).toBeDefined(); 82 | expect(head.base.toString()).toMatchSnapshot(); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /__tests__/server/bodyAttributes.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('Declarative API', () => { 20 | it('renders body attributes as component', () => { 21 | const head = renderContext( 22 | 23 | 24 | 25 | ); 26 | const attrs = head.bodyAttributes.toComponent(); 27 | 28 | expect(attrs).toBeDefined(); 29 | 30 | const markup = ReactServer.renderToStaticMarkup(); 31 | 32 | expect(markup).toMatchSnapshot(); 33 | }); 34 | 35 | it('renders body attributes as string', () => { 36 | const body = renderContext( 37 | 38 | 39 | 40 | ); 41 | 42 | expect(body.bodyAttributes).toBeDefined(); 43 | expect(body.bodyAttributes.toString).toBeDefined(); 44 | expect(body.bodyAttributes.toString()).toMatchSnapshot(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/server/helmetData.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Helmet } from '../../src'; 4 | import Provider from '../../src/Provider'; 5 | import HelmetData from '../../src/HelmetData'; 6 | import { HELMET_ATTRIBUTE } from '../../src/constants'; 7 | import { render } from '../utils'; 8 | 9 | Helmet.defaultProps.defer = false; 10 | 11 | describe('Helmet Data', () => { 12 | describe('server', () => { 13 | beforeAll(() => { 14 | Provider.canUseDOM = false; 15 | }); 16 | 17 | afterAll(() => { 18 | Provider.canUseDOM = true; 19 | }); 20 | 21 | it('renders without context', () => { 22 | const helmetData = new HelmetData({}); 23 | 24 | render( 25 | 26 | ); 27 | 28 | const head = helmetData.context.helmet; 29 | 30 | expect(head.base).toBeDefined(); 31 | expect(head.base.toString).toBeDefined(); 32 | expect(head.base.toString()).toMatchSnapshot(); 33 | }); 34 | 35 | it('renders declarative without context', () => { 36 | const helmetData = new HelmetData({}); 37 | 38 | render( 39 | 40 | 41 | 42 | ); 43 | 44 | const head = helmetData.context.helmet; 45 | 46 | expect(head.base).toBeDefined(); 47 | expect(head.base.toString).toBeDefined(); 48 | expect(head.base.toString()).toMatchSnapshot(); 49 | }); 50 | 51 | it('sets base tag based on deepest nested component', () => { 52 | const helmetData = new HelmetData({}); 53 | 54 | render( 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | ); 64 | 65 | const head = helmetData.context.helmet; 66 | 67 | expect(head.base).toBeDefined(); 68 | expect(head.base.toString).toBeDefined(); 69 | expect(head.base.toString()).toMatchSnapshot(); 70 | }); 71 | 72 | it('works with the same context object but separate HelmetData instances', () => { 73 | const context = {} as any; 74 | 75 | render( 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 |
84 | ); 85 | 86 | const head = context.helmet; 87 | 88 | expect(head.base).toBeDefined(); 89 | expect(head.base.toString).toBeDefined(); 90 | expect(head.base.toString()).toMatchSnapshot(); 91 | }); 92 | }); 93 | 94 | describe('browser', () => { 95 | it('renders without context', () => { 96 | const helmetData = new HelmetData({}); 97 | 98 | render( 99 | 100 | ); 101 | 102 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 103 | const [firstTag] = existingTags; 104 | 105 | expect(existingTags).toBeDefined(); 106 | expect(existingTags).toHaveLength(1); 107 | 108 | expect(firstTag).toBeInstanceOf(Element); 109 | expect(firstTag.getAttribute).toBeDefined(); 110 | expect(firstTag).toHaveAttribute('href', 'http://localhost/'); 111 | expect(firstTag.outerHTML).toMatchSnapshot(); 112 | }); 113 | 114 | it('renders declarative without context', () => { 115 | const helmetData = new HelmetData({}); 116 | 117 | render( 118 | 119 | 120 | 121 | ); 122 | 123 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 124 | const [firstTag] = existingTags; 125 | 126 | expect(existingTags).toBeDefined(); 127 | expect(existingTags).toHaveLength(1); 128 | 129 | expect(firstTag).toBeInstanceOf(Element); 130 | expect(firstTag.getAttribute).toBeDefined(); 131 | expect(firstTag).toHaveAttribute('href', 'http://localhost/'); 132 | expect(firstTag.outerHTML).toMatchSnapshot(); 133 | }); 134 | 135 | it('sets base tag based on deepest nested component', () => { 136 | const helmetData = new HelmetData({}); 137 | 138 | render( 139 |
140 | 141 | 142 | 143 | 144 | 145 | 146 |
147 | ); 148 | 149 | const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; 150 | const [firstTag] = existingTags; 151 | 152 | expect(existingTags).toBeDefined(); 153 | expect(existingTags).toHaveLength(1); 154 | 155 | expect(firstTag).toBeInstanceOf(Element); 156 | expect(firstTag.getAttribute).toBeDefined(); 157 | expect(firstTag).toHaveAttribute('href', 'http://mysite.com/public'); 158 | expect(firstTag.outerHTML).toMatchSnapshot(); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /__tests__/server/htmlAttributes.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders html attributes as component', () => { 21 | const head = renderContext( 22 | 28 | ); 29 | 30 | const attrs = head.htmlAttributes.toComponent(); 31 | 32 | expect(attrs).toBeDefined(); 33 | 34 | const markup = ReactServer.renderToStaticMarkup(); 35 | 36 | expect(markup).toMatchSnapshot(); 37 | }); 38 | 39 | it('renders html attributes as string', () => { 40 | const head = renderContext( 41 | 47 | ); 48 | 49 | expect(head.htmlAttributes).toBeDefined(); 50 | expect(head.htmlAttributes.toString).toBeDefined(); 51 | expect(head.htmlAttributes.toString()).toMatchSnapshot(); 52 | }); 53 | }); 54 | 55 | describe('Declarative API', () => { 56 | it('renders html attributes as component', () => { 57 | const head = renderContext( 58 | 59 | 60 | 61 | ); 62 | 63 | const attrs = head.htmlAttributes.toComponent(); 64 | 65 | expect(attrs).toBeDefined(); 66 | 67 | const markup = ReactServer.renderToStaticMarkup(); 68 | 69 | expect(markup).toMatchSnapshot(); 70 | }); 71 | 72 | it('renders html attributes as string', () => { 73 | const head = renderContext( 74 | 75 | 76 | 77 | ); 78 | 79 | expect(head.htmlAttributes).toBeDefined(); 80 | expect(head.htmlAttributes.toString).toBeDefined(); 81 | expect(head.htmlAttributes.toString()).toMatchSnapshot(); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /__tests__/server/link.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext, isArray } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders link tags as React components', () => { 21 | const head = renderContext( 22 | 32 | ); 33 | 34 | expect(head.link).toBeDefined(); 35 | expect(head.link.toComponent).toBeDefined(); 36 | 37 | const linkComponent = head.link.toComponent(); 38 | 39 | expect(linkComponent).toEqual(isArray); 40 | expect(linkComponent).toHaveLength(2); 41 | 42 | linkComponent.forEach((link: Element) => { 43 | expect(link).toEqual(expect.objectContaining({ type: 'link' })); 44 | }); 45 | 46 | const markup = ReactServer.renderToStaticMarkup(linkComponent); 47 | 48 | expect(markup).toMatchSnapshot(); 49 | }); 50 | 51 | it('renders link tags as string', () => { 52 | const head = renderContext( 53 | 63 | ); 64 | 65 | expect(head.link).toBeDefined(); 66 | expect(head.link.toString).toBeDefined(); 67 | expect(head.link.toString()).toMatchSnapshot(); 68 | }); 69 | }); 70 | 71 | describe('Declarative API', () => { 72 | it('renders link tags as React components', () => { 73 | const head = renderContext( 74 | 75 | 76 | 77 | 78 | ); 79 | 80 | expect(head.link).toBeDefined(); 81 | expect(head.link.toComponent).toBeDefined(); 82 | 83 | const linkComponent = head.link.toComponent(); 84 | 85 | expect(linkComponent).toEqual(isArray); 86 | expect(linkComponent).toHaveLength(2); 87 | 88 | linkComponent.forEach((link: Element) => { 89 | expect(link).toEqual(expect.objectContaining({ type: 'link' })); 90 | }); 91 | 92 | const markup = ReactServer.renderToStaticMarkup(linkComponent); 93 | 94 | expect(markup).toMatchSnapshot(); 95 | }); 96 | 97 | it('renders link tags as string', () => { 98 | const head = renderContext( 99 | 100 | 101 | 102 | 103 | ); 104 | 105 | expect(head.link).toBeDefined(); 106 | expect(head.link.toString).toBeDefined(); 107 | expect(head.link.toString()).toMatchSnapshot(); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /__tests__/server/meta.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext, isArray } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders meta tags as React components', () => { 21 | const head = renderContext( 22 | < `', 28 | }, 29 | { 'http-equiv': 'content-type', content: 'text/html' }, 30 | { property: 'og:type', content: 'article' }, 31 | { itemprop: 'name', content: 'Test name itemprop' }, 32 | ]} 33 | /> 34 | ); 35 | 36 | expect(head.meta).toBeDefined(); 37 | expect(head.meta.toComponent).toBeDefined(); 38 | 39 | const metaComponent = head.meta.toComponent(); 40 | 41 | expect(metaComponent).toEqual(isArray); 42 | expect(metaComponent).toHaveLength(5); 43 | 44 | metaComponent.forEach((meta: Element) => { 45 | expect(meta).toEqual(expect.objectContaining({ type: 'meta' })); 46 | }); 47 | 48 | const markup = ReactServer.renderToStaticMarkup(metaComponent); 49 | 50 | expect(markup).toMatchSnapshot(); 51 | }); 52 | 53 | it('renders meta tags as string', () => { 54 | const head = renderContext( 55 | < `', 61 | }, 62 | { 'http-equiv': 'content-type', content: 'text/html' }, 63 | { property: 'og:type', content: 'article' }, 64 | { itemprop: 'name', content: 'Test name itemprop' }, 65 | ]} 66 | /> 67 | ); 68 | 69 | expect(head.meta).toBeDefined(); 70 | expect(head.meta.toString).toBeDefined(); 71 | expect(head.meta.toString()).toMatchSnapshot(); 72 | }); 73 | }); 74 | 75 | describe('Declarative API', () => { 76 | it('renders meta tags as React components', () => { 77 | const head = renderContext( 78 | 79 | 80 | < `'} 83 | /> 84 | 85 | 86 | 87 | 88 | ); 89 | 90 | expect(head.meta).toBeDefined(); 91 | expect(head.meta.toComponent).toBeDefined(); 92 | 93 | const metaComponent = head.meta.toComponent(); 94 | 95 | expect(metaComponent).toEqual(isArray); 96 | expect(metaComponent).toHaveLength(5); 97 | 98 | metaComponent.forEach((meta: Element) => { 99 | expect(meta).toEqual(expect.objectContaining({ type: 'meta' })); 100 | }); 101 | 102 | const markup = ReactServer.renderToStaticMarkup(metaComponent); 103 | 104 | expect(markup).toMatchSnapshot(); 105 | }); 106 | 107 | it('renders meta tags as string', () => { 108 | const head = renderContext( 109 | 110 | 111 | 115 | 116 | 117 | 118 | 119 | ); 120 | 121 | expect(head.meta).toBeDefined(); 122 | expect(head.meta.toString).toBeDefined(); 123 | expect(head.meta.toString()).toMatchSnapshot(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /__tests__/server/noscript.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext, isArray } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders noscript tags as React components', () => { 21 | const head = renderContext( 22 | ', 27 | }, 28 | { 29 | id: 'bar', 30 | innerHTML: '', 31 | }, 32 | ]} 33 | /> 34 | ); 35 | 36 | expect(head.noscript).toBeDefined(); 37 | expect(head.noscript.toComponent).toBeDefined(); 38 | 39 | const noscriptComponent = head.noscript.toComponent(); 40 | 41 | expect(noscriptComponent).toEqual(isArray); 42 | expect(noscriptComponent).toHaveLength(2); 43 | 44 | noscriptComponent.forEach((noscript: Element) => { 45 | expect(noscript).toEqual(expect.objectContaining({ type: 'noscript' })); 46 | }); 47 | 48 | const markup = ReactServer.renderToStaticMarkup(noscriptComponent); 49 | 50 | expect(markup).toMatchSnapshot(); 51 | }); 52 | }); 53 | 54 | describe('Declarative API', () => { 55 | it('renders noscript tags as React components', () => { 56 | const head = renderContext( 57 | 58 | 59 | 60 | 61 | ); 62 | 63 | expect(head.noscript).toBeDefined(); 64 | expect(head.noscript.toComponent).toBeDefined(); 65 | 66 | const noscriptComponent = head.noscript.toComponent(); 67 | 68 | expect(noscriptComponent).toEqual(isArray); 69 | expect(noscriptComponent).toHaveLength(2); 70 | 71 | noscriptComponent.forEach((noscript: Element) => { 72 | expect(noscript).toEqual(expect.objectContaining({ type: 'noscript' })); 73 | }); 74 | 75 | const markup = ReactServer.renderToStaticMarkup(noscriptComponent); 76 | 77 | expect(markup).toMatchSnapshot(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /__tests__/server/script.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactServer from 'react-dom/server'; 3 | 4 | import { Helmet } from '../../src'; 5 | import Provider from '../../src/Provider'; 6 | import { renderContext, isArray } from '../utils'; 7 | 8 | Helmet.defaultProps.defer = false; 9 | 10 | beforeAll(() => { 11 | Provider.canUseDOM = false; 12 | }); 13 | 14 | afterAll(() => { 15 | Provider.canUseDOM = true; 16 | }); 17 | 18 | describe('server', () => { 19 | describe('API', () => { 20 | it('renders script tags as React components', () => { 21 | const head = renderContext( 22 | 34 | ); 35 | 36 | expect(head.script).toBeDefined(); 37 | expect(head.script.toComponent).toBeDefined(); 38 | 39 | const scriptComponent = head.script.toComponent(); 40 | 41 | expect(scriptComponent).toEqual(isArray); 42 | expect(scriptComponent).toHaveLength(2); 43 | 44 | scriptComponent.forEach((script: Element) => { 45 | expect(script).toEqual(expect.objectContaining({ type: 'script' })); 46 | }); 47 | 48 | const markup = ReactServer.renderToStaticMarkup(scriptComponent); 49 | 50 | expect(markup).toMatchSnapshot(); 51 | }); 52 | 53 | it('renders script tags as string', () => { 54 | const head = renderContext( 55 | 67 | ); 68 | 69 | expect(head.script).toBeDefined(); 70 | expect(head.script.toString).toBeDefined(); 71 | expect(head.script.toString()).toMatchSnapshot(); 72 | }); 73 | }); 74 | 75 | describe('Declarative API', () => { 76 | it('renders script tags as React components', () => { 77 | const head = renderContext( 78 | 79 |