rsnext/test/e2e/app-dir/next-font/next-font.test.ts
Josh Story 4f5f4769e5
preload fonts using ReactDOM.preload (#48931)
This PR updates the way we preload fonts. Previously we tracked which
fonts we needed to preload for each layer and rendered a `<link
rel="preload" href="..." as="font" />` tag for each preloadable font.
This unfortunately gets blocked by data fetching and we want to be able
to hint these preloads as soon as possible. Now that React support Float
methods in RSC we can use `ReactDOM.preload(..., { as: "font" })` to
implement this functionality

This PR makes the following changes
1. expose a `preloadFont` method through the RSC graph
2. expose a `preconnect` metho through the RSC graph
3. refactor the preloads generation to use `preloadFont` instead of
rendering a preload link
4. If there are no fonts to preload but fonts are being used in CSS then
a `preconnect` asset origin is called instead of rendering a preconnect
link
5. instead of emitting a data attribute per font preload indicating
whether the project is using size-adjust we now emit a single global
meta tag. In the future we may get more granular about which fonts are
being size adjusted. In the meantime the current hueristic is to add
`-s` to the filename so it can still be inferred.

In the process of completing this work I discovered there were some bugs
in how the preconnect logic was originally implemented. Previously it
was possible to get multiple preconnects per render. Additionally the
preconnect href was always `"/"` which is not correct if you are hosting
your fonts at a CDN. The refactor fixed both of these issues

I want to do a larger refactor of the asset loading logic in App-Render
but I'll save that for a couple weeks from now

Additionally, the serialized output of preloads now omits the word
anonymous when using crossorigin so tests were updated to reflect
`crossorigin=""`

Additionally, tests were updated to no longer look for the size-adjust
data attribute on preloads

Additionally, There is a note about leaving a `{null}` render in place
to avoid a conflict with how the router models lazy trees. I'll follow
up with a PR addressing this

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-04-29 00:50:20 +02:00

424 lines
14 KiB
TypeScript

import { createNextDescribe, FileRef } from 'e2e-utils'
import { getRedboxSource, hasRedbox } from 'next-test-utils'
import { join } from 'path'
const getAttrs = (elems: Cheerio) =>
Array.from(elems)
.map((elem) => elem.attribs)
// There is something weord that causes different machines to have different order of things
// My machine behaves differently to CI
.sort((a, b) => (a.href < b.href ? -1 : 1))
describe.each([['app'], ['app-old']])('%s', (fixture: string) => {
createNextDescribe(
'app dir next-font',
{
files: {
app: new FileRef(join(__dirname, fixture)),
fonts: new FileRef(join(__dirname, 'fonts')),
'next.config.js': new FileRef(join(__dirname, 'next.config.js')),
},
dependencies: {
'@next/font': 'canary',
},
skipDeployment: true,
},
({ next, isNextDev: isDev }) => {
describe('import values', () => {
it('should have correct values at /', async () => {
const $ = await next.render$('/')
// layout
expect(JSON.parse($('#root-layout').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
variable: expect.stringMatching(/^__variable_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font1_.{6}'$/),
},
})
// page
expect(JSON.parse($('#root-page').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
variable: expect.stringMatching(/^__variable_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font2_.{6}'$/),
},
})
// Comp
expect(JSON.parse($('#root-comp').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font3_.{6}'$/),
fontStyle: 'italic',
fontWeight: 900,
},
})
})
it('should have correct values at /client', async () => {
const $ = await next.render$('/client')
// root layout
expect(JSON.parse($('#root-layout').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
variable: expect.stringMatching(/^__variable_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font1_.{6}'$/),
},
})
// layout
expect(JSON.parse($('#client-layout').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font4_.{6}'$/),
fontWeight: 100,
},
})
// page
expect(JSON.parse($('#client-page').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font5_.{6}'$/),
fontStyle: 'italic',
},
})
// Comp
expect(JSON.parse($('#client-comp').text())).toEqual({
className: expect.stringMatching(/^__className_.{6}$/),
style: {
fontFamily: expect.stringMatching(/^'__font6_.{6}'$/),
},
})
})
})
describe('computed styles', () => {
it('should have correct styles at /', async () => {
const browser = await next.browser('/')
// layout
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontFamily'
)
).toMatch(/^__font1_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontWeight'
)
).toBe('400')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontStyle'
)
).toBe('normal')
// page
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-page")).fontFamily'
)
).toMatch(/^__font2_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-page")).fontWeight'
)
).toBe('400')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-page")).fontStyle'
)
).toBe('normal')
// Comp
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-comp")).fontFamily'
)
).toMatch(/^__font3_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-comp")).fontWeight'
)
).toBe('900')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-comp")).fontStyle'
)
).toBe('italic')
})
it('should have correct styles at /client', async () => {
const browser = await next.browser('/client')
// root layout
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontFamily'
)
).toMatch(/^__font1_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontWeight'
)
).toBe('400')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#root-layout")).fontStyle'
)
).toBe('normal')
// layout
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-layout")).fontFamily'
)
).toMatch(/^__font4_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-layout")).fontWeight'
)
).toBe('100')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-layout")).fontStyle'
)
).toBe('normal')
// page
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-page")).fontFamily'
)
).toMatch(/^__font5_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-page")).fontWeight'
)
).toBe('400')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-page")).fontStyle'
)
).toBe('italic')
// Comp
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-comp")).fontFamily'
)
).toMatch(/^__font6_.{6}$/)
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-comp")).fontWeight'
)
).toBe('400')
expect(
await browser.eval(
'getComputedStyle(document.querySelector("#client-comp")).fontStyle'
)
).toBe('normal')
})
})
if (!isDev) {
describe('preload', () => {
it('should preload correctly with server components', async () => {
const $ = await next.render$('/')
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(0)
// From root layout
expect($('link[as="font"]').length).toBe(3)
expect(getAttrs($('link[as="font"]'))).toEqual([
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/b2104791981359ae-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/b61859a50be14c53-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/e9b9dc0d8ba35f48-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
])
})
it('should preload correctly with client components', async () => {
const $ = await next.render$('/client')
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(0)
// From root layout
expect(getAttrs($('link[as="font"]'))).toEqual([
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/e1053f04babc7571-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/e9b9dc0d8ba35f48-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/feab2c68f2a8e9a4-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
])
})
it('should preload correctly with layout using fonts', async () => {
const $ = await next.render$('/layout-with-fonts')
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(0)
// From root layout
expect(getAttrs($('link[as="font"]'))).toEqual([
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/75c5faeeb9c86969-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/e9b9dc0d8ba35f48-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
])
})
it('should preload correctly with page using fonts', async () => {
const $ = await next.render$('/page-with-fonts')
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(0)
// From root layout
expect(getAttrs($('link[as="font"]'))).toEqual([
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/568e4c6d8123c4d6-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
{
as: 'font',
crossorigin: '',
href: '/_next/static/media/e9b9dc0d8ba35f48-s.p.woff2',
rel: 'preload',
type: 'font/woff2',
},
])
})
})
describe('preconnect', () => {
it.each([['page'], ['layout'], ['component']])(
'should add preconnect when preloading is disabled in %s',
async (type: string) => {
const $ = await next.render$(`/preconnect-${type}`)
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(1)
expect($('link[rel="preconnect"]').get(0).attribs).toEqual({
crossorigin: '',
href: '/',
rel: 'preconnect',
})
// Preload
expect($('link[as="font"]').length).toBe(0)
}
)
it('should not preconnect when css is used but no fonts', async () => {
const $ = await next.render$('/no-preconnect')
// Preconnect
expect($('link[rel="preconnect"]').length).toBe(0)
// Preload
expect(getAttrs($('link[as="font"]'))).toEqual([])
})
})
}
describe('navigation', () => {
it('should not have duplicate preload tags on navigation', async () => {
const browser = await next.browser('/navigation')
// Before navigation, root layout imports the font
const preloadBeforeNavigation = await browser.elementsByCss(
'link[as="font"]'
)
expect(preloadBeforeNavigation.length).toBe(1)
expect(await preloadBeforeNavigation[0].getAttribute('href')).toBe(
'/_next/static/media/c287665b44f047d4-s.p.woff2'
)
// Navigate to a page that also imports that font
await browser.elementByCss('a').click()
await browser.waitForElementByCss('#page-with-same-font')
// After navigating
const preloadAfterNavigation = await browser.elementsByCss(
'link[as="font"]'
)
expect(preloadAfterNavigation.length).toBe(1)
expect(await preloadAfterNavigation[0].getAttribute('href')).toBe(
'/_next/static/media/c287665b44f047d4-s.p.woff2'
)
})
})
if (isDev) {
describe('Dev errors', () => {
it('should recover on font loader error', async () => {
const browser = await next.browser('/')
const font1Content = await next.readFile('fonts/index.js')
// Break file
await next.patchFile(
'fonts/index.js',
font1Content.replace('./font1.woff2', './does-not-exist.woff2')
)
expect(await hasRedbox(browser, true)).toBeTrue()
expect(await getRedboxSource(browser)).toInclude(
"Can't resolve './does-not-exist.woff2'"
)
// Fix file
await next.patchFile('fonts/index.js', font1Content)
await browser.waitForElementByCss('#root-page')
})
})
}
}
)
})