Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby): Allow <html> and <body> attributes to be updated from Head #37449

Merged
merged 16 commits into from
Jan 19, 2023
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"

describe(`Html and body attributes`, () => {
it(`Page has body and html attributes on direct visit`, () => {
cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)
})

it(`Page has body and html attributes on client-side navigation`, () => {
cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange()

cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`not.have.attr`, `class`, `foo`)
cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`not.have.attr`, `lang`, `fr`)

cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)
})

it(`Body and html attributes are removed on client-side navigation when new page doesn't set them`, () => {
cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)

cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange()

cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`not.have.attr`, `class`, `foo`)
cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`not.have.attr`, `lang`, `fr`)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const page = {
invalidElements: `${path}/invalid-elements/`,
fsRouteApi: `${path}/fs-route-api/`,
deduplication: `${path}/deduplication/`,
htmlAndBodyAttributes: `${path}/html-and-body-attributes/`,
}

const data = {
Expand All @@ -23,7 +24,7 @@ const data = {
style: `rebeccapurple`,
link: `/used-by-head-function-export-basic.css`,
extraMeta: `Extra meta tag that should be removed during navigation`,
jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`
jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`,
},
queried: {
base: `http://localhost:8000`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"

Cypress.on("uncaught:exception", err => {
if (
(err.message.includes("Minified React error #418") ||
err.message.includes("Minified React error #423") ||
err.message.includes("Minified React error #425")) &&
Cypress.env(`TEST_PLUGIN_OFFLINE`)
) {
return false
}
})

describe(`Html and body attributes`, () => {
it(`Page has body and html attributes on direct visit`, () => {
cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)
})

it(`Page has body and html attributes on client-side navigation`, () => {
cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange()

cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`not.have.attr`, `class`, `foo`)
cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`not.have.attr`, `lang`, `fr`)

cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)
})

it(`Body and html attributes are removed on client-side navigation when new page doesn't set them`, () => {
cy.visit(
headFunctionExportSharedData.page.htmlAndBodyAttributes
).waitForRouteChange()

cy.get(`body`).should(`have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`have.attr`, `class`, `foo`)
cy.get(`html`).should(`have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`have.attr`, `lang`, `fr`)

cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange()

cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`)
cy.get(`body`).should(`not.have.attr`, `class`, `foo`)
cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`)
cy.get(`html`).should(`not.have.attr`, `lang`, `fr`)
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { page, data } from "../../../shared-data/head-function-export.js"

Cypress.on("uncaught:exception", err => {
if (
(err.message.includes("Minified React error #418") ||
err.message.includes("Minified React error #423") ||
err.message.includes("Minified React error #425")) &&
Cypress.env(`TEST_PLUGIN_OFFLINE`)
) {
return false
}
})

describe(`Head function export html insertion`, () => {
it(`should work with static data`, () => {
cy.visit(page.basic).waitForRouteChange()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const page = {
fsRouteApi: `${path}/fs-route-api/`,
deduplication: `${path}/deduplication/`,
pageWithUseLocation: `${path}/page-with-uselocation/`,
htmlAndBodyAttributes: `${path}/html-and-body-attributes/`,
}

const data = {
Expand All @@ -24,7 +25,7 @@ const data = {
style: `rebeccapurple`,
link: `/used-by-head-function-export-basic.css`,
extraMeta: `Extra meta tag that should be removed during navigation`,
jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`
jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`,
},
queried: {
base: `http://localhost:9000`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,24 @@ describe(`Head function export SSR'ed HTML output`, () => {
// alternate links are not using id, so should have multiple instances
expect(dom.querySelectorAll(`link[rel=alternate]`)?.length).toEqual(2)
})

it(`should allow setting html and body attributes`, () => {
const html = readFileSync(
`${publicDir}${page.bodyAndHtmlAttributes}/index.html`
)
const dom = parse(html)
expect(dom.querySelector(`html`).attributes).toMatchInlineSnapshot(`
{
"data-foo": "bar",
"lang": "fr",
}
`)

expect(dom.querySelector(`body`).attributes).toMatchInlineSnapshot(`
{
"class": "foo",
"data-foo": "baz",
}
`)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const page = {
warnings: `${path}/warnings/`,
allProps: `${path}/all-props/`,
deduplication: `${path}/deduplication/`,
bodyAndHtmlAttributes: `${path}/html-and-body-attributes/`,
}

const data = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
18 changes: 18 additions & 0 deletions integration-tests/ssr/__tests__/ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ describe(`SSR`, () => {
const ssrHead = ssrDom.querySelector(`[data-testid=title]`)

expect(devSsrHead.textContent).toEqual(ssrHead.textContent)
expect(devSsrDom.querySelector(`html`).attributes).toEqual(
ssrDom.querySelector(`html`).attributes
)
expect(devSsrDom.querySelector(`html`).attributes).toMatchInlineSnapshot(`
Object {
"data-foo": "bar",
"lang": "fr",
}
`)

expect(devSsrDom.querySelector(`body`).attributes).toEqual(
ssrDom.querySelector(`body`).attributes
)
expect(devSsrDom.querySelector(`body`).attributes).toMatchInlineSnapshot(`
Object {
"data-foo": "baz",
}
`)
})

describe(`it generates an error page correctly`, () => {
Expand Down
8 changes: 7 additions & 1 deletion integration-tests/ssr/src/pages/head-function-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@ export default function PageWithHeadFunctionExport() {
}

export function Head() {
return <title data-testid="title">Hello world</title>
return (
<>
<html lang="fr" data-foo="bar" />
<body data-foo="baz" />
<title data-testid="title">Hello world</title>
</>
)
}
2 changes: 2 additions & 0 deletions packages/gatsby/cache-dir/head/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export const VALID_NODE_NAMES = [
`base`,
`noscript`,
`script`,
`html`,
`body`,
]
Loading