Hey there,
I'm running into an issue with testing. There's a good chance this is user error on my part, but I'd appreciate any light you guys can shed :)
Specifically, we recently added jsdom to our test suite. This means that global.window and global.document are available.
This was causing Helmet.rewind() to throw an exception, because it erroneously believed that we were trying to call it on the client.
I found a previous issue that illustrated that I can set this property manually:
if (process.env.TEST) {
Helmet.canUseDOM = false;
}
This fixed the issue, but introduced another one, and I'm having a hard time understanding it.
For the very first test that runs, the Component.rewind() call returns what I assume is the original object. It looks like this:
{
htmlAttributes: {},
title: 'Site Name',
titleAttributes: {},
baseTag: [],
metaTags: [
{ name: 'charset', content: 'UTF8' },
{ name: 'viewport',
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimal-ui' },
...
]
}
Subsequent tests, oddly, get mapped correctly:
{
htmlAttributes: {
toComponent: [Function: toComponent],
toString: [Function: toString]
},
title: {
toComponent: [Function: toComponent],
toString: [Function: toString]
},
base: {
toComponent: [Function: toComponent],
toString: [Function: toString]
},
meta: {
toComponent: [Function: toComponent],
toString: [Function: toString]
},
...
}
It's problematic because the tests try to access head.meta.toString(), and it explodes because the object hasn't been "converted".
Appreciate any guidance!
@joshwcomeau You can certainly use the Helmet.canUseDOM to force Helmet into server-side rendering. We do that in our tests as well. But keep in mind, when you call rewind it returns the current head state and clears the array of mounted instances to save memory on the server. It's really meant to only be called once on the server to then render the page for client to re-create the array of instances. I wonder, on your subsequent tests are you unmounting and re-mounting your component? This would guarantee that your array of instances is created fresh for you on the "server" for you to call rewind. Hope that helps.
Hi @cwelch5,
Thanks for the quick response!
So, I think I may have miscommunicated my issue. That, or I'm misunderstanding your response.
The code that I have, simplified, looks something like this:
(Note, the following code all runs on the server. We use supertest to make the request, generate the html response, and then run tests on the response string).
////// JSDOM init code, runs once before any of the tests run
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
global[property] = document.defaultView[property];
}
});
////// Request code, runs at the start of every request
// Read our HTML file. It has placeholders for stuff that we'll inject later
const layout = fs.readFileSync('path/to/layout.html');
// Render to string
const reactHtml = renderToString(
...
);
// Create response html
// Because JSDom exists, ExecutionEnvironment assumes that we're on the client.
// Correct that erroneous belief by setting an override
if (process.env.TEST) {
Helmet.canUseDOM = false;
}
const head = Helmet.rewind();
// Inject meta tags, as well as our rendered React string, into layout
return layout
.replace('!TITLE!', head.title.toString())
.replace('!META!', head.meta.toString())
.replace('!LINK!', head.link.toString())
.replace('!HTML!', reactHtml)
What's happening is that, for the very first request, head contains an "untransformed" set of data. The data is correct; it has the right title, meta tags, etc, but it doesn't have the toString and toComponent methods:
{
htmlAttributes: {},
title: 'Our Site Name',
titleAttributes: {},
baseTag: [],
metaTags: [
{ name: 'charset', content: 'UTF8' },
{ name: 'viewport',
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimal-ui' },
...
]
}
When our layout tries to inject the title, by calling head.title.toString(), the whole thing blows up.
The interesting thing is that I don't have this problem if I don't use JSDOM before the test suite runs.
So yeah, I'm curious if you know why the data isn't being "converted" into the format, where I can use .toString?
To answer your question, I don't believe we're ever mounting/unmounting the components. We're just generating their initial structure and then testing against it with Cheerio.
Hey guys!
So, the issue was user error on my part; I was setting Helmet.canUseDOM _after_ the renderToString call, so for the first render, Helmet had already done its work.
Sorry for the inconvenience!
Most helpful comment
Hey guys!
So, the issue was user error on my part; I was setting
Helmet.canUseDOM_after_ therenderToStringcall, so for the first render, Helmet had already done its work.Sorry for the inconvenience!