Skip to content

Commit 40810c4

Browse files
marvinjudepiehtyhoppLekoArts
authored
feat(gatsby): Gatsby Head API (#35980)
Co-authored-by: Michal Piechowiak <[email protected]> Co-authored-by: pieh <[email protected]> Co-authored-by: tyhopp <[email protected]> Co-authored-by: Lennart <[email protected]> Co-authored-by: Ty Hopp <[email protected]>
1 parent b7b3f31 commit 40810c4

File tree

112 files changed

+2943
-204
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+2943
-204
lines changed

.circleci/config.yml

+9
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,13 @@ jobs:
320320
test_path: integration-tests/functions
321321
test_command: yarn test
322322

323+
integration_tests_head_function_export:
324+
executor: node
325+
steps:
326+
- e2e-test:
327+
test_path: integration-tests/head-function-export
328+
test_command: yarn test
329+
323330
e2e_tests_path-prefix:
324331
<<: *e2e-executor
325332
environment:
@@ -626,6 +633,8 @@ workflows:
626633
<<: *e2e-test-workflow
627634
- integration_tests_functions:
628635
<<: *e2e-test-workflow
636+
- integration_tests_head_function_export:
637+
<<: *e2e-test-workflow
629638
- integration_tests_gatsby_cli:
630639
requires:
631640
- bootstrap

e2e-tests/development-runtime/cypress/integration/eslint-rules/limited-exports-page-templates.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe(`limited-exports-page-templates`, () => {
2323
it(`should initially not log to console`, () => {
2424
cy.get(`@hmrConsoleLog`).should(
2525
`not.be.calledWithMatch`,
26-
/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
26+
/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
2727
)
2828
})
2929
it(`should log warning to console for invalid export`, () => {
@@ -34,11 +34,11 @@ describe(`limited-exports-page-templates`, () => {
3434

3535
cy.get(`@hmrConsoleLog`).should(
3636
`be.calledWithMatch`,
37-
/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
37+
/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
3838
)
3939
cy.get(`@hmrConsoleLog`).should(
4040
`not.be.calledWithMatch`,
41-
/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
41+
/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
4242
)
4343
})
4444
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"
2+
3+
it(`Head function export receive correct props`, () => {
4+
cy.visit(headFunctionExportSharedData.page.correctProps).waitForRouteChange()
5+
6+
const data = {
7+
site: {
8+
siteMetadata: {
9+
headFunctionExport: {
10+
...headFunctionExportSharedData.data.queried,
11+
},
12+
},
13+
},
14+
}
15+
const location = {
16+
pathname: headFunctionExportSharedData.page.correctProps,
17+
}
18+
19+
const pageContext = headFunctionExportSharedData.data.context
20+
21+
cy.getTestElement(`pageContext`)
22+
.invoke(`attr`, `content`)
23+
.should(`equal`, JSON.stringify(pageContext, null, 2))
24+
25+
cy.getTestElement(`location`)
26+
.invoke(`attr`, `content`)
27+
.should(`equal`, JSON.stringify(location, null, 2))
28+
29+
cy.getTestElement(`data`)
30+
.invoke(`attr`, `content`)
31+
.should(`equal`, JSON.stringify(data, null, 2))
32+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { page, data } from "../../../shared-data/head-function-export.js"
2+
3+
it(`Head function export with FS Route API should work`, () => {
4+
cy.visit(page.fsRouteApi).waitForRouteChange()
5+
cy.getTestElement(`title`).should(`have.text`, data.fsRouteApi.slug)
6+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { page, data } from "../../../shared-data/head-function-export.js"
2+
3+
describe(`Head function export html insertion`, () => {
4+
it(`should work with static data`, () => {
5+
cy.visit(page.basic).waitForRouteChange()
6+
cy.getTestElement(`base`)
7+
.invoke(`attr`, `href`)
8+
.should(`equal`, data.static.base)
9+
cy.getTestElement(`title`).should(`have.text`, data.static.title)
10+
cy.getTestElement(`meta`)
11+
.invoke(`attr`, `content`)
12+
.should(`equal`, data.static.meta)
13+
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
14+
cy.getTestElement(`style`).should(`contain`, data.static.style)
15+
cy.getTestElement(`link`)
16+
.invoke(`attr`, `href`)
17+
.should(`equal`, data.static.link)
18+
})
19+
20+
it(`should work with data from a page query`, () => {
21+
cy.visit(page.pageQuery).waitForRouteChange()
22+
cy.getTestElement(`base`)
23+
.invoke(`attr`, `href`)
24+
.should(`equal`, data.queried.base)
25+
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
26+
cy.getTestElement(`meta`)
27+
.invoke(`attr`, `content`)
28+
.should(`equal`, data.queried.meta)
29+
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
30+
cy.getTestElement(`style`).should(`contain`, data.queried.style)
31+
cy.getTestElement(`link`)
32+
.invoke(`attr`, `href`)
33+
.should(`equal`, data.queried.link)
34+
})
35+
36+
it(`should work when a head function with static data is re-exported from the page`, () => {
37+
cy.visit(page.reExport).waitForRouteChange()
38+
cy.getTestElement(`base`)
39+
.invoke(`attr`, `href`)
40+
.should(`equal`, data.static.base)
41+
cy.getTestElement(`title`).should(`have.text`, data.static.title)
42+
cy.getTestElement(`meta`)
43+
.invoke(`attr`, `content`)
44+
.should(`equal`, data.static.meta)
45+
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
46+
cy.getTestElement(`style`).should(`contain`, data.static.style)
47+
cy.getTestElement(`link`)
48+
.invoke(`attr`, `href`)
49+
.should(`equal`, data.static.link)
50+
})
51+
52+
it(`should work when an imported Head component with queried data is used`, () => {
53+
cy.visit(page.staticQuery).waitForRouteChange()
54+
cy.getTestElement(`base`)
55+
.invoke(`attr`, `href`)
56+
.should(`equal`, data.queried.base)
57+
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
58+
cy.getTestElement(`meta`)
59+
.invoke(`attr`, `content`)
60+
.should(`equal`, data.queried.meta)
61+
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
62+
cy.getTestElement(`style`).should(`contain`, data.queried.style)
63+
cy.getTestElement(`link`)
64+
.invoke(`attr`, `href`)
65+
.should(`equal`, data.queried.link)
66+
})
67+
68+
it(`should work in a DSG page (exporting function named config)`, () => {
69+
cy.visit(page.dsg).waitForRouteChange()
70+
cy.getTestElement(`base`)
71+
.invoke(`attr`, `href`)
72+
.should(`equal`, data.dsg.base)
73+
cy.getTestElement(`title`).should(`have.text`, data.dsg.title)
74+
cy.getTestElement(`meta`)
75+
.invoke(`attr`, `content`)
76+
.should(`equal`, data.dsg.meta)
77+
cy.getTestElement(`noscript`).should(`have.text`, data.dsg.noscript)
78+
cy.getTestElement(`style`).should(`contain`, data.dsg.style)
79+
cy.getTestElement(`link`)
80+
.invoke(`attr`, `href`)
81+
.should(`equal`, data.dsg.link)
82+
})
83+
84+
it(`should work in an SSR page (exporting function named getServerData)`, () => {
85+
cy.visit(page.ssr).waitForRouteChange()
86+
cy.getTestElement(`base`)
87+
.invoke(`attr`, `href`)
88+
.should(`equal`, data.ssr.base)
89+
cy.getTestElement(`title`).should(`have.text`, data.ssr.title)
90+
cy.getTestElement(`meta`)
91+
.invoke(`attr`, `content`)
92+
.should(`equal`, data.ssr.meta)
93+
cy.getTestElement(`noscript`).should(`have.text`, data.ssr.noscript)
94+
cy.getTestElement(`style`).should(`contain`, data.ssr.style)
95+
cy.getTestElement(`link`)
96+
.invoke(`attr`, `href`)
97+
.should(`equal`, data.ssr.link)
98+
})
99+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { page, data } from "../../../shared-data/head-function-export.js"
2+
3+
it(`Head function export should not include invalid elements`, () => {
4+
cy.visit(page.invalidElements).waitForRouteChange()
5+
6+
cy.get(`head > h1`).should(`not.exist`)
7+
cy.get(`head > div`).should(`not.exist`)
8+
cy.get(`head > audio`).should(`not.exist`)
9+
cy.get(`head > video`).should(`not.exist`)
10+
cy.get(`head > title`)
11+
.should(`exist`)
12+
.and(`have.text`, data.invalidElements.title)
13+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { page, data } from "../../../shared-data/head-function-export.js"
2+
3+
// No need to test SSR navigation (anchor tags) because it's effectively covered in the html insertion tests
4+
5+
describe(`Head function export behavior during CSR navigation (Gatsby Link)`, () => {
6+
it(`should remove tags not on next page`, () => {
7+
cy.visit(page.basic).waitForRouteChange()
8+
9+
cy.getTestElement(`extra-meta`)
10+
.invoke(`attr`, `content`)
11+
.should(`equal`, data.static.extraMeta)
12+
13+
cy.getTestElement(`gatsby-link`).click().waitForRouteChange()
14+
15+
cy.get(`[data-testid="extra-meta"]`).should(`not.exist`)
16+
})
17+
18+
it(`should add tags not on next page`, () => {
19+
cy.visit(page.basic).waitForRouteChange()
20+
21+
cy.get(`[data-testid="extra-meta-2"]`).should(`not.exist`)
22+
23+
cy.getTestElement(`gatsby-link`).click().waitForRouteChange()
24+
25+
cy.getTestElement(`extra-meta-2`)
26+
.invoke(`attr`, `content`)
27+
.should(`equal`, data.queried.extraMeta2)
28+
})
29+
30+
it(`should not contain tags from old tags when we navigate to page without Head export`, () => {
31+
cy.visit(page.basic).waitForRouteChange()
32+
33+
cy.getTestElement(`base`)
34+
.invoke(`attr`, `href`)
35+
.should(`equal`, data.static.base)
36+
cy.getTestElement(`title`).should(`have.text`, data.static.title)
37+
cy.getTestElement(`meta`)
38+
.invoke(`attr`, `content`)
39+
.should(`equal`, data.static.meta)
40+
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
41+
cy.getTestElement(`style`).should(`contain`, data.static.style)
42+
cy.getTestElement(`link`)
43+
.invoke(`attr`, `href`)
44+
.should(`equal`, data.static.link)
45+
46+
cy.getTestElement(`navigate-to-page-without-head-export`)
47+
.click()
48+
.waitForRouteChange()
49+
50+
cy.getTestElement(`base`).should(`not.exist`)
51+
cy.getTestElement(`title`).should(`not.exist`)
52+
cy.getTestElement(`meta`).should(`not.exist`)
53+
cy.getTestElement(`noscript`).should(`not.exist`)
54+
cy.getTestElement(`style`).should(`not.exist`)
55+
cy.getTestElement(`link`).should(`not.exist`)
56+
})
57+
58+
/**
59+
* Technically nodes are always removed from the DOM and new ones added (in other words nodes are not reused with different data),
60+
* but since this is an implementation detail we'll still test the behavior we expect as if we didn't know that.
61+
*/
62+
it(`should change meta tag values`, () => {
63+
// Initial load
64+
cy.visit(page.basic).waitForRouteChange()
65+
66+
// Validate data from initial load
67+
cy.getTestElement(`base`)
68+
.invoke(`attr`, `href`)
69+
.should(`equal`, data.static.base)
70+
cy.getTestElement(`title`).should(`have.text`, data.static.title)
71+
cy.getTestElement(`meta`)
72+
.invoke(`attr`, `content`)
73+
.should(`equal`, data.static.meta)
74+
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
75+
cy.getTestElement(`style`).should(`contain`, data.static.style)
76+
cy.getTestElement(`link`)
77+
.invoke(`attr`, `href`)
78+
.should(`equal`, data.static.link)
79+
80+
// Navigate to a different page via Gatsby Link
81+
cy.getTestElement(`gatsby-link`).click()
82+
83+
// Validate data on navigated-to page
84+
cy.getTestElement(`base`)
85+
.invoke(`attr`, `href`)
86+
.should(`equal`, data.queried.base)
87+
cy.getTestElement(`title`).should(`have.text`, data.queried.title)
88+
cy.getTestElement(`meta`)
89+
.invoke(`attr`, `content`)
90+
.should(`equal`, data.queried.meta)
91+
cy.getTestElement(`noscript`).should(`have.text`, data.queried.noscript)
92+
cy.getTestElement(`style`).should(`contain`, data.queried.style)
93+
cy.getTestElement(`link`)
94+
.invoke(`attr`, `href`)
95+
.should(`equal`, data.queried.link)
96+
97+
// Navigate back to original page via Gatsby Link
98+
cy.getTestElement(`gatsby-link`).click().waitForRouteChange()
99+
100+
// Validate data is same as initial load
101+
cy.getTestElement(`base`)
102+
.invoke(`attr`, `href`)
103+
.should(`equal`, data.static.base)
104+
cy.getTestElement(`title`).should(`have.text`, data.static.title)
105+
cy.getTestElement(`meta`)
106+
.invoke(`attr`, `content`)
107+
.should(`equal`, data.static.meta)
108+
cy.getTestElement(`noscript`).should(`have.text`, data.static.noscript)
109+
cy.getTestElement(`style`).should(`contain`, data.static.style)
110+
cy.getTestElement(`link`)
111+
.invoke(`attr`, `href`)
112+
.should(`equal`, data.static.link)
113+
})
114+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
describe(`Tsx Pages`, () => {
2+
it(`Works with Head export`, () => {
3+
cy.visit(`/head-function-export/tsx-page`)
4+
5+
cy.getTestElement(`title`).should(`contain`, `TypeScript`)
6+
7+
cy.getTestElement(`name`)
8+
.invoke(`attr`, `content`)
9+
.should(`equal`, `TypeScript`)
10+
})
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { VALID_NODE_NAMES } from "gatsby/cache-dir/head/constants"
2+
import { page } from "../../../shared-data/head-function-export.js"
3+
4+
describe(`Head function export should warn`, () => {
5+
beforeEach(() => {
6+
cy.visit(page.warnings, {
7+
onBeforeLoad(win) {
8+
cy.stub(win.console, `warn`).as(`consoleWarn`)
9+
},
10+
}).waitForRouteChange()
11+
})
12+
13+
it(`for elements that belong in the body`, () => {
14+
cy.get(`@consoleWarn`).should(
15+
`be.calledWith`,
16+
`<h1> is not a valid head element. Please use one of the following: ${VALID_NODE_NAMES.join(
17+
`, `
18+
)}`
19+
)
20+
})
21+
22+
it(`for scripts that could use the script component`, () => {
23+
cy.get(`@consoleWarn`).should(
24+
`be.calledWith`,
25+
`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/`
26+
)
27+
})
28+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { data } = require("../../../shared-data/head-function-export")
2+
3+
const TEST_ID = `extra-meta-for-hot-reloading`
4+
5+
describe(`hot reloading Head export`, () => {
6+
beforeEach(() => {
7+
cy.visit(`/head-function-export/basic`).waitForRouteChange()
8+
})
9+
10+
it(`displays placeholder content on launch`, () => {
11+
cy.getTestElement(TEST_ID)
12+
.invoke(`attr`, `content`)
13+
.should(`contain.equal`, "%SOME_EXTRA_META%")
14+
})
15+
16+
it(`hot reloads with new content`, () => {
17+
const text = `New Title by HMR`
18+
cy.exec(
19+
`npm run update -- --file src/pages/head-function-export/basic.js --replacements "SOME_EXTRA_META:${text}"`
20+
)
21+
22+
cy.waitForHmr()
23+
24+
cy.getTestElement(TEST_ID)
25+
.invoke(`attr`, `content`)
26+
.should(`contain.equal`, text)
27+
})
28+
})

0 commit comments

Comments
 (0)