Skip to content

Commit

Permalink
feat(gatsby): Gatsby Head API (#35980)
Browse files Browse the repository at this point in the history
Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
Co-authored-by: pieh <misiek.piechowiak@gmail.com>
Co-authored-by: tyhopp <hopp.ty.c@gmail.com>
Co-authored-by: Lennart <lekoarts@gmail.com>
Co-authored-by: Ty Hopp <tyhopp@users.noreply.github.com>
  • Loading branch information
5 people committed Jul 14, 2022
1 parent b7b3f31 commit 40810c4
Show file tree
Hide file tree
Showing 112 changed files with 2,943 additions and 204 deletions.
9 changes: 9 additions & 0 deletions .circleci/config.yml
Expand Up @@ -320,6 +320,13 @@ jobs:
test_path: integration-tests/functions
test_command: yarn test

integration_tests_head_function_export:
executor: node
steps:
- e2e-test:
test_path: integration-tests/head-function-export
test_command: yarn test

e2e_tests_path-prefix:
<<: *e2e-executor
environment:
Expand Down Expand Up @@ -626,6 +633,8 @@ workflows:
<<: *e2e-test-workflow
- integration_tests_functions:
<<: *e2e-test-workflow
- integration_tests_head_function_export:
<<: *e2e-test-workflow
- integration_tests_gatsby_cli:
requires:
- bootstrap
Expand Down
Expand Up @@ -23,7 +23,7 @@ describe(`limited-exports-page-templates`, () => {
it(`should initially not log to console`, () => {
cy.get(`@hmrConsoleLog`).should(
`not.be.calledWithMatch`,
/13:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData or config are allowed./i
/13:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData, Head or config are allowed./i
)
})
it(`should log warning to console for invalid export`, () => {
Expand All @@ -34,11 +34,11 @@ describe(`limited-exports-page-templates`, () => {

cy.get(`@hmrConsoleLog`).should(
`be.calledWithMatch`,
/13:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData or config are allowed./i
/13:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData, Head or config are allowed./i
)
cy.get(`@hmrConsoleLog`).should(
`not.be.calledWithMatch`,
/15:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData or config are allowed./i
/15:1 {2}warning {2}In page templates only a default export of a valid React component and the named exports of a page query, getServerData, Head or config are allowed./i
)
})
})
@@ -0,0 +1,32 @@
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"

it(`Head function export receive correct props`, () => {
cy.visit(headFunctionExportSharedData.page.correctProps).waitForRouteChange()

const data = {
site: {
siteMetadata: {
headFunctionExport: {
...headFunctionExportSharedData.data.queried,
},
},
},
}
const location = {
pathname: headFunctionExportSharedData.page.correctProps,
}

const pageContext = headFunctionExportSharedData.data.context

cy.getTestElement(`pageContext`)
.invoke(`attr`, `content`)
.should(`equal`, JSON.stringify(pageContext, null, 2))

cy.getTestElement(`location`)
.invoke(`attr`, `content`)
.should(`equal`, JSON.stringify(location, null, 2))

cy.getTestElement(`data`)
.invoke(`attr`, `content`)
.should(`equal`, JSON.stringify(data, null, 2))
})
@@ -0,0 +1,6 @@
import { page, data } from "../../../shared-data/head-function-export.js"

it(`Head function export with FS Route API should work`, () => {
cy.visit(page.fsRouteApi).waitForRouteChange()
cy.getTestElement(`title`).should(`have.text`, data.fsRouteApi.slug)
})
@@ -0,0 +1,99 @@
import { page, data } from "../../../shared-data/head-function-export.js"

describe(`Head function export html insertion`, () => {
it(`should work with static data`, () => {
cy.visit(page.basic).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.base)
cy.getTestElement(`title`).should(`have.text`, data.static.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
cy.getTestElement(`style`).should(`contain`, data.static.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.link)
})

it(`should work with data from a page query`, () => {
cy.visit(page.pageQuery).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.base)
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.queried.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
cy.getTestElement(`style`).should(`contain`, data.queried.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.link)
})

it(`should work when a head function with static data is re-exported from the page`, () => {
cy.visit(page.reExport).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.base)
cy.getTestElement(`title`).should(`have.text`, data.static.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
cy.getTestElement(`style`).should(`contain`, data.static.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.link)
})

it(`should work when an imported Head component with queried data is used`, () => {
cy.visit(page.staticQuery).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.base)
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.queried.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
cy.getTestElement(`style`).should(`contain`, data.queried.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.link)
})

it(`should work in a DSG page (exporting function named config)`, () => {
cy.visit(page.dsg).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.dsg.base)
cy.getTestElement(`title`).should(`have.text`, data.dsg.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.dsg.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.dsg.noscript)
cy.getTestElement(`style`).should(`contain`, data.dsg.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.dsg.link)
})

it(`should work in an SSR page (exporting function named getServerData)`, () => {
cy.visit(page.ssr).waitForRouteChange()
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.ssr.base)
cy.getTestElement(`title`).should(`have.text`, data.ssr.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.ssr.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.ssr.noscript)
cy.getTestElement(`style`).should(`contain`, data.ssr.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.ssr.link)
})
})
@@ -0,0 +1,13 @@
import { page, data } from "../../../shared-data/head-function-export.js"

