diff --git a/.babelrc b/.babelrc deleted file mode 100644 index a43c1889..00000000 --- a/.babelrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "presets": [ - ["kyt-react", {"modules": true}], - ], - "plugins": [ - "transform-class-properties", - "transform-es2015-modules-commonjs", - "transform-object-rest-spread", - "transform-regenerator", - ] -} diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..75678ea8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,31 @@ +version: 2 +jobs: + build: + docker: + - image: cimg/node:18.17.1 + + steps: + - checkout + + - restore_cache: + keys: + - deps-{{ checksum "package.json" }} + + - run: yarn + + - save_cache: + paths: + - node_modules + key: deps-{{ checksum "package.json" }} + + - run: + name: Tests + command: yarn test + + - run: + name: Type checks + command: yarn tsc + + - run: + name: ESLint + command: yarn lint diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..a1d158e5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/lib +/node_modules +package.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..97a8b2c2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,36 @@ +/** + * @type {import('@types/eslint').Linter.BaseConfig} + */ +module.exports = { + extends: [ + '@remix-run/eslint-config', + '@remix-run/eslint-config/node', + '@remix-run/eslint-config/jest-testing-library', + 'prettier', + ], + plugins: ['prettier'], + rules: { + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + }, + ], + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'es5', + useTabs: false, + tabWidth: 2, + printWidth: 100, + }, + ], + 'testing-library/render-result-naming-convention': 'off', + }, + settings: { + jest: { + version: 27, + }, + }, +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 03c1371e..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "eslint-config-kyt" - ] -} diff --git a/.gitignore b/.gitignore index f39528b3..3063f07d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -es lib node_modules diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..2785bc1d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +yarn commitlint --edit $1 diff --git a/.npmignore b/.npmignore index 876e5a28..fe00227a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ -.babelrc -.eslintrc.json +.eslintrc.js jest.setup.js +*.config.js node_modules src __tests__ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..4a1f488b --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.17.1 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..09ddce2b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +/lib +/es +/coverage +/node_modules +package.json +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..54983dda --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid", + "printWidth": 100, + "useTabs": false, + "tabWidth": 2 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..17201341 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..aa7a7d51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 The New York Times Company + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 597559e7..cac92ba9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ # react-helmet-async +[![CircleCI](https://circleci.com/gh/staylor/react-helmet-async.svg?style=svg)](https://circleci.com/gh/staylor/react-helmet-async) + +[Announcement post on Times Open blog](https://open.nytimes.com/the-future-of-meta-tag-management-for-modern-react-development-ec26a7dc9183) + This package is a fork of [React Helmet](https://github.com/nfl/react-helmet). -`` usage is synonymous, but SSR is radically different. +`` usage is synonymous, but server and client now requires `` to encapsulate state per request. -`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 to encapsulate data on a per-request basis, this package does just that. +`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. ## Usage -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: +**New is 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'` + +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: ```javascript -import Helmet, { HelmetProvider } from 'react-helmet-async'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Helmet, HelmetProvider } from 'react-helmet-async'; const app = ( @@ -25,6 +31,11 @@ const app = ( ); + +ReactDOM.hydrate( + app, + document.getElementById(‘app’) +); ``` On the server, we will no longer use static methods to extract state. `react-side-effect` @@ -32,7 +43,9 @@ exposed a `.rewind()` method, which Helmet used when calling `Helmet.renderStati to pass a `context` prop to `HelmetProvider`, which will hold our state specific to each request. ```javascript -import { HelmetProvider } from 'react-helmet-async'; +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import { Helmet, HelmetProvider } from 'react-helmet-async'; const helmetContext = {}; @@ -48,9 +61,11 @@ const app = ( ); +const html = renderToString(app); + const { helmet } = helmetContext; -// helmet.title.toString() etc... +// helmet.title.toString() etc… ``` ## Streams @@ -62,7 +77,7 @@ This is possible if your data hydration method already parses your React tree. E import through from 'through'; import { renderToNodeStream } from 'react-dom/server'; import { getDataFromTree } from 'react-apollo'; -import Helmet, { HelmetProvider } from 'react-helmet-async'; +import { Helmet, HelmetProvider } from 'react-helmet-async'; import template from 'server/template'; const helmetContext = {}; @@ -102,6 +117,88 @@ renderToNodeStream(app) .pipe(res); ``` +## Usage in Jest +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. + +```javascript +import { HelmetProvider } from 'react-helmet-async'; + +HelmetProvider.canUseDOM = false; +``` + +## Prioritizing tags for SEO + +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. + +In the component: +```javascript + + A fancy webpage + + + + + +``` + +In your server template: + +```javascript + + + ${helmet.title.toString()} + ${helmet.priority.toString()} + ${helmet.meta.toString()} + ${helmet.link.toString()} + ${helmet.script.toString()} + + ... + +``` + +Will result in: + +```html + + + A fancy webpage + + + + + + ... + +``` + +A list of prioritized tags and attributes can be found in [constants.ts](./src/constants.ts). + +## Usage without Context +You can optionally use `` outside a context by manually creating a stateful `HelmetData` instance, and passing that stateful object to each `` instance: + + +```js +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import { Helmet, HelmetProvider, HelmetData } from 'react-helmet-async'; + +const helmetData = new HelmetData({}); + +const app = ( + + + Hello World + + +

