├── .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 | [](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 |
113 | Main Title
114 |
115 |
116 | );
117 |
118 | expect(onChange).toHaveBeenCalled();
119 |
120 | const newState = onChange.mock.calls[0][0];
121 | const addedTags = onChange.mock.calls[0][1];
122 | const removedTags = onChange.mock.calls[0][2];
123 |
124 | expect(newState).toEqual(expect.objectContaining({ title: 'Main Title' }));
125 | expect(newState.baseTag[0]).toEqual(
126 | expect.objectContaining({
127 | href: 'http://mysite.com/',
128 | })
129 | );
130 | expect(newState.metaTags[0]).toEqual(expect.objectContaining({ charset: 'utf-8' }));
131 | expect(newState.linkTags[0]).toEqual(
132 | expect.objectContaining({
133 | href: 'http://localhost/helmet',
134 | rel: 'canonical',
135 | })
136 | );
137 | expect(newState.scriptTags[0]).toEqual(
138 | expect.objectContaining({
139 | src: 'http://localhost/test.js',
140 | type: 'text/javascript',
141 | })
142 | );
143 |
144 | expect(addedTags.baseTag).toBeDefined();
145 | expect(addedTags.baseTag[0]).toBeDefined();
146 | expect(addedTags.baseTag[0].outerHTML).toMatchSnapshot();
147 |
148 | expect(addedTags.metaTags).toBeDefined();
149 | expect(addedTags.metaTags[0]).toBeDefined();
150 | expect(addedTags.metaTags[0].outerHTML).toMatchSnapshot();
151 |
152 | expect(addedTags.linkTags).toBeDefined();
153 | expect(addedTags.linkTags[0]).toBeDefined();
154 | expect(addedTags.linkTags[0].outerHTML).toMatchSnapshot();
155 |
156 | expect(addedTags.scriptTags).toBeDefined();
157 | expect(addedTags.scriptTags[0]).toBeDefined();
158 | expect(addedTags.scriptTags[0].outerHTML).toMatchSnapshot();
159 |
160 | expect(removedTags).toEqual({});
161 | });
162 | });
163 |
164 | // it('calls the deepest defined callback with the deepest state', () => {
165 | // const onChange = vi.fn();
166 | // render(
167 | //
168 | //
169 | // Main Title
170 | //
171 | //
172 | // Deeper Title
173 | //
174 | //
175 | // );
176 | //
177 | // expect(onChange).toBeCalled();
178 | // expect(onChange.mock.calls).toHaveLength(1);
179 | // expect(onChange.mock.calls[0][0]).toEqual(
180 | // expect.objectContaining({
181 | // title: 'Deeper Title',
182 | // })
183 | // );
184 | // });
185 | });
186 |
--------------------------------------------------------------------------------
/__tests__/api/htmlAttributes.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('html attributes', () => {
10 | describe('API', () => {
11 | it('updates html attributes', () => {
12 | render(
13 |
19 | );
20 |
21 | const htmlTag = document.documentElement;
22 |
23 | expect(htmlTag).toHaveAttribute('class', 'myClassName');
24 | expect(htmlTag).toHaveAttribute('lang', 'en');
25 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'class,lang');
26 | });
27 |
28 | it('sets attributes based on the deepest nested component', () => {
29 | render(
30 |
31 |
36 |
41 |
42 | );
43 |
44 | const htmlTag = document.documentElement;
45 |
46 | expect(htmlTag).toHaveAttribute('lang', 'ja');
47 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang');
48 | });
49 |
50 | it('handles valueless attributes', () => {
51 | render(
52 |
57 | );
58 |
59 | const htmlTag = document.documentElement;
60 |
61 | expect(htmlTag).toHaveAttribute('amp', '');
62 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'amp');
63 | });
64 |
65 | it('clears html attributes that are handled within helmet', () => {
66 | render(
67 |
73 | );
74 |
75 | render( );
76 |
77 | const htmlTag = document.documentElement;
78 |
79 | expect(htmlTag).not.toHaveAttribute('lang');
80 | expect(htmlTag).not.toHaveAttribute('amp');
81 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
82 | });
83 |
84 | it('updates with multiple additions and removals - overwrite and new', () => {
85 | render(
86 |
92 | );
93 |
94 | render(
95 |
102 | );
103 |
104 | const htmlTag = document.documentElement;
105 |
106 | expect(htmlTag).not.toHaveAttribute('amp');
107 | expect(htmlTag).toHaveAttribute('lang', 'ja');
108 | expect(htmlTag).toHaveAttribute('id', 'html-tag');
109 | expect(htmlTag).toHaveAttribute('title', 'html tag');
110 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang,id,title');
111 | });
112 |
113 | it('updates with multiple additions and removals - all new', () => {
114 | render(
115 |
121 | );
122 |
123 | render(
124 |
130 | );
131 |
132 | const htmlTag = document.documentElement;
133 |
134 | expect(htmlTag).not.toHaveAttribute('amp');
135 | expect(htmlTag).not.toHaveAttribute('lang');
136 | expect(htmlTag).toHaveAttribute('id', 'html-tag');
137 | expect(htmlTag).toHaveAttribute('title', 'html tag');
138 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'id,title');
139 | });
140 |
141 | describe('initialized outside of helmet', () => {
142 | beforeEach(() => {
143 | const htmlTag = document.documentElement;
144 | htmlTag.setAttribute('test', 'test');
145 | });
146 |
147 | it('attributes are not cleared', () => {
148 | render( );
149 |
150 | const htmlTag = document.documentElement;
151 |
152 | expect(htmlTag).toHaveAttribute('test', 'test');
153 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
154 | });
155 |
156 | it('attributes are overwritten if specified in helmet', () => {
157 | render(
158 |
163 | );
164 |
165 | const htmlTag = document.documentElement;
166 |
167 | expect(htmlTag).toHaveAttribute('test', 'helmet-attr');
168 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'test');
169 | });
170 |
171 | it('attributes are cleared once managed in helmet', () => {
172 | render(
173 |
178 | );
179 |
180 | render( );
181 |
182 | const htmlTag = document.documentElement;
183 |
184 | expect(htmlTag).not.toHaveAttribute('test');
185 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
186 | });
187 | });
188 | });
189 |
190 | describe('Declarative API', () => {
191 | it('updates html attributes', () => {
192 | render(
193 |
194 |
195 |
196 | );
197 |
198 | const htmlTag = document.documentElement;
199 |
200 | expect(htmlTag).toHaveAttribute('class', 'myClassName');
201 | expect(htmlTag).toHaveAttribute('lang', 'en');
202 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'class,lang');
203 | });
204 |
205 | it('sets attributes based on the deepest nested component', () => {
206 | render(
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | );
216 |
217 | const htmlTag = document.documentElement;
218 |
219 | expect(htmlTag).toHaveAttribute('lang', 'ja');
220 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang');
221 | });
222 |
223 | it('handles valueless attributes', () => {
224 | render(
225 |
226 |
230 |
231 | );
232 |
233 | const htmlTag = document.documentElement;
234 |
235 | expect(htmlTag).toHaveAttribute('amp', 'true');
236 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'amp');
237 | });
238 |
239 | it('clears html attributes that are handled within helmet', () => {
240 | render(
241 |
242 |
247 |
248 | );
249 |
250 | render( );
251 |
252 | const htmlTag = document.documentElement;
253 |
254 | expect(htmlTag).not.toHaveAttribute('lang');
255 | expect(htmlTag).not.toHaveAttribute('amp');
256 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
257 | });
258 |
259 | it('updates with multiple additions and removals - overwrite and new', () => {
260 | render(
261 |
262 |
267 |
268 | );
269 |
270 | render(
271 |
272 |
273 |
274 | );
275 |
276 | const htmlTag = document.documentElement;
277 |
278 | expect(htmlTag).not.toHaveAttribute('amp');
279 | expect(htmlTag).toHaveAttribute('lang', 'ja');
280 | expect(htmlTag).toHaveAttribute('id', 'html-tag');
281 | expect(htmlTag).toHaveAttribute('title', 'html tag');
282 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang,id,title');
283 | });
284 |
285 | it('updates with multiple additions and removals - all new', () => {
286 | render(
287 |
288 |
293 |
294 | );
295 |
296 | render(
297 |
298 |
299 |
300 | );
301 |
302 | const htmlTag = document.documentElement;
303 |
304 | expect(htmlTag).not.toHaveAttribute('amp');
305 | expect(htmlTag).not.toHaveAttribute('lang');
306 | expect(htmlTag).toHaveAttribute('id', 'html-tag');
307 | expect(htmlTag).toHaveAttribute('title', 'html tag');
308 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'id,title');
309 | });
310 |
311 | describe('initialized outside of helmet', () => {
312 | beforeEach(() => {
313 | const htmlTag = document.documentElement;
314 | htmlTag.setAttribute('test', 'test');
315 | });
316 |
317 | it('are not cleared', () => {
318 | render( );
319 |
320 | const htmlTag = document.documentElement;
321 |
322 | expect(htmlTag).toHaveAttribute('test', 'test');
323 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
324 | });
325 |
326 | it('overwritten if specified in helmet', () => {
327 | render(
328 |
329 |
333 |
334 | );
335 |
336 | const htmlTag = document.documentElement;
337 |
338 | expect(htmlTag).toHaveAttribute('test', 'helmet-attr');
339 | expect(htmlTag).toHaveAttribute(HELMET_ATTRIBUTE, 'test');
340 | });
341 |
342 | it('cleared once it is managed in helmet', () => {
343 | render(
344 |
345 |
349 |
350 | );
351 |
352 | render( );
353 |
354 | const htmlTag = document.documentElement;
355 |
356 | expect(htmlTag).not.toHaveAttribute('test');
357 | expect(htmlTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
358 | });
359 | });
360 | });
361 | });
362 |
--------------------------------------------------------------------------------
/__tests__/api/noscript.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('noscript tags', () => {
10 | describe('API', () => {
11 | it('updates noscript tags', () => {
12 | render(
13 | `,
18 | },
19 | ]}
20 | />
21 | );
22 |
23 | const existingTags = [...document.head.getElementsByTagName('noscript')];
24 | const [firstTag] = existingTags;
25 |
26 | expect(existingTags).toBeDefined();
27 | expect(existingTags).toHaveLength(1);
28 | expect(firstTag.id).toBe('bar');
29 | expect(firstTag.outerHTML).toMatchSnapshot();
30 | });
31 |
32 | it('clears all noscripts tags if none are specified', () => {
33 | render( );
34 |
35 | render( );
36 |
37 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
38 |
39 | expect(existingTags).toBeDefined();
40 | expect(existingTags).toHaveLength(0);
41 | });
42 |
43 | it("tags without 'innerHTML' are not accepted", () => {
44 | render( );
45 |
46 | const existingTags = document.head.querySelectorAll(`noscript[${HELMET_ATTRIBUTE}]`);
47 |
48 | expect(existingTags).toBeDefined();
49 | expect(existingTags).toHaveLength(0);
50 | });
51 |
52 | it('does not render tag when primary attribute is null', () => {
53 | render(
54 |
62 | );
63 |
64 | const tagNodes = document.head.querySelectorAll(`noscript[${HELMET_ATTRIBUTE}]`);
65 | const existingTags = [].slice.call(tagNodes);
66 |
67 | expect(existingTags).toHaveLength(0);
68 | });
69 | });
70 |
71 | describe('Declarative API', () => {
72 | it('updates noscript tags', () => {
73 | render(
74 |
75 | {` `}
76 |
77 | );
78 |
79 | const existingTags = [...document.head.getElementsByTagName('noscript')];
80 | const [firstTag] = existingTags;
81 |
82 | expect(existingTags).toBeDefined();
83 | expect(existingTags).toHaveLength(1);
84 | expect(firstTag.id).toBe('bar');
85 | expect(firstTag.outerHTML).toMatchSnapshot();
86 | });
87 |
88 | it('clears all noscripts tags if none are specified', () => {
89 | render(
90 |
91 |
92 |
93 | );
94 |
95 | render( );
96 |
97 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
98 |
99 | expect(existingTags).toBeDefined();
100 | expect(existingTags).toHaveLength(0);
101 | });
102 |
103 | it("tags without 'innerHTML' are not accepted", () => {
104 | render(
105 |
106 |
107 |
108 | );
109 |
110 | const existingTags = document.head.querySelectorAll(`noscript[${HELMET_ATTRIBUTE}]`);
111 |
112 | expect(existingTags).toBeDefined();
113 | expect(existingTags).toHaveLength(0);
114 | });
115 |
116 | it('does not render tag when primary attribute is null', () => {
117 | render(
118 |
119 | {undefined}
120 |
121 | );
122 |
123 | const tagNodes = document.head.querySelectorAll(`noscript[${HELMET_ATTRIBUTE}]`);
124 | const existingTags = [].slice.call(tagNodes);
125 |
126 | expect(existingTags).toHaveLength(0);
127 | });
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/__tests__/api/script.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('script tags', () => {
10 | describe('API', () => {
11 | it('updates script tags', () => {
12 | const scriptInnerHTML = `
13 | {
14 | "@context": "http://schema.org",
15 | "@type": "NewsArticle",
16 | "url": "http://localhost/helmet"
17 | }
18 | `;
19 | render(
20 |
36 | );
37 |
38 | const existingTags = [...document.head.getElementsByTagName('script')];
39 |
40 | expect(existingTags).toBeDefined();
41 |
42 | const filteredTags = existingTags.filter(
43 | tag =>
44 | (tag.getAttribute('src') === 'http://localhost/test.js' &&
45 | tag.getAttribute('type') === 'text/javascript') ||
46 | (tag.getAttribute('src') === 'http://localhost/test2.js' &&
47 | tag.getAttribute('type') === 'text/javascript') ||
48 | (tag.getAttribute('type') === 'application/ld+json' && tag.innerHTML === scriptInnerHTML)
49 | );
50 |
51 | expect(filteredTags.length).toBeGreaterThanOrEqual(3);
52 | });
53 |
54 | it('clears all scripts tags if none are specified', () => {
55 | render(
56 |
64 | );
65 |
66 | render( );
67 |
68 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
69 |
70 | expect(existingTags).toBeDefined();
71 | expect(existingTags).toHaveLength(0);
72 | });
73 |
74 | it("tags without 'src' are not accepted", () => {
75 | render( );
76 |
77 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
78 |
79 | expect(existingTags).toBeDefined();
80 | expect(existingTags).toHaveLength(0);
81 | });
82 |
83 | it('sets script tags based on deepest nested component', () => {
84 | render(
85 |
86 |
94 |
102 |
103 | );
104 |
105 | const existingTags = [...document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`)];
106 | const [firstTag, secondTag] = existingTags;
107 |
108 | expect(existingTags).toBeDefined();
109 | expect(existingTags.length).toBeGreaterThanOrEqual(2);
110 |
111 | expect(firstTag).toBeInstanceOf(Element);
112 | expect(firstTag.getAttribute).toBeDefined();
113 | expect(firstTag).toHaveAttribute('src', 'http://localhost/test.js');
114 | expect(firstTag).toHaveAttribute('type', 'text/javascript');
115 | expect(firstTag.outerHTML).toMatchSnapshot();
116 |
117 | expect(secondTag).toBeInstanceOf(Element);
118 | expect(secondTag.getAttribute).toBeDefined();
119 | expect(secondTag).toHaveAttribute('src', 'http://localhost/test2.js');
120 | expect(secondTag).toHaveAttribute('type', 'text/javascript');
121 | expect(secondTag.outerHTML).toMatchSnapshot();
122 | });
123 |
124 | it('sets undefined attribute values to empty strings', () => {
125 | render(
126 |
135 | );
136 |
137 | const existingTag = document.head.querySelector(`script[${HELMET_ATTRIBUTE}]`) as Element;
138 |
139 | expect(existingTag).toBeDefined();
140 | expect(existingTag.outerHTML).toMatchSnapshot();
141 | });
142 |
143 | it('does not render tag when primary attribute (src) is null', () => {
144 | render(
145 |
154 | );
155 |
156 | const tagNodes = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
157 | const existingTags = [].slice.call(tagNodes);
158 |
159 | expect(existingTags).toHaveLength(0);
160 | });
161 |
162 | it('does not render tag when primary attribute (innerHTML) is null', () => {
163 | render(
164 |
172 | );
173 |
174 | const tagNodes = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
175 | const existingTags = [].slice.call(tagNodes);
176 |
177 | expect(existingTags).toHaveLength(0);
178 | });
179 | });
180 |
181 | describe('Declarative API', () => {
182 | it('updates script tags', () => {
183 | const scriptInnerHTML = `
184 | {
185 | "@context": "http://schema.org",
186 | "@type": "NewsArticle",
187 | "url": "http://localhost/helmet"
188 | }
189 | `;
190 | render(
191 |
192 |
193 |
194 |
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 |
218 |
219 | );
220 |
221 | render( );
222 |
223 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
224 |
225 | expect(existingTags).toBeDefined();
226 | expect(existingTags).toHaveLength(0);
227 | });
228 |
229 | it("tags without 'src' are not accepted", () => {
230 | render(
231 |
232 |
233 |
234 | );
235 |
236 | const existingTags = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
237 |
238 | expect(existingTags).toBeDefined();
239 | expect(existingTags).toHaveLength(0);
240 | });
241 |
242 | it('sets script tags based on deepest nested component', () => {
243 | render(
244 |
245 |
246 |
247 |
248 |
249 |
250 | );
251 |
252 | const existingTags = [...document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`)];
253 | const [firstTag, secondTag] = existingTags;
254 |
255 | expect(existingTags).toBeDefined();
256 | expect(existingTags.length).toBeGreaterThanOrEqual(2);
257 |
258 | expect(firstTag).toBeInstanceOf(Element);
259 | expect(firstTag.getAttribute).toBeDefined();
260 | expect(firstTag).toHaveAttribute('src', 'http://localhost/test.js');
261 | expect(firstTag).toHaveAttribute('type', 'text/javascript');
262 | expect(firstTag.outerHTML).toMatchSnapshot();
263 |
264 | expect(secondTag).toBeInstanceOf(Element);
265 | expect(secondTag.getAttribute).toBeDefined();
266 | expect(secondTag).toHaveAttribute('src', 'http://localhost/test2.js');
267 | expect(secondTag).toHaveAttribute('type', 'text/javascript');
268 | expect(secondTag.outerHTML).toMatchSnapshot();
269 | });
270 |
271 | it('sets undefined attribute values to empty strings', () => {
272 | render(
273 |
274 |
275 |
276 | );
277 |
278 | const existingTag = document.head.querySelector(`script[${HELMET_ATTRIBUTE}]`) as Element;
279 |
280 | expect(existingTag).toBeDefined();
281 | expect(existingTag.outerHTML).toMatchSnapshot();
282 | });
283 |
284 | it('does not render tag when primary attribute (src) is null', () => {
285 | render(
286 |
287 |
288 |
289 | );
290 |
291 | const tagNodes = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
292 | const existingTags = [].slice.call(tagNodes);
293 |
294 | expect(existingTags).toHaveLength(0);
295 | });
296 |
297 | it('does not render tag when primary attribute (innerHTML) is null', () => {
298 | render(
299 |
300 |
304 |
305 | );
306 |
307 | const tagNodes = document.head.querySelectorAll(`script[${HELMET_ATTRIBUTE}]`);
308 | const existingTags = [].slice.call(tagNodes);
309 |
310 | expect(existingTags).toHaveLength(0);
311 | });
312 | });
313 | });
314 |
--------------------------------------------------------------------------------
/__tests__/api/style.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('style tags', () => {
10 | it('updates style tags', () => {
11 | const cssText1 = `
12 | body {
13 | background-color: green;
14 | }
15 | `;
16 | const cssText2 = `
17 | p {
18 | font-size: 12px;
19 | }
20 | `;
21 | render(
22 |
33 | );
34 |
35 | const existingTags = [...document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`)];
36 | const [firstTag, secondTag] = existingTags;
37 |
38 | expect(existingTags).toBeDefined();
39 | expect(existingTags).toHaveLength(2);
40 |
41 | expect(firstTag).toBeInstanceOf(Element);
42 | expect(firstTag.getAttribute).toBeDefined();
43 | expect(firstTag).toHaveAttribute('type', 'text/css');
44 | expect(firstTag.innerHTML).toEqual(cssText1);
45 | expect(firstTag.outerHTML).toMatchSnapshot();
46 |
47 | expect(secondTag).toBeInstanceOf(Element);
48 | expect(secondTag.innerHTML).toEqual(cssText2);
49 | expect(secondTag.outerHTML).toMatchSnapshot();
50 | });
51 |
52 | it('clears all style tags if none are specified', () => {
53 | const cssText = `
54 | body {
55 | background-color: green;
56 | }
57 | `;
58 | render(
59 |
67 | );
68 |
69 | render( );
70 |
71 | const existingTags = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
72 |
73 | expect(existingTags).toBeDefined();
74 | expect(existingTags).toHaveLength(0);
75 | });
76 |
77 | it("tags without 'cssText' are not accepted", () => {
78 | render( );
79 |
80 | const existingTags = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
81 |
82 | expect(existingTags).toBeDefined();
83 | expect(existingTags).toHaveLength(0);
84 | });
85 |
86 | it('does not render tag when primary attribute is null', () => {
87 | render(
88 |
96 | );
97 |
98 | const tagNodes = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
99 | const existingTags = [].slice.call(tagNodes);
100 |
101 | expect(existingTags).toHaveLength(0);
102 | });
103 | });
104 |
105 | describe('Declarative API', () => {
106 | it('updates style tags', () => {
107 | const cssText1 = `
108 | body {
109 | background-color: green;
110 | }
111 | `;
112 | const cssText2 = `
113 | p {
114 | font-size: 12px;
115 | }
116 | `;
117 |
118 | render(
119 |
120 |
121 |
122 |
123 | );
124 |
125 | const existingTags = [...document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`)];
126 | const [firstTag, secondTag] = existingTags;
127 |
128 | expect(existingTags).toBeDefined();
129 | expect(existingTags).toHaveLength(2);
130 |
131 | expect(firstTag).toBeInstanceOf(Element);
132 | expect(firstTag.getAttribute).toBeDefined();
133 | expect(firstTag).toHaveAttribute('type', 'text/css');
134 | expect(firstTag.innerHTML).toEqual(cssText1);
135 | expect(firstTag.outerHTML).toMatchSnapshot();
136 |
137 | expect(secondTag).toBeInstanceOf(Element);
138 | expect(secondTag.innerHTML).toEqual(cssText2);
139 | expect(secondTag.outerHTML).toMatchSnapshot();
140 | });
141 |
142 | it('clears all style tags if none are specified', () => {
143 | const cssText = `
144 | body {
145 | background-color: green;
146 | }
147 | `;
148 | render(
149 |
150 |
151 |
152 | );
153 |
154 | render( );
155 |
156 | const existingTags = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
157 |
158 | expect(existingTags).toBeDefined();
159 | expect(existingTags).toHaveLength(0);
160 | });
161 |
162 | it("tags without 'cssText' are not accepted", () => {
163 | render(
164 |
165 |
166 |
167 | );
168 |
169 | const existingTags = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
170 |
171 | expect(existingTags).toBeDefined();
172 | expect(existingTags).toHaveLength(0);
173 | });
174 |
175 | it('does not render tag when primary attribute is null', () => {
176 | render(
177 |
178 |
179 |
180 | );
181 |
182 | const tagNodes = document.head.querySelectorAll(`style[${HELMET_ATTRIBUTE}]`);
183 | const existingTags = [].slice.call(tagNodes);
184 |
185 | expect(existingTags).toHaveLength(0);
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/__tests__/api/title.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('title', () => {
9 | describe('API', () => {
10 | it('updates page title', () => {
11 | render( );
12 |
13 | expect(document.title).toMatchSnapshot();
14 | });
15 |
16 | it('updates page title with multiple children', () => {
17 | render(
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | expect(document.title).toMatchSnapshot();
26 | });
27 |
28 | it('sets title based on deepest nested component', () => {
29 | render(
30 |
31 |
32 |
33 |
34 | );
35 |
36 | expect(document.title).toMatchSnapshot();
37 | });
38 |
39 | it('sets title using deepest nested component with a defined title', () => {
40 | render(
41 |
42 |
43 |
44 |
45 | );
46 |
47 | expect(document.title).toMatchSnapshot();
48 | });
49 |
50 | it('uses defaultTitle if no title is defined', () => {
51 | render(
52 |
57 | );
58 |
59 | expect(document.title).toMatchSnapshot();
60 | });
61 |
62 | it('uses a titleTemplate if defined', () => {
63 | render(
64 |
69 | );
70 |
71 | expect(document.title).toMatchSnapshot();
72 | });
73 |
74 | it('replaces multiple title strings in titleTemplate', () => {
75 | render(
76 |
80 | );
81 |
82 | expect(document.title).toMatchSnapshot();
83 | });
84 |
85 | it('uses a titleTemplate based on deepest nested component', () => {
86 | render(
87 |
88 |
89 |
90 |
91 | );
92 |
93 | expect(document.title).toMatchSnapshot();
94 | });
95 |
96 | it('merges deepest component title with nearest upstream titleTemplate', () => {
97 | render(
98 |
99 |
100 |
101 |
102 | );
103 |
104 | expect(document.title).toMatchSnapshot();
105 | });
106 |
107 | it('renders dollar characters in a title correctly when titleTemplate present', () => {
108 | const dollarTitle = 'te$t te$$t te$$$t te$$$$t';
109 |
110 | render( );
111 |
112 | expect(document.title).toMatchSnapshot();
113 | });
114 |
115 | it('does not encode all characters with HTML character entity equivalents', () => {
116 | const chineseTitle = '膣膗 鍆錌雔';
117 |
118 | render(
119 |
120 |
121 |
122 | );
123 |
124 | expect(document.title).toMatchSnapshot();
125 | });
126 |
127 | it('page title with prop itemprop', () => {
128 | render(
129 |
134 | );
135 |
136 | const titleTag = document.getElementsByTagName('title')[0];
137 |
138 | expect(document.title).toMatchSnapshot();
139 | expect(titleTag).toHaveAttribute('itemprop', 'name');
140 | });
141 | });
142 |
143 | describe('Declarative API', () => {
144 | it('updates page title', () => {
145 | render(
146 |
147 | Test Title
148 |
149 | );
150 |
151 | expect(document.title).toMatchSnapshot();
152 | });
153 |
154 | it('updates page title and allows children containing expressions', () => {
155 | const someValue = 'Some Great Title';
156 |
157 | render(
158 |
159 | Title: {someValue}
160 |
161 | );
162 |
163 | expect(document.title).toMatchSnapshot();
164 | });
165 |
166 | it('updates page title with multiple children', () => {
167 | render(
168 |
169 |
170 | Test Title
171 |
172 |
173 | Child One Title
174 |
175 |
176 | Child Two Title
177 |
178 |
179 | );
180 |
181 | expect(document.title).toMatchSnapshot();
182 | });
183 |
184 | it('sets title based on deepest nested component', () => {
185 | render(
186 |
187 |
188 | Main Title
189 |
190 |
191 | Nested Title
192 |
193 |
194 | );
195 |
196 | expect(document.title).toMatchSnapshot();
197 | });
198 |
199 | it('sets title using deepest nested component with a defined title', () => {
200 | render(
201 |
202 |
203 | Main Title
204 |
205 |
206 |
207 | );
208 |
209 | expect(document.title).toMatchSnapshot();
210 | });
211 |
212 | it('uses defaultTitle if no title is defined', () => {
213 | render(
214 |
215 |
216 |
217 | );
218 |
219 | expect(document.title).toMatchSnapshot();
220 | });
221 |
222 | it('uses a titleTemplate if defined', () => {
223 | render(
224 |
225 | Test
226 |
227 | );
228 |
229 | expect(document.title).toMatchSnapshot();
230 | });
231 |
232 | it('replaces multiple title strings in titleTemplate', () => {
233 | render(
234 |
235 | Test
236 |
237 | );
238 |
239 | expect(document.title).toMatchSnapshot();
240 | });
241 |
242 | it('uses a titleTemplate based on deepest nested component', () => {
243 | render(
244 |
245 |
246 | Test
247 |
248 |
249 | Second Test
250 |
251 |
252 | );
253 |
254 | expect(document.title).toMatchSnapshot();
255 | });
256 |
257 | it('merges deepest component title with nearest upstream titleTemplate', () => {
258 | render(
259 |
260 |
261 | Test
262 |
263 |
264 | Second Test
265 |
266 |
267 | );
268 |
269 | expect(document.title).toMatchSnapshot();
270 | });
271 |
272 | it('renders dollar characters in a title correctly when titleTemplate present', () => {
273 | const dollarTitle = 'te$t te$$t te$$$t te$$$$t';
274 |
275 | render(
276 |
277 | {dollarTitle}
278 |
279 | );
280 |
281 | expect(document.title).toMatchSnapshot();
282 | });
283 |
284 | it('does not encode all characters with HTML character entity equivalents', () => {
285 | const chineseTitle = '膣膗 鍆錌雔';
286 |
287 | render(
288 |
289 | {chineseTitle}
290 |
291 | );
292 |
293 | expect(document.title).toMatchSnapshot();
294 | });
295 |
296 | it('page title with prop itemProp', () => {
297 | render(
298 |
299 | Test Title with itemProp
300 |
301 | );
302 |
303 | const titleTag = document.getElementsByTagName('title')[0];
304 |
305 | expect(document.title).toMatchSnapshot();
306 | expect(titleTag).toHaveAttribute('itemprop', 'name');
307 | });
308 |
309 | it('retains existing title tag when no title tag is defined', () => {
310 | document.head.innerHTML = `Existing Title `;
311 |
312 | render(
313 |
314 |
315 |
316 | );
317 |
318 | expect(document.title).toMatchSnapshot();
319 | });
320 |
321 | it.skip('clears title tag if empty title is defined', () => {
322 | render(
323 |
324 | Existing Title
325 |
326 |
327 | );
328 |
329 | expect(document.title).toMatchSnapshot();
330 |
331 | render(
332 |
333 |
334 |
335 |
336 | );
337 |
338 | expect(document.title).toBe('');
339 | });
340 | });
341 | });
342 |
--------------------------------------------------------------------------------
/__tests__/api/titleAttributes.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('title attributes', () => {
10 | beforeEach(() => {
11 | document.head.innerHTML = `Test Title `;
12 | });
13 |
14 | describe('API', () => {
15 | it('update title attributes', () => {
16 | render(
17 |
22 | );
23 |
24 | const titleTag = document.getElementsByTagName('title')[0];
25 |
26 | expect(titleTag).toHaveAttribute('itemprop', 'name');
27 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'itemprop');
28 | });
29 |
30 | it('sets attributes based on the deepest nested component', () => {
31 | render(
32 |
33 |
40 |
45 |
46 | );
47 |
48 | const titleTag = document.getElementsByTagName('title')[0];
49 |
50 | expect(titleTag).toHaveAttribute('lang', 'ja');
51 | expect(titleTag).toHaveAttribute('hidden', '');
52 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang,hidden');
53 | });
54 |
55 | it('handles valueless attributes', () => {
56 | render(
57 |
63 | );
64 |
65 | const titleTag = document.getElementsByTagName('title')[0];
66 |
67 | expect(titleTag).toHaveAttribute('hidden', '');
68 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'hidden');
69 | });
70 |
71 | it('clears title attributes that are handled within helmet', () => {
72 | render(
73 |
80 | );
81 |
82 | render( );
83 |
84 | const titleTag = document.getElementsByTagName('title')[0];
85 |
86 | expect(titleTag).not.toHaveAttribute('lang');
87 | expect(titleTag).not.toHaveAttribute('hidden');
88 | expect(titleTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
89 | });
90 | });
91 |
92 | describe('Declarative API', () => {
93 | it('updates title attributes', () => {
94 | render(
95 |
96 |
97 |
98 | );
99 |
100 | const titleTag = document.getElementsByTagName('title')[0];
101 |
102 | expect(titleTag).toHaveAttribute('itemprop', 'name');
103 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'itemprop');
104 | });
105 |
106 | it('sets attributes based on the deepest nested component', () => {
107 | render(
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 |
118 | const titleTag = document.getElementsByTagName('title')[0];
119 |
120 | expect(titleTag).toHaveAttribute('lang', 'ja');
121 | expect(titleTag).toHaveAttribute('hidden', 'true');
122 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'lang,hidden');
123 | });
124 |
125 | it('handles valueless attributes', () => {
126 | render(
127 |
128 |
129 |
130 | );
131 |
132 | const titleTag = document.getElementsByTagName('title')[0];
133 |
134 | expect(titleTag).toHaveAttribute('hidden', 'true');
135 | expect(titleTag).toHaveAttribute(HELMET_ATTRIBUTE, 'hidden');
136 | });
137 |
138 | it('clears title attributes that are handled within helmet', () => {
139 | render(
140 |
141 |
142 |
143 | );
144 |
145 | render( );
146 |
147 | const titleTag = document.getElementsByTagName('title')[0];
148 |
149 | expect(titleTag).not.toHaveAttribute('lang');
150 | expect(titleTag).not.toHaveAttribute('hidden');
151 | expect(titleTag).not.toHaveAttribute(HELMET_ATTRIBUTE);
152 | });
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/__tests__/deferred.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { MockedFunction } from 'vitest';
3 |
4 | import { Helmet } from '../src';
5 |
6 | import { render } from './utils';
7 | import './window';
8 |
9 | declare global {
10 | interface Window {
11 | __spy__: MockedFunction;
12 | }
13 | }
14 |
15 | describe.skip('deferred tags', () => {
16 | beforeEach(() => {
17 | Object.defineProperty(window, '__spy__', {
18 | configurable: true,
19 | value: vi.fn(() => {}),
20 | });
21 | });
22 |
23 | afterEach(() => {
24 | // @ts-ignore
25 | delete window.__spy__;
26 | });
27 |
28 | describe('API', () => {
29 | it('executes synchronously when defer={true} and async otherwise', async () => {
30 | render(
31 |
32 |
40 |
47 |
48 | );
49 |
50 | expect(window.__spy__).toHaveBeenCalledTimes(1);
51 |
52 | await vi.waitFor(
53 | () =>
54 | new Promise(resolve => {
55 | requestAnimationFrame(() => {
56 | // @ts-ignore
57 | expect(window.__spy__).toHaveBeenCalledTimes(2);
58 | // @ts-ignore
59 | expect(window.__spy__.mock.calls).toStrictEqual([[1], [2]]);
60 |
61 | resolve(true);
62 | });
63 | })
64 | );
65 | });
66 | });
67 |
68 | describe('Declarative API', () => {
69 | it('executes synchronously when defer={true} and async otherwise', async () => {
70 | render(
71 |
72 |
73 |
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(