it(`Head function export should not include invalid elements`, () => {
cy.visit(page.invalidElements).waitForRouteChange()

cy.get(`head > h1`).should(`not.exist`)
cy.get(`head > div`).should(`not.exist`)
cy.get(`head > audio`).should(`not.exist`)
cy.get(`head > video`).should(`not.exist`)
cy.get(`head > title`)
.should(`exist`)
.and(`have.text`, data.invalidElements.title)
})
@@ -0,0 +1,114 @@
import { page, data } from "../../../shared-data/head-function-export.js"

// No need to test SSR navigation (anchor tags) because it's effectively covered in the html insertion tests

describe(`Head function export behavior during CSR navigation (Gatsby Link)`, () => {
it(`should remove tags not on next page`, () => {
cy.visit(page.basic).waitForRouteChange()

cy.getTestElement(`extra-meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.extraMeta)

cy.getTestElement(`gatsby-link`).click().waitForRouteChange()

cy.get(`[data-testid="extra-meta"]`).should(`not.exist`)
})

it(`should add tags not on next page`, () => {
cy.visit(page.basic).waitForRouteChange()

cy.get(`[data-testid="extra-meta-2"]`).should(`not.exist`)

cy.getTestElement(`gatsby-link`).click().waitForRouteChange()

cy.getTestElement(`extra-meta-2`)
.invoke(`attr`, `content`)
.should(`equal`, data.queried.extraMeta2)
})

it(`should not contain tags from old tags when we navigate to page without Head export`, () => {
cy.visit(page.basic).waitForRouteChange()

cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.base)
cy.getTestElement(`title`).should(`have.text`, data.static.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
cy.getTestElement(`style`).should(`contain`, data.static.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.link)

cy.getTestElement(`navigate-to-page-without-head-export`)
.click()
.waitForRouteChange()

cy.getTestElement(`base`).should(`not.exist`)
cy.getTestElement(`title`).should(`not.exist`)
cy.getTestElement(`meta`).should(`not.exist`)
cy.getTestElement(`noscript`).should(`not.exist`)
cy.getTestElement(`style`).should(`not.exist`)
cy.getTestElement(`link`).should(`not.exist`)
})

/**
* Technically nodes are always removed from the DOM and new ones added (in other words nodes are not reused with different data),
* but since this is an implementation detail we'll still test the behavior we expect as if we didn't know that.
*/
it(`should change meta tag values`, () => {
// Initial load
cy.visit(page.basic).waitForRouteChange()

// Validate data from initial load
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.base)
cy.getTestElement(`title`).should(`have.text`, data.static.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
cy.getTestElement(`style`).should(`contain`, data.static.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.link)

// Navigate to a different page via Gatsby Link
cy.getTestElement(`gatsby-link`).click()

// Validate data on navigated-to page
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.base)
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.queried.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
cy.getTestElement(`style`).should(`contain`, data.queried.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.queried.link)

// Navigate back to original page via Gatsby Link
cy.getTestElement(`gatsby-link`).click().waitForRouteChange()

// Validate data is same as initial load
cy.getTestElement(`base`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.base)
cy.getTestElement(`title`).should(`have.text`, data.static.title)
cy.getTestElement(`meta`)
.invoke(`attr`, `content`)
.should(`equal`, data.static.meta)
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
cy.getTestElement(`style`).should(`contain`, data.static.style)
cy.getTestElement(`link`)
.invoke(`attr`, `href`)
.should(`equal`, data.static.link)
})
})
@@ -0,0 +1,11 @@
describe(`Tsx Pages`, () => {
it(`Works with Head export`, () => {
cy.visit(`/head-function-export/tsx-page`)

cy.getTestElement(`title`).should(`contain`, `TypeScript`)

cy.getTestElement(`name`)
.invoke(`attr`, `content`)
.should(`equal`, `TypeScript`)
})
})
@@ -0,0 +1,28 @@
import { VALID_NODE_NAMES } from "gatsby/cache-dir/head/constants"
import { page } from "../../../shared-data/head-function-export.js"

describe(`Head function export should warn`, () => {
beforeEach(() => {
cy.visit(page.warnings, {
onBeforeLoad(win) {
cy.stub(win.console, `warn`).as(`consoleWarn`)
},
}).waitForRouteChange()
})

it(`for elements that belong in the body`, () => {
cy.get(`@consoleWarn`).should(
`be.calledWith`,
`<h1> is not a valid head element. Please use one of the following: ${VALID_NODE_NAMES.join(
`, `
)}`
)
})

it(`for scripts that could use the script component`, () => {
cy.get(`@consoleWarn`).should(
`be.calledWith`,
`Do not add scripts here. Please use the <Script> component in your page template instead. For more info see: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-script/`
)
})
})
@@ -0,0 +1,28 @@
const { data } = require("../../../shared-data/head-function-export")

const TEST_ID = `extra-meta-for-hot-reloading`

describe(`hot reloading Head export`, () => {
beforeEach(() => {
cy.visit(`/head-function-export/basic`).waitForRouteChange()
})

it(`displays placeholder content on launch`, () => {
cy.getTestElement(TEST_ID)
.invoke(`attr`, `content`)
.should(`contain.equal`, "%SOME_EXTRA_META%")
})

it(`hot reloads with new content`, () => {
const text = `New Title by HMR`
cy.exec(
`npm run update -- --file src/pages/head-function-export/basic.js --replacements "SOME_EXTRA_META:${text}"`
)

cy.waitForHmr()

cy.getTestElement(TEST_ID)
.invoke(`attr`, `content`)
.should(`contain.equal`, text)
})
})

0 comments on commit 40810c4

Please sign in to comment.