Hello World

+
+); + +const html = renderToString(app); + +const { helmet } = helmetData.context; +``` + ## License -Licensed under the MIT License, Copyright © 2018 Scott Taylor +Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor diff --git a/__tests__/__snapshots__/fragment.test.js.snap b/__tests__/__snapshots__/fragment.test.js.snap new file mode 100644 index 00000000..c6f21889 --- /dev/null +++ b/__tests__/__snapshots__/fragment.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fragments parses Fragments 1`] = `"Hello"`; + +exports[`fragments parses nested Fragments 1`] = `"Baz"`; diff --git a/__tests__/__snapshots__/misc.test.tsx.snap b/__tests__/__snapshots__/misc.test.tsx.snap new file mode 100644 index 00000000..a88069ad --- /dev/null +++ b/__tests__/__snapshots__/misc.test.tsx.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`misc > API > encodes special characters 1`] = `""`; + +exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; + +exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; + +exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; + +exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; + +exports[`misc > API > only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; + +exports[`misc > API > recognizes valid tags regardless of attribute ordering 1`] = `""`; + +exports[`misc > Declarative API > encodes special characters 1`] = `""`; + +exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 1`] = `""`; + +exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 2`] = `""`; + +exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 3`] = `""`; + +exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 4`] = `""`; + +exports[`misc > Declarative API > only adds new tags and preserves tags when rendering additional Helmet instances 5`] = `""`; + +exports[`misc > Declarative API > recognizes valid tags regardless of attribute ordering 1`] = `""`; diff --git a/__tests__/api/__snapshots__/base.test.js.snap b/__tests__/api/__snapshots__/base.test.js.snap deleted file mode 100644 index 76328f55..00000000 --- a/__tests__/api/__snapshots__/base.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`base tag API sets base tag based on deepest nested component 1`] = `""`; - -exports[`base tag Declarative API sets base tag based on deepest nested component 1`] = `""`; diff --git a/__tests__/api/__snapshots__/base.test.tsx.snap b/__tests__/api/__snapshots__/base.test.tsx.snap new file mode 100644 index 00000000..d9681db6 --- /dev/null +++ b/__tests__/api/__snapshots__/base.test.tsx.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`base tag > API > sets base tag based on deepest nested component 1`] = `""`; + +exports[`base tag > Declarative API > sets base tag based on deepest nested component 1`] = `""`; diff --git a/__tests__/api/__snapshots__/client.test.js.snap b/__tests__/api/__snapshots__/client.test.js.snap deleted file mode 100644 index e5e6c3e8..00000000 --- a/__tests__/api/__snapshots__/client.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`onChangeClientState API when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; - -exports[`onChangeClientState API when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; - -exports[`onChangeClientState API when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; - -exports[`onChangeClientState API when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; - -exports[`onChangeClientState Declarative API when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; - -exports[`onChangeClientState Declarative API when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; - -exports[`onChangeClientState Declarative API when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; - -exports[`onChangeClientState Declarative API when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; diff --git a/__tests__/api/__snapshots__/client.test.tsx.snap b/__tests__/api/__snapshots__/client.test.tsx.snap new file mode 100644 index 00000000..2642229d --- /dev/null +++ b/__tests__/api/__snapshots__/client.test.tsx.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; + +exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; + +exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; + +exports[`onChangeClientState > API > when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; + +exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 1`] = `""`; + +exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 2`] = `""`; + +exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 3`] = `""`; + +exports[`onChangeClientState > Declarative API > when handling client state change, calls the function with new state, addedTags and removedTags 4`] = `""`; diff --git a/__tests__/api/__snapshots__/link.test.js.snap b/__tests__/api/__snapshots__/link.test.js.snap deleted file mode 100644 index 84926dff..00000000 --- a/__tests__/api/__snapshots__/link.test.js.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`link tags API allows duplicate link tags if specified in the same component 1`] = `""`; - -exports[`link tags API allows duplicate link tags if specified in the same component 2`] = `""`; - -exports[`link tags API does not render tag when primary attribute is null 1`] = `""`; - -exports[`link tags API overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; - -exports[`link tags API overrides single link tag with duplicate link tags in a nested component 1`] = `""`; - -exports[`link tags API overrides single link tag with duplicate link tags in a nested component 2`] = `""`; - -exports[`link tags API sets link tags based on deepest nested component 1`] = `""`; - -exports[`link tags API sets link tags based on deepest nested component 2`] = `""`; - -exports[`link tags API sets link tags based on deepest nested component 3`] = `""`; - -exports[`link tags API tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; - -exports[`link tags API tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; - -exports[`link tags API tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; - -exports[`link tags Declarative API allows duplicate link tags if specified in the same component 1`] = `""`; - -exports[`link tags Declarative API allows duplicate link tags if specified in the same component 2`] = `""`; - -exports[`link tags Declarative API does not render tag when primary attribute is null 1`] = `""`; - -exports[`link tags Declarative API overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; - -exports[`link tags Declarative API overrides single link tag with duplicate link tags in a nested component 1`] = `""`; - -exports[`link tags Declarative API overrides single link tag with duplicate link tags in a nested component 2`] = `""`; - -exports[`link tags Declarative API sets link tags based on deepest nested component 1`] = `""`; - -exports[`link tags Declarative API sets link tags based on deepest nested component 2`] = `""`; - -exports[`link tags Declarative API sets link tags based on deepest nested component 3`] = `""`; - -exports[`link tags Declarative API tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; - -exports[`link tags Declarative API tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; - -exports[`link tags Declarative API tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; diff --git a/__tests__/api/__snapshots__/link.test.tsx.snap b/__tests__/api/__snapshots__/link.test.tsx.snap new file mode 100644 index 00000000..370b1aa9 --- /dev/null +++ b/__tests__/api/__snapshots__/link.test.tsx.snap @@ -0,0 +1,49 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`link tags > API > allows duplicate link tags if specified in the same component 1`] = `""`; + +exports[`link tags > API > allows duplicate link tags if specified in the same component 2`] = `""`; + +exports[`link tags > API > does not render tag when primary attribute is null 1`] = `""`; + +exports[`link tags > API > overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; + +exports[`link tags > API > overrides single link tag with duplicate link tags in a nested component 1`] = `""`; + +exports[`link tags > API > overrides single link tag with duplicate link tags in a nested component 2`] = `""`; + +exports[`link tags > API > sets link tags based on deepest nested component 1`] = `""`; + +exports[`link tags > API > sets link tags based on deepest nested component 2`] = `""`; + +exports[`link tags > API > sets link tags based on deepest nested component 3`] = `""`; + +exports[`link tags > API > tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; + +exports[`link tags > API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; + +exports[`link tags > API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; + +exports[`link tags > Declarative API > allows duplicate link tags if specified in the same component 1`] = `""`; + +exports[`link tags > Declarative API > allows duplicate link tags if specified in the same component 2`] = `""`; + +exports[`link tags > Declarative API > does not render tag when primary attribute is null 1`] = `""`; + +exports[`link tags > Declarative API > overrides duplicate link tags with a single link tag in a nested component 1`] = `""`; + +exports[`link tags > Declarative API > overrides single link tag with duplicate link tags in a nested component 1`] = `""`; + +exports[`link tags > Declarative API > overrides single link tag with duplicate link tags in a nested component 2`] = `""`; + +exports[`link tags > Declarative API > sets link tags based on deepest nested component 1`] = `""`; + +exports[`link tags > Declarative API > sets link tags based on deepest nested component 2`] = `""`; + +exports[`link tags > Declarative API > sets link tags based on deepest nested component 3`] = `""`; + +exports[`link tags > Declarative API > tags 'rel' and 'href' properly use 'rel' as the primary identification for this tag, regardless of ordering 1`] = `""`; + +exports[`link tags > Declarative API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 1`] = `""`; + +exports[`link tags > Declarative API > tags with rel='stylesheet' uses the href as the primary identification of the tag, regardless of ordering 2`] = `""`; diff --git a/__tests__/api/__snapshots__/meta.test.js.snap b/__tests__/api/__snapshots__/meta.test.js.snap deleted file mode 100644 index 93dba226..00000000 --- a/__tests__/api/__snapshots__/meta.test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`meta tags API allows duplicate meta tags if specified in the same component 1`] = `""`; - -exports[`meta tags API allows duplicate meta tags if specified in the same component 2`] = `""`; - -exports[`meta tags API fails gracefully when meta is wrong shape 1`] = `"Helmet: meta should be of type \\"Array\\". Instead found type \\"object\\""`; - -exports[`meta tags API overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; - -exports[`meta tags API overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; - -exports[`meta tags API overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; - -exports[`meta tags API sets meta tags based on deepest nested component 1`] = `""`; - -exports[`meta tags API sets meta tags based on deepest nested component 2`] = `""`; - -exports[`meta tags API sets meta tags based on deepest nested component 3`] = `""`; - -exports[`meta tags Declarative API allows duplicate meta tags if specified in the same component 1`] = `""`; - -exports[`meta tags Declarative API allows duplicate meta tags if specified in the same component 2`] = `""`; - -exports[`meta tags Declarative API overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; - -exports[`meta tags Declarative API overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; - -exports[`meta tags Declarative API overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; - -exports[`meta tags Declarative API sets meta tags based on deepest nested component 1`] = `""`; - -exports[`meta tags Declarative API sets meta tags based on deepest nested component 2`] = `""`; - -exports[`meta tags Declarative API sets meta tags based on deepest nested component 3`] = `""`; diff --git a/__tests__/api/__snapshots__/meta.test.tsx.snap b/__tests__/api/__snapshots__/meta.test.tsx.snap new file mode 100644 index 00000000..83e3f683 --- /dev/null +++ b/__tests__/api/__snapshots__/meta.test.tsx.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`meta tags > API > allows duplicate meta tags if specified in the same component 1`] = `""`; + +exports[`meta tags > API > allows duplicate meta tags if specified in the same component 2`] = `""`; + +exports[`meta tags > API > fails gracefully when meta is wrong shape 1`] = `"Helmet: meta should be of type \\"Array\\". Instead found type \\"object\\""`; + +exports[`meta tags > API > overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; + +exports[`meta tags > API > overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; + +exports[`meta tags > API > overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; + +exports[`meta tags > API > sets meta tags based on deepest nested component 1`] = `""`; + +exports[`meta tags > API > sets meta tags based on deepest nested component 2`] = `""`; + +exports[`meta tags > API > sets meta tags based on deepest nested component 3`] = `""`; + +exports[`meta tags > Declarative API > allows duplicate meta tags if specified in the same component 1`] = `""`; + +exports[`meta tags > Declarative API > allows duplicate meta tags if specified in the same component 2`] = `""`; + +exports[`meta tags > Declarative API > overrides duplicate meta tags with single meta tag in a nested component 1`] = `""`; + +exports[`meta tags > Declarative API > overrides single meta tag with duplicate meta tags in a nested component 1`] = `""`; + +exports[`meta tags > Declarative API > overrides single meta tag with duplicate meta tags in a nested component 2`] = `""`; + +exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 1`] = `""`; + +exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 2`] = `""`; + +exports[`meta tags > Declarative API > sets meta tags based on deepest nested component 3`] = `""`; diff --git a/__tests__/api/__snapshots__/noscript.test.js.snap b/__tests__/api/__snapshots__/noscript.test.js.snap deleted file mode 100644 index 9e1c59ae..00000000 --- a/__tests__/api/__snapshots__/noscript.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`noscript tags API updates noscript tags 1`] = `""`; - -exports[`noscript tags Declarative API updates noscript tags 1`] = `""`; diff --git a/__tests__/api/__snapshots__/noscript.test.tsx.snap b/__tests__/api/__snapshots__/noscript.test.tsx.snap new file mode 100644 index 00000000..d99ccac8 --- /dev/null +++ b/__tests__/api/__snapshots__/noscript.test.tsx.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`noscript tags > API > updates noscript tags 1`] = `""`; + +exports[`noscript tags > Declarative API > updates noscript tags 1`] = `""`; diff --git a/__tests__/api/__snapshots__/script.test.js.snap b/__tests__/api/__snapshots__/script.test.js.snap deleted file mode 100644 index 3e2e4a4a..00000000 --- a/__tests__/api/__snapshots__/script.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`script tags API sets script tags based on deepest nested component 1`] = `""`; - -exports[`script tags API sets script tags based on deepest nested component 2`] = `""`; - -exports[`script tags API sets undefined attribute values to empty strings 1`] = `""`; - -exports[`script tags Declarative API sets script tags based on deepest nested component 1`] = `""`; - -exports[`script tags Declarative API sets script tags based on deepest nested component 2`] = `""`; - -exports[`script tags Declarative API sets undefined attribute values to empty strings 1`] = `""`; diff --git a/__tests__/api/__snapshots__/script.test.tsx.snap b/__tests__/api/__snapshots__/script.test.tsx.snap new file mode 100644 index 00000000..72d0db70 --- /dev/null +++ b/__tests__/api/__snapshots__/script.test.tsx.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`script tags > API > sets script tags based on deepest nested component 1`] = `""`; + +exports[`script tags > API > sets script tags based on deepest nested component 2`] = `""`; + +exports[`script tags > API > sets undefined attribute values to empty strings 1`] = `""`; + +exports[`script tags > Declarative API > sets script tags based on deepest nested component 1`] = `""`; + +exports[`script tags > Declarative API > sets script tags based on deepest nested component 2`] = `""`; + +exports[`script tags > Declarative API > sets undefined attribute values to empty strings 1`] = `""`; diff --git a/__tests__/api/__snapshots__/style.test.js.snap b/__tests__/api/__snapshots__/style.test.tsx.snap similarity index 67% rename from __tests__/api/__snapshots__/style.test.js.snap rename to __tests__/api/__snapshots__/style.test.tsx.snap index d3d01962..f7fc5b2f 100644 --- a/__tests__/api/__snapshots__/style.test.js.snap +++ b/__tests__/api/__snapshots__/style.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Declarative API updates style tags 1`] = ` +exports[`Declarative API > updates style tags 1`] = ` "" `; -exports[`Declarative API updates style tags 2`] = ` +exports[`Declarative API > updates style tags 2`] = ` "" `; -exports[`style tags updates style tags 1`] = ` +exports[`style tags > updates style tags 1`] = ` "" `; -exports[`style tags updates style tags 2`] = ` +exports[`style tags > updates style tags 2`] = ` ""`; - -exports[`server API renders style tags as string 1`] = `""`; - -exports[`server Declarative API renders style tags as React components 1`] = `""`; - -exports[`server Declarative API renders style tags as string 1`] = `""`; diff --git a/__tests__/server/__snapshots__/style.test.tsx.snap b/__tests__/server/__snapshots__/style.test.tsx.snap new file mode 100644 index 00000000..4fb5a067 --- /dev/null +++ b/__tests__/server/__snapshots__/style.test.tsx.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`server > API > renders style tags as React components 1`] = `""`; + +exports[`server > API > renders style tags as string 1`] = `""`; + +exports[`server > Declarative API > renders style tags as React components 1`] = `""`; + +exports[`server > Declarative API > renders style tags as string 1`] = `""`; diff --git a/__tests__/server/__snapshots__/title.test.js.snap b/__tests__/server/__snapshots__/title.test.js.snap deleted file mode 100644 index a77924dc..00000000 --- a/__tests__/server/__snapshots__/title.test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`server API does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; - -exports[`server API encodes special characters in title 1`] = `"Dangerous <script> include"`; - -exports[`server API opts out of string encoding 1`] = `"This is text and & and '."`; - -exports[`server API renders title as React component 1`] = `"Dangerous <script> include"`; - -exports[`server API renders title tag as string 1`] = `"Dangerous <script> include"`; - -exports[`server API renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; - -exports[`server API renders title with itemprop name as string 1`] = `"Title with Itemprop"`; - -exports[`server Declarative API does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; - -exports[`server Declarative API encodes special characters in title 1`] = `"Dangerous <script> include"`; - -exports[`server Declarative API opts out of string encoding 1`] = `"This is text and & and '."`; - -exports[`server Declarative API renders title and allows children containing expressions 1`] = `"Title: Some Great Title"`; - -exports[`server Declarative API renders title as React component 1`] = `"Dangerous <script> include"`; - -exports[`server Declarative API renders title tag as string 1`] = `"Dangerous <script> include"`; - -exports[`server Declarative API renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; - -exports[`server Declarative API renders title with itemprop name as string 1`] = `"Title with Itemprop"`; - -exports[`server renderStatic does html encode title 1`] = `"Dangerous <script> include"`; - -exports[`server renderStatic renders title as React component 1`] = `"Dangerous <script> include"`; diff --git a/__tests__/server/__snapshots__/title.test.tsx.snap b/__tests__/server/__snapshots__/title.test.tsx.snap new file mode 100644 index 00000000..1a76da97 --- /dev/null +++ b/__tests__/server/__snapshots__/title.test.tsx.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`server > API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; + +exports[`server > API > encodes special characters in title 1`] = `"Dangerous <script> include"`; + +exports[`server > API > opts out of string encoding 1`] = `"This is text and & and '."`; + +exports[`server > API > renders title as React component 1`] = `"Dangerous <script> include"`; + +exports[`server > API > renders title tag as string 1`] = `"Dangerous <script> include"`; + +exports[`server > API > renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; + +exports[`server > API > renders title with itemprop name as string 1`] = `"Title with Itemprop"`; + +exports[`server > Declarative API > does not encode all characters with HTML character entity equivalents 1`] = `"膣膗 鍆錌雔"`; + +exports[`server > Declarative API > encodes special characters in title 1`] = `"Dangerous <script> include"`; + +exports[`server > Declarative API > opts out of string encoding 1`] = `"This is text and & and '."`; + +exports[`server > Declarative API > renders title and allows children containing expressions 1`] = `"Title: Some Great Title"`; + +exports[`server > Declarative API > renders title as React component 1`] = `"Dangerous <script> include"`; + +exports[`server > Declarative API > renders title tag as string 1`] = `"Dangerous <script> include"`; + +exports[`server > Declarative API > renders title with itemprop name as React component 1`] = `"Title with Itemprop"`; + +exports[`server > Declarative API > renders title with itemprop name as string 1`] = `"Title with Itemprop"`; + +exports[`server > renderStatic > does html encode title 1`] = `"Dangerous <script> include"`; + +exports[`server > renderStatic > renders title as React component 1`] = `"Dangerous <script> include"`; diff --git a/__tests__/server/base.test.js b/__tests__/server/base.test.tsx similarity index 67% rename from __tests__/server/base.test.js rename to __tests__/server/base.test.tsx index d0a5cbf9..af8b09af 100644 --- a/__tests__/server/base.test.js +++ b/__tests__/server/base.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext, isArray } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -20,16 +15,10 @@ afterAll(() => { Provider.canUseDOM = true; }); -const isArray = { - asymmetricMatch: actual => Array.isArray(actual), -}; - describe('server', () => { describe('API', () => { it('renders base tag as React component', () => { - const context = {}; - render(, context); - const head = context.helmet; + const head = renderContext(); expect(head.base).toBeDefined(); expect(head.base.toComponent).toBeDefined(); @@ -39,7 +28,7 @@ describe('server', () => { expect(baseComponent).toEqual(isArray); expect(baseComponent).toHaveLength(1); - baseComponent.forEach(base => { + baseComponent.forEach((base: Element) => { expect(base).toEqual(expect.objectContaining({ type: 'base' })); }); @@ -49,11 +38,7 @@ describe('server', () => { }); it('renders base tags as string', () => { - const context = {}; - render(, context); - - const head = context.helmet; - + const head = renderContext(); expect(head.base).toBeDefined(); expect(head.base.toString).toBeDefined(); expect(head.base.toString()).toMatchSnapshot(); @@ -62,16 +47,12 @@ describe('server', () => { describe('Declarative API', () => { it('renders base tag as React component', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.base).toBeDefined(); expect(head.base.toComponent).toBeDefined(); @@ -80,7 +61,7 @@ describe('server', () => { expect(baseComponent).toEqual(isArray); expect(baseComponent).toHaveLength(1); - baseComponent.forEach(base => { + baseComponent.forEach((base: Element) => { expect(base).toEqual(expect.objectContaining({ type: 'base' })); }); @@ -90,16 +71,12 @@ describe('server', () => { }); it('renders base tags as string', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.base).toBeDefined(); expect(head.base.toString).toBeDefined(); expect(head.base.toString()).toMatchSnapshot(); diff --git a/__tests__/server/bodyAttributes.test.js b/__tests__/server/bodyAttributes.test.tsx similarity index 64% rename from __tests__/server/bodyAttributes.test.js rename to __tests__/server/bodyAttributes.test.tsx index 394c9a14..a30e69ec 100644 --- a/__tests__/server/bodyAttributes.test.js +++ b/__tests__/server/bodyAttributes.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -23,16 +18,12 @@ afterAll(() => { describe('server', () => { describe('Declarative API', () => { it('renders body attributes as component', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - - const { bodyAttributes } = context.helmet; - const attrs = bodyAttributes.toComponent(); + const attrs = head.bodyAttributes.toComponent(); expect(attrs).toBeDefined(); @@ -42,16 +33,12 @@ describe('server', () => { }); it('renders body attributes as string', () => { - const context = {}; - render( + const body = renderContext( - , - context + ); - const body = context.helmet; - expect(body.bodyAttributes).toBeDefined(); expect(body.bodyAttributes.toString).toBeDefined(); expect(body.bodyAttributes.toString()).toMatchSnapshot(); diff --git a/__tests__/server/helmetData.test.tsx b/__tests__/server/helmetData.test.tsx new file mode 100644 index 00000000..0c2ec67f --- /dev/null +++ b/__tests__/server/helmetData.test.tsx @@ -0,0 +1,161 @@ +import React from 'react'; + +import { Helmet } from '../../src'; +import Provider from '../../src/Provider'; +import HelmetData from '../../src/HelmetData'; +import { HELMET_ATTRIBUTE } from '../../src/constants'; +import { render } from '../utils'; + +Helmet.defaultProps.defer = false; + +describe('Helmet Data', () => { + describe('server', () => { + beforeAll(() => { + Provider.canUseDOM = false; + }); + + afterAll(() => { + Provider.canUseDOM = true; + }); + + it('renders without context', () => { + const helmetData = new HelmetData({}); + + render( + + ); + + const head = helmetData.context.helmet; + + expect(head.base).toBeDefined(); + expect(head.base.toString).toBeDefined(); + expect(head.base.toString()).toMatchSnapshot(); + }); + + it('renders declarative without context', () => { + const helmetData = new HelmetData({}); + + render( + + + + ); + + const head = helmetData.context.helmet; + + expect(head.base).toBeDefined(); + expect(head.base.toString).toBeDefined(); + expect(head.base.toString()).toMatchSnapshot(); + }); + + it('sets base tag based on deepest nested component', () => { + const helmetData = new HelmetData({}); + + render( +
+ + + + + + +
+ ); + + const head = helmetData.context.helmet; + + expect(head.base).toBeDefined(); + expect(head.base.toString).toBeDefined(); + expect(head.base.toString()).toMatchSnapshot(); + }); + + it('works with the same context object but separate HelmetData instances', () => { + const context = {} as any; + + render( +
+ + + + + + +
+ ); + + const head = context.helmet; + + expect(head.base).toBeDefined(); + expect(head.base.toString).toBeDefined(); + expect(head.base.toString()).toMatchSnapshot(); + }); + }); + + describe('browser', () => { + it('renders without context', () => { + const helmetData = new HelmetData({}); + + render( + + ); + + const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; + const [firstTag] = existingTags; + + expect(existingTags).toBeDefined(); + expect(existingTags).toHaveLength(1); + + expect(firstTag).toBeInstanceOf(Element); + expect(firstTag.getAttribute).toBeDefined(); + expect(firstTag).toHaveAttribute('href', 'http://localhost/'); + expect(firstTag.outerHTML).toMatchSnapshot(); + }); + + it('renders declarative without context', () => { + const helmetData = new HelmetData({}); + + render( + + + + ); + + const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; + const [firstTag] = existingTags; + + expect(existingTags).toBeDefined(); + expect(existingTags).toHaveLength(1); + + expect(firstTag).toBeInstanceOf(Element); + expect(firstTag.getAttribute).toBeDefined(); + expect(firstTag).toHaveAttribute('href', 'http://localhost/'); + expect(firstTag.outerHTML).toMatchSnapshot(); + }); + + it('sets base tag based on deepest nested component', () => { + const helmetData = new HelmetData({}); + + render( +
+ + + + + + +
+ ); + + const existingTags = [...document.head.querySelectorAll(`base[${HELMET_ATTRIBUTE}]`)]; + const [firstTag] = existingTags; + + expect(existingTags).toBeDefined(); + expect(existingTags).toHaveLength(1); + + expect(firstTag).toBeInstanceOf(Element); + expect(firstTag.getAttribute).toBeDefined(); + expect(firstTag).toHaveAttribute('href', 'http://mysite.com/public'); + expect(firstTag.outerHTML).toMatchSnapshot(); + }); + }); +}); diff --git a/__tests__/server/htmlAttributes.test.js b/__tests__/server/htmlAttributes.test.tsx similarity index 68% rename from __tests__/server/htmlAttributes.test.js rename to __tests__/server/htmlAttributes.test.tsx index 8743c676..7cd87a9a 100644 --- a/__tests__/server/htmlAttributes.test.js +++ b/__tests__/server/htmlAttributes.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -23,19 +18,16 @@ afterAll(() => { describe('server', () => { describe('API', () => { it('renders html attributes as component', () => { - const context = {}; - render( + const head = renderContext( , - context + /> ); - const { htmlAttributes } = context.helmet; - const attrs = htmlAttributes.toComponent(); + const attrs = head.htmlAttributes.toComponent(); expect(attrs).toBeDefined(); @@ -45,19 +37,15 @@ describe('server', () => { }); it('renders html attributes as string', () => { - const context = {}; - render( + const head = renderContext( , - context + /> ); - const head = context.helmet; - expect(head.htmlAttributes).toBeDefined(); expect(head.htmlAttributes.toString).toBeDefined(); expect(head.htmlAttributes.toString()).toMatchSnapshot(); @@ -66,16 +54,13 @@ describe('server', () => { describe('Declarative API', () => { it('renders html attributes as component', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const { htmlAttributes } = context.helmet; - const attrs = htmlAttributes.toComponent(); + const attrs = head.htmlAttributes.toComponent(); expect(attrs).toBeDefined(); @@ -85,16 +70,12 @@ describe('server', () => { }); it('renders html attributes as string', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.htmlAttributes).toBeDefined(); expect(head.htmlAttributes.toString).toBeDefined(); expect(head.htmlAttributes.toString()).toMatchSnapshot(); diff --git a/__tests__/server/link.test.js b/__tests__/server/link.test.tsx similarity index 76% rename from __tests__/server/link.test.js rename to __tests__/server/link.test.tsx index d21a8944..059b0b94 100644 --- a/__tests__/server/link.test.js +++ b/__tests__/server/link.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext, isArray } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -20,15 +15,10 @@ afterAll(() => { Provider.canUseDOM = true; }); -const isArray = { - asymmetricMatch: actual => Array.isArray(actual), -}; - describe('server', () => { describe('API', () => { it('renders link tags as React components', () => { - const context = {}; - render( + const head = renderContext( { type: 'text/css', }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.link).toBeDefined(); expect(head.link.toComponent).toBeDefined(); @@ -52,7 +39,7 @@ describe('server', () => { expect(linkComponent).toEqual(isArray); expect(linkComponent).toHaveLength(2); - linkComponent.forEach(link => { + linkComponent.forEach((link: Element) => { expect(link).toEqual(expect.objectContaining({ type: 'link' })); }); @@ -62,8 +49,7 @@ describe('server', () => { }); it('renders link tags as string', () => { - const context = {}; - render( + const head = renderContext( { type: 'text/css', }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.link).toBeDefined(); expect(head.link.toString).toBeDefined(); expect(head.link.toString()).toMatchSnapshot(); @@ -87,17 +70,13 @@ describe('server', () => { describe('Declarative API', () => { it('renders link tags as React components', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.link).toBeDefined(); expect(head.link.toComponent).toBeDefined(); @@ -106,7 +85,7 @@ describe('server', () => { expect(linkComponent).toEqual(isArray); expect(linkComponent).toHaveLength(2); - linkComponent.forEach(link => { + linkComponent.forEach((link: Element) => { expect(link).toEqual(expect.objectContaining({ type: 'link' })); }); @@ -116,17 +95,13 @@ describe('server', () => { }); it('renders link tags as string', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.link).toBeDefined(); expect(head.link.toString).toBeDefined(); expect(head.link.toString()).toMatchSnapshot(); diff --git a/__tests__/server/meta.test.js b/__tests__/server/meta.test.tsx similarity index 73% rename from __tests__/server/meta.test.js rename to __tests__/server/meta.test.tsx index a10fc14e..979a34d9 100644 --- a/__tests__/server/meta.test.js +++ b/__tests__/server/meta.test.tsx @@ -1,17 +1,12 @@ -import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import React from 'react'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext, isArray } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -20,15 +15,10 @@ afterAll(() => { Provider.canUseDOM = true; }); -const isArray = { - asymmetricMatch: actual => Array.isArray(actual), -}; - describe('server', () => { describe('API', () => { it('renders meta tags as React components', () => { - const context = {}; - render( + const head = renderContext( { { property: 'og:type', content: 'article' }, { itemprop: 'name', content: 'Test name itemprop' }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.meta).toBeDefined(); expect(head.meta.toComponent).toBeDefined(); @@ -54,18 +41,17 @@ describe('server', () => { expect(metaComponent).toEqual(isArray); expect(metaComponent).toHaveLength(5); - metaComponent.forEach(meta => { + metaComponent.forEach((meta: Element) => { expect(meta).toEqual(expect.objectContaining({ type: 'meta' })); }); - const markup = ReactServer.renderToStaticMarkup({metaComponent}); + const markup = ReactServer.renderToStaticMarkup(metaComponent); expect(markup).toMatchSnapshot(); }); it('renders meta tags as string', () => { - const context = {}; - render( + const head = renderContext( { { property: 'og:type', content: 'article' }, { itemprop: 'name', content: 'Test name itemprop' }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.meta).toBeDefined(); expect(head.meta.toString).toBeDefined(); expect(head.meta.toString()).toMatchSnapshot(); @@ -91,8 +74,7 @@ describe('server', () => { describe('Declarative API', () => { it('renders meta tags as React components', () => { - const context = {}; - render( + const head = renderContext( { - , - context + ); - const head = context.helmet; - expect(head.meta).toBeDefined(); expect(head.meta.toComponent).toBeDefined(); @@ -116,33 +95,29 @@ describe('server', () => { expect(metaComponent).toEqual(isArray); expect(metaComponent).toHaveLength(5); - metaComponent.forEach(meta => { + metaComponent.forEach((meta: Element) => { expect(meta).toEqual(expect.objectContaining({ type: 'meta' })); }); - const markup = ReactServer.renderToStaticMarkup({metaComponent}); + const markup = ReactServer.renderToStaticMarkup(metaComponent); expect(markup).toMatchSnapshot(); }); it('renders meta tags as string', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.meta).toBeDefined(); expect(head.meta.toString).toBeDefined(); expect(head.meta.toString()).toMatchSnapshot(); diff --git a/__tests__/server/noscript.test.js b/__tests__/server/noscript.test.tsx similarity index 76% rename from __tests__/server/noscript.test.js rename to __tests__/server/noscript.test.tsx index 000a51ab..edc514df 100644 --- a/__tests__/server/noscript.test.js +++ b/__tests__/server/noscript.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext, isArray } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -20,15 +15,10 @@ afterAll(() => { Provider.canUseDOM = true; }); -const isArray = { - asymmetricMatch: actual => Array.isArray(actual), -}; - describe('server', () => { describe('API', () => { it('renders noscript tags as React components', () => { - const context = {}; - render( + const head = renderContext( { innerHTML: '', }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.noscript).toBeDefined(); expect(head.noscript.toComponent).toBeDefined(); @@ -54,7 +41,7 @@ describe('server', () => { expect(noscriptComponent).toEqual(isArray); expect(noscriptComponent).toHaveLength(2); - noscriptComponent.forEach(noscript => { + noscriptComponent.forEach((noscript: Element) => { expect(noscript).toEqual(expect.objectContaining({ type: 'noscript' })); }); @@ -66,17 +53,13 @@ describe('server', () => { describe('Declarative API', () => { it('renders noscript tags as React components', () => { - const context = {}; - render( + const head = renderContext( - , - context + ); - const head = context.helmet; - expect(head.noscript).toBeDefined(); expect(head.noscript.toComponent).toBeDefined(); @@ -85,7 +68,7 @@ describe('server', () => { expect(noscriptComponent).toEqual(isArray); expect(noscriptComponent).toHaveLength(2); - noscriptComponent.forEach(noscript => { + noscriptComponent.forEach((noscript: Element) => { expect(noscript).toEqual(expect.objectContaining({ type: 'noscript' })); }); diff --git a/__tests__/server/script.test.js b/__tests__/server/script.test.tsx similarity index 76% rename from __tests__/server/script.test.js rename to __tests__/server/script.test.tsx index a2d01f72..7f844ab9 100644 --- a/__tests__/server/script.test.js +++ b/__tests__/server/script.test.tsx @@ -1,17 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import ReactServer from 'react-dom/server'; -import Helmet from '../../src'; + +import { Helmet } from '../../src'; import Provider from '../../src/Provider'; +import { renderContext, isArray } from '../utils'; Helmet.defaultProps.defer = false; -const mount = document.getElementById('mount'); - -const render = (node, context = {}) => { - ReactDOM.render({node}, mount); -}; - beforeAll(() => { Provider.canUseDOM = false; }); @@ -20,15 +15,10 @@ afterAll(() => { Provider.canUseDOM = true; }); -const isArray = { - asymmetricMatch: actual => Array.isArray(actual), -}; - describe('server', () => { describe('API', () => { it('renders script tags as React components', () => { - const context = {}; - render( + const head = renderContext( { type: 'text/javascript', }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.script).toBeDefined(); expect(head.script.toComponent).toBeDefined(); @@ -54,7 +41,7 @@ describe('server', () => { expect(scriptComponent).toEqual(isArray); expect(scriptComponent).toHaveLength(2); - scriptComponent.forEach(script => { + scriptComponent.forEach((script: Element) => { expect(script).toEqual(expect.objectContaining({ type: 'script' })); }); @@ -64,8 +51,7 @@ describe('server', () => { }); it('renders script tags as string', () => { - const context = {}; - render( + const head = renderContext( { type: 'text/javascript', }, ]} - />, - context + /> ); - const head = context.helmet; - expect(head.script).toBeDefined(); expect(head.script.toString).toBeDefined(); expect(head.script.toString()).toMatchSnapshot(); @@ -91,17 +74,13 @@ describe('server', () => { describe('Declarative API', () => { it('renders script tags as React components', () => { - const context = {}; - render( + const head = renderContext(