未验证 提交 6895f9b0 编写于 作者: J Joe Haddad 提交者: GitHub

Replace <link rel=stylesheet> client-side transitions with <style> tags (#16581)

This pull request replaces our client-side style transitions with `<style>` tags over async `<link rel=stylesheet>` tags. This should fix some edge cases users see with Chrome accidentally causing a FOUC.

This also removes the need to perform an async operation before starting the render, which should remove any perceivable navigation delay.

---

Fixes #16289
上级 990c1792
...@@ -17,7 +17,7 @@ import * as envConfig from '../next-server/lib/runtime-config' ...@@ -17,7 +17,7 @@ import * as envConfig from '../next-server/lib/runtime-config'
import { getURL, loadGetInitialProps, ST } from '../next-server/lib/utils' import { getURL, loadGetInitialProps, ST } from '../next-server/lib/utils'
import type { NEXT_DATA } from '../next-server/lib/utils' import type { NEXT_DATA } from '../next-server/lib/utils'
import initHeadManager from './head-manager' import initHeadManager from './head-manager'
import PageLoader, { createLink } from './page-loader' import PageLoader, { StyleSheetTuple } from './page-loader'
import measureWebVitals from './performance-relayer' import measureWebVitals from './performance-relayer'
import { createRouter, makePublicRouterInstance } from './router' import { createRouter, makePublicRouterInstance } from './router'
...@@ -84,13 +84,25 @@ if (hasBasePath(asPath)) { ...@@ -84,13 +84,25 @@ if (hasBasePath(asPath)) {
type RegisterFn = (input: [string, () => void]) => void type RegisterFn = (input: [string, () => void]) => void
const looseToArray = <T extends {}>(input: any): T[] => [].slice.call(input)
const pageLoader = new PageLoader( const pageLoader = new PageLoader(
buildId, buildId,
prefix, prefix,
page, page,
[].slice looseToArray<CSSStyleSheet>(document.styleSheets)
.call(document.querySelectorAll('link[rel=stylesheet][data-n-p]')) .filter(
.map((e: HTMLLinkElement) => e.getAttribute('href')!) (el: CSSStyleSheet) =>
el.ownerNode &&
(el.ownerNode as Element).tagName === 'LINK' &&
(el.ownerNode as Element).hasAttribute('data-n-p')
)
.map((sheet) => ({
href: (sheet.ownerNode as Element).getAttribute('href')!,
text: looseToArray<CSSRule>(sheet.cssRules)
.map((r) => r.cssText)
.join(''),
}))
) )
const register: RegisterFn = ([r, f]) => pageLoader.registerPage(r, f) const register: RegisterFn = ([r, f]) => pageLoader.registerPage(r, f)
if (window.__NEXT_P) { if (window.__NEXT_P) {
...@@ -109,7 +121,7 @@ let lastRenderReject: (() => void) | null ...@@ -109,7 +121,7 @@ let lastRenderReject: (() => void) | null
let webpackHMR: any let webpackHMR: any
export let router: Router export let router: Router
let CachedComponent: React.ComponentType let CachedComponent: React.ComponentType
let cachedStyleSheets: string[] let cachedStyleSheets: StyleSheetTuple[]
let CachedApp: AppComponent, onPerfEntry: (metric: any) => void let CachedApp: AppComponent, onPerfEntry: (metric: any) => void
class Container extends React.Component<{ class Container extends React.Component<{
...@@ -574,8 +586,8 @@ function doRender({ ...@@ -574,8 +586,8 @@ function doRender({
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error. // lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
lastAppProps = appProps lastAppProps = appProps
let canceled = false
let resolvePromise: () => void let resolvePromise: () => void
let renderPromiseReject: () => void
const renderPromise = new Promise((resolve, reject) => { const renderPromise = new Promise((resolve, reject) => {
if (lastRenderReject) { if (lastRenderReject) {
lastRenderReject() lastRenderReject()
...@@ -584,7 +596,8 @@ function doRender({ ...@@ -584,7 +596,8 @@ function doRender({
lastRenderReject = null lastRenderReject = null
resolve() resolve()
} }
renderPromiseReject = lastRenderReject = () => { lastRenderReject = () => {
canceled = true
lastRenderReject = null lastRenderReject = null
const error: any = new Error('Cancel rendering route') const error: any = new Error('Cancel rendering route')
...@@ -593,12 +606,9 @@ function doRender({ ...@@ -593,12 +606,9 @@ function doRender({
} }
}) })
// TODO: consider replacing this with real `<style>` tags that have // This function has a return type to ensure it doesn't start returning a
// plain-text CSS content that's provided by RouteInfo. That'd remove the // Promise. It should remain synchronous.
// need for the staging `<link>`s and the ability for CSS to be missing at function onStart(): boolean {
// this phase, allowing us to remove the error handling flow that reloads the
// page.
function onStart(): Promise<void[]> {
if ( if (
// We can skip this during hydration. Running it wont cause any harm, but // We can skip this during hydration. Running it wont cause any harm, but
// we may as well save the CPU cycles. // we may as well save the CPU cycles.
...@@ -607,78 +617,27 @@ function doRender({ ...@@ -607,78 +617,27 @@ function doRender({
// unless we're in production: // unless we're in production:
process.env.NODE_ENV !== 'production' process.env.NODE_ENV !== 'production'
) { ) {
return Promise.resolve([]) return false
} }
// Clean up previous render if canceling: const currentStyleTags = looseToArray<HTMLStyleElement>(
;([].slice.call( document.querySelectorAll('style[data-n-href]')
document.querySelectorAll( )
'link[data-n-staging], noscript[data-n-staging]' const currentHrefs = new Set(
) currentStyleTags.map((tag) => tag.getAttribute('data-n-href'))
) as HTMLLinkElement[]).forEach((el) => {
el.parentNode!.removeChild(el)
})
const referenceNodes: HTMLLinkElement[] = [].slice.call(
document.querySelectorAll('link[data-n-g], link[data-n-p]')
) as HTMLLinkElement[]
const referenceHrefs = new Set(
referenceNodes.map((e) => e.getAttribute('href'))
) )
let referenceNode: Element | undefined =
referenceNodes[referenceNodes.length - 1]
const required: (Promise<any> | true)[] = styleSheets.map((href) => {
let newNode: Element, promise: Promise<any> | true
const existingLink = referenceHrefs.has(href)
if (existingLink) {
newNode = document.createElement('noscript')
newNode.setAttribute('data-n-staging', href)
promise = true
} else {
const [link, onload] = createLink(href, 'stylesheet')
link.setAttribute('data-n-staging', '')
// Media `none` does not work in Firefox, so `print` is more
// cross-browser. Since this is so short lived we don't have to worry
// about style thrashing in a print view (where no routing is going to be
// happening anyway).
link.setAttribute('media', 'print')
newNode = link
promise = onload
}
if (referenceNode) { styleSheets.forEach(({ href, text }) => {
referenceNode.parentNode!.insertBefore( if (!currentHrefs.has(href)) {
newNode, const styleTag = document.createElement('style')
referenceNode.nextSibling styleTag.setAttribute('data-n-href', href)
) styleTag.setAttribute('media', 'x')
referenceNode = newNode
} else { document.head.appendChild(styleTag)
document.head.appendChild(newNode) styleTag.appendChild(document.createTextNode(text))
} }
return promise
})
return Promise.all(required).catch(() => {
// This is too late in the rendering lifecycle to use the existing
// `PAGE_LOAD_ERROR` flow (via `handleRouteInfoError`).
// To match that behavior, we request the page to reload with the current
// asPath. This is already set at this phase since we "committed" to the
// render.
// This handles an edge case where a new deployment is rolled during
// client-side transition and the CSS assets are missing.
// This prevents:
// 1. An unstyled page from being rendered (old behavior)
// 2. The `/_error` page being rendered (we want to reload for the new
// deployment)
window.location.href = router.asPath
// Instead of rethrowing the CSS loading error, we give a promise that
// won't resolve. This pauses the rendering process until the page
// reloads. Re-throwing the error could result in a flash of error page.
// throw cssLoadingError
return new Promise(() => {})
}) })
return true
} }
function onCommit() { function onCommit() {
...@@ -689,40 +648,58 @@ function doRender({ ...@@ -689,40 +648,58 @@ function doRender({
// We can skip this during hydration. Running it wont cause any harm, but // We can skip this during hydration. Running it wont cause any harm, but
// we may as well save the CPU cycles: // we may as well save the CPU cycles:
!isInitialRender && !isInitialRender &&
// Ensure this render commit owns the currently staged stylesheets: // Ensure this render was not canceled
renderPromiseReject === lastRenderReject !canceled
) { ) {
// Remove or relocate old stylesheets: const desiredHrefs = new Set(styleSheets.map((s) => s.href))
const relocatePlaceholders = [].slice.call( const currentStyleTags = looseToArray<HTMLStyleElement>(
document.querySelectorAll('noscript[data-n-staging]') document.querySelectorAll('style[data-n-href]')
) as HTMLElement[]
const relocateHrefs = relocatePlaceholders.map((e) =>
e.getAttribute('data-n-staging')
) )
;([].slice.call( const currentHrefs = currentStyleTags.map(
document.querySelectorAll('link[data-n-p]') (tag) => tag.getAttribute('data-n-href')!
) as HTMLLinkElement[]).forEach((el) => { )
const currentHref = el.getAttribute('href')
const relocateIndex = relocateHrefs.indexOf(currentHref) // Toggle `<style>` tags on or off depending on if they're needed:
if (relocateIndex !== -1) { for (let idx = 0; idx < currentHrefs.length; ++idx) {
const placeholderElement = relocatePlaceholders[relocateIndex] if (desiredHrefs.has(currentHrefs[idx])) {
placeholderElement.parentNode?.replaceChild(el, placeholderElement) currentStyleTags[idx].removeAttribute('media')
} else { } else {
el.parentNode!.removeChild(el) currentStyleTags[idx].setAttribute('media', 'x')
} }
}) }
// Activate new stylesheets: // Reorder styles into intended order:
;[].slice let referenceNode = document.querySelector('noscript[data-n-css]')
.call(document.querySelectorAll('link[data-n-staging]')) if (
.forEach((el: HTMLLinkElement) => { // This should be an invariant:
el.removeAttribute('data-n-staging') referenceNode
el.removeAttribute('media') ) {
el.setAttribute('data-n-p', '') styleSheets.forEach(({ href }) => {
const targetTag = document.querySelector(
`style[data-n-href="${href}"]`
)
if (
// This should be an invariant:
targetTag
) {
referenceNode!.parentNode!.insertBefore(
targetTag,
referenceNode!.nextSibling
)
referenceNode = targetTag
}
}) })
}
// Finally, clean up server rendered stylesheets:
looseToArray<HTMLLinkElement>(
document.querySelectorAll('link[data-n-p]')
).forEach((el) => {
el.parentNode!.removeChild(el)
})
// Force browser to recompute layout, which prevents a flash of unstyled // Force browser to recompute layout, which should prevent a flash of
// content: // unstyled content:
getComputedStyle(document.body, 'height') getComputedStyle(document.body, 'height')
} }
...@@ -737,33 +714,19 @@ function doRender({ ...@@ -737,33 +714,19 @@ function doRender({
</Root> </Root>
) )
onStart()
// We catch runtime errors using componentDidCatch which will trigger renderError // We catch runtime errors using componentDidCatch which will trigger renderError
return Promise.race([ renderReactElement(
// Download required CSS assets first: process.env.__NEXT_STRICT_MODE ? (
onStart() <React.StrictMode>{elem}</React.StrictMode>
.then(() => { ) : (
// Ensure a new render has not been started: elem
if (renderPromiseReject === lastRenderReject) { ),
// Queue rendering: appElement!
renderReactElement( )
process.env.__NEXT_STRICT_MODE ? (
<React.StrictMode>{elem}</React.StrictMode> return renderPromise
) : (
elem
),
appElement!
)
}
})
.then(
() =>
// Wait for rendering to complete:
renderPromise
),
// Bail early on route cancelation (rejection):
renderPromise,
])
} }
function Root({ function Root({
......
...@@ -33,6 +33,7 @@ const relPrefetch = ...@@ -33,6 +33,7 @@ const relPrefetch =
'prefetch' 'prefetch'
const relPreload = hasRel('preload') ? 'preload' : relPrefetch const relPreload = hasRel('preload') ? 'preload' : relPrefetch
const relPreloadStyle = 'fetch'
const hasNoModule = 'noModule' in document.createElement('script') const hasNoModule = 'noModule' in document.createElement('script')
...@@ -51,33 +52,27 @@ function normalizeRoute(route: string) { ...@@ -51,33 +52,27 @@ function normalizeRoute(route: string) {
return route.replace(/\/$/, '') return route.replace(/\/$/, '')
} }
export function createLink( function appendLink(
href: string, href: string,
rel: string, rel: string,
as?: string, as?: string,
link?: HTMLLinkElement link?: HTMLLinkElement
): [HTMLLinkElement, Promise<any>] { ): Promise<any> {
link = document.createElement('link') return new Promise((res, rej) => {
return [ link = document.createElement('link')
link,
new Promise((res, rej) => { // The order of property assignment here is intentional:
// The order of property assignment here is intentional: if (as) link!.as = as
if (as) link!.as = as link!.rel = rel
link!.rel = rel link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN! link!.onload = res
link!.onload = res link!.onerror = rej
link!.onerror = rej
// `href` should always be last:
// `href` should always be last: link!.href = href
link!.href = href
}),
]
}
function appendLink(href: string, rel: string, as?: string): Promise<any> { document.head.appendChild(link)
const [link, res] = createLink(href, rel, as) })
document.head.appendChild(link)
return res
} }
function loadScript(url: string): Promise<any> { function loadScript(url: string): Promise<any> {
...@@ -94,16 +89,17 @@ function loadScript(url: string): Promise<any> { ...@@ -94,16 +89,17 @@ function loadScript(url: string): Promise<any> {
}) })
} }
export type StyleSheetTuple = { href: string; text: string }
export type GoodPageCache = { export type GoodPageCache = {
page: ComponentType page: ComponentType
mod: any mod: any
styleSheets: string[] styleSheets: StyleSheetTuple[]
} }
export type PageCacheEntry = { error: any } | GoodPageCache export type PageCacheEntry = { error: any } | GoodPageCache
export default class PageLoader { export default class PageLoader {
private initialPage: string private initialPage: string
private initialStyleSheets: string[] private initialStyleSheets: StyleSheetTuple[]
private buildId: string private buildId: string
private assetPrefix: string private assetPrefix: string
private pageCache: Record<string, PageCacheEntry> private pageCache: Record<string, PageCacheEntry>
...@@ -117,7 +113,7 @@ export default class PageLoader { ...@@ -117,7 +113,7 @@ export default class PageLoader {
buildId: string, buildId: string,
assetPrefix: string, assetPrefix: string,
initialPage: string, initialPage: string,
initialStyleSheets: string[] initialStyleSheets: StyleSheetTuple[]
) { ) {
this.initialPage = initialPage this.initialPage = initialPage
this.initialStyleSheets = initialStyleSheets this.initialStyleSheets = initialStyleSheets
...@@ -348,7 +344,7 @@ export default class PageLoader { ...@@ -348,7 +344,7 @@ export default class PageLoader {
// wait for these to resolve. To prevent an unhandled // wait for these to resolve. To prevent an unhandled
// rejection, we swallow the error which is handled later in // rejection, we swallow the error which is handled later in
// the rendering cycle (this is just a preload optimization). // the rendering cycle (this is just a preload optimization).
appendLink(d, relPreload, 'style').catch(() => { appendLink(d, relPreload, relPreloadStyle).catch(() => {
/* ignore preload error */ /* ignore preload error */
}) })
} }
...@@ -381,7 +377,7 @@ export default class PageLoader { ...@@ -381,7 +377,7 @@ export default class PageLoader {
// This method if called by the route code. // This method if called by the route code.
registerPage(route: string, regFn: () => any) { registerPage(route: string, regFn: () => any) {
const register = (styleSheets: string[]) => { const register = (styleSheets: StyleSheetTuple[]) => {
try { try {
const mod = regFn() const mod = regFn()
const pageData: PageCacheEntry = { const pageData: PageCacheEntry = {
...@@ -419,7 +415,14 @@ export default class PageLoader { ...@@ -419,7 +415,14 @@ export default class PageLoader {
} }
} }
const promisedDeps: Promise<string[]> = function fetchStyleSheet(href: string): Promise<StyleSheetTuple> {
return fetch(href).then((res) => {
if (!res.ok) throw pageLoadError(href)
return res.text().then((text) => ({ href, text }))
})
}
const promisedDeps: Promise<StyleSheetTuple[]> =
// Shared styles will already be on the page: // Shared styles will already be on the page:
route === '/_app' || route === '/_app' ||
// We use `style-loader` in development: // We use `style-loader` in development:
...@@ -430,8 +433,14 @@ export default class PageLoader { ...@@ -430,8 +433,14 @@ export default class PageLoader {
: // Tests that this does not block hydration: : // Tests that this does not block hydration:
// test/integration/css-fixtures/hydrate-without-deps/ // test/integration/css-fixtures/hydrate-without-deps/
this.getDependencies(route) this.getDependencies(route)
.then((deps) => deps.filter((d) => d.endsWith('.css')))
.then((cssFiles) =>
// These files should've already been fetched by now, so this
// should resolve pretty much instantly.
Promise.all(cssFiles.map((d) => fetchStyleSheet(d)))
)
promisedDeps.then( promisedDeps.then(
(deps) => register(deps.filter((d) => d.endsWith('.css'))), (deps) => register(deps),
(error) => { (error) => {
this.pageCache[route] = { error } this.pageCache[route] = { error }
this.pageRegisterEvents.emit(route, { error }) this.pageRegisterEvents.emit(route, { error })
...@@ -478,7 +487,7 @@ export default class PageLoader { ...@@ -478,7 +487,7 @@ export default class PageLoader {
appendLink( appendLink(
url, url,
relPrefetch, relPrefetch,
url.endsWith('.css') ? 'style' : 'script' url.endsWith('.css') ? relPreloadStyle : 'script'
), ),
process.env.NODE_ENV === 'production' && process.env.NODE_ENV === 'production' &&
!isDependency && !isDependency &&
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
normalizePathTrailingSlash, normalizePathTrailingSlash,
removePathTrailingSlash, removePathTrailingSlash,
} from '../../../client/normalize-trailing-slash' } from '../../../client/normalize-trailing-slash'
import { GoodPageCache } from '../../../client/page-loader' import { GoodPageCache, StyleSheetTuple } from '../../../client/page-loader'
import { denormalizePagePath } from '../../server/denormalize-page-path' import { denormalizePagePath } from '../../server/denormalize-page-path'
import mitt, { MittEmitter } from '../mitt' import mitt, { MittEmitter } from '../mitt'
import { import {
...@@ -159,7 +159,7 @@ export type PrefetchOptions = { ...@@ -159,7 +159,7 @@ export type PrefetchOptions = {
export type PrivateRouteInfo = { export type PrivateRouteInfo = {
Component: ComponentType Component: ComponentType
styleSheets: string[] styleSheets: StyleSheetTuple[]
__N_SSG?: boolean __N_SSG?: boolean
__N_SSP?: boolean __N_SSP?: boolean
props?: Record<string, any> props?: Record<string, any>
...@@ -268,7 +268,7 @@ export default class Router implements BaseRouter { ...@@ -268,7 +268,7 @@ export default class Router implements BaseRouter {
initialProps: any initialProps: any
pageLoader: any pageLoader: any
Component: ComponentType Component: ComponentType
initialStyleSheets: string[] initialStyleSheets: StyleSheetTuple[]
App: AppComponent App: AppComponent
wrapApp: (App: AppComponent) => any wrapApp: (App: AppComponent) => any
err?: Error err?: Error
......
...@@ -504,6 +504,7 @@ export class Head extends Component< ...@@ -504,6 +504,7 @@ export class Head extends Component<
{process.env.__NEXT_OPTIMIZE_FONTS {process.env.__NEXT_OPTIMIZE_FONTS
? this.makeStylesheetInert(this.getCssLinks(files)) ? this.makeStylesheetInert(this.getCssLinks(files))
: this.getCssLinks(files)} : this.getCssLinks(files)}
<noscript data-n-css />
{!disableRuntimeJS && this.getPreloadDynamicChunks()} {!disableRuntimeJS && this.getPreloadDynamicChunks()}
{!disableRuntimeJS && this.getPreloadMainLinks(files)} {!disableRuntimeJS && this.getPreloadMainLinks(files)}
{this.context.isDevelopment && ( {this.context.isDevelopment && (
......
...@@ -88,7 +88,7 @@ describe('CSS Module client-side navigation in Production', () => { ...@@ -88,7 +88,7 @@ describe('CSS Module client-side navigation in Production', () => {
// Check that Red was preloaded // Check that Red was preloaded
const result = await browser.eval( const result = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel="prefetch"][as="style"]')).map(e=>({href:e.href})).sort()` `[].slice.call(document.querySelectorAll('link[rel="prefetch"][as="fetch"]')).map(e=>({href:e.href})).sort()`
) )
expect(result.length).toBe(1) expect(result.length).toBe(1)
...@@ -96,11 +96,13 @@ describe('CSS Module client-side navigation in Production', () => { ...@@ -96,11 +96,13 @@ describe('CSS Module client-side navigation in Production', () => {
const cssPreloads = await browser.eval( const cssPreloads = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel=preload][href*=".css"]')).map(e=>e.as)` `[].slice.call(document.querySelectorAll('link[rel=preload][href*=".css"]')).map(e=>e.as)`
) )
expect(cssPreloads.every((e) => e === 'style')).toBe(true) expect(cssPreloads.every((e) => e === 'style' || e === 'fetch')).toBe(
true
)
const cssPreloads2 = await browser.eval( const cssPreloads2 = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel=prefetch][href*=".css"]')).map(e=>e.as)` `[].slice.call(document.querySelectorAll('link[rel=prefetch][href*=".css"]')).map(e=>e.as)`
) )
expect(cssPreloads2.every((e) => e === 'style')).toBe(true) expect(cssPreloads2.every((e) => e === 'fetch')).toBe(true)
await browser.elementByCss('#link-red').click() await browser.elementByCss('#link-red').click()
...@@ -245,7 +247,7 @@ describe.skip('CSS Module client-side navigation in Production (Modern)', () => ...@@ -245,7 +247,7 @@ describe.skip('CSS Module client-side navigation in Production (Modern)', () =>
// Check that Red was preloaded // Check that Red was preloaded
const result = await browser.eval( const result = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel="prefetch"][as="style"]')).map(e=>({href:e.href})).sort()` `[].slice.call(document.querySelectorAll('link[rel="prefetch"][as="fetch"]')).map(e=>({href:e.href})).sort()`
) )
expect(result.length).toBe(1) expect(result.length).toBe(1)
...@@ -253,11 +255,13 @@ describe.skip('CSS Module client-side navigation in Production (Modern)', () => ...@@ -253,11 +255,13 @@ describe.skip('CSS Module client-side navigation in Production (Modern)', () =>
const cssPreloads = await browser.eval( const cssPreloads = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel=preload][href*=".css"]')).map(e=>e.as)` `[].slice.call(document.querySelectorAll('link[rel=preload][href*=".css"]')).map(e=>e.as)`
) )
expect(cssPreloads.every((e) => e === 'style')).toBe(true) expect(cssPreloads.every((e) => e === 'style' || e === 'fetch')).toBe(
true
)
const cssPreloads2 = await browser.eval( const cssPreloads2 = await browser.eval(
`[].slice.call(document.querySelectorAll('link[rel=prefetch][href*=".css"]')).map(e=>e.as)` `[].slice.call(document.querySelectorAll('link[rel=prefetch][href*=".css"]')).map(e=>e.as)`
) )
expect(cssPreloads2.every((e) => e === 'style')).toBe(true) expect(cssPreloads2.every((e) => e === 'fetch')).toBe(true)
await browser.elementByCss('#link-red').click() await browser.elementByCss('#link-red').click()
......
...@@ -1133,15 +1133,29 @@ describe('CSS Support', () => { ...@@ -1133,15 +1133,29 @@ describe('CSS Support', () => {
await browser.waitForElementByCss('#link-other').click() await browser.waitForElementByCss('#link-other').click()
await checkRedTitle(browser) await checkRedTitle(browser)
const newPrevSiblingHref = await browser.eval( const newPrevSibling = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').previousSibling.getAttribute('href')` `document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
) )
const newPageHref = await browser.eval( const newPageHref = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').getAttribute('href')` `document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
) )
expect(newPrevSibling).toBeTruthy()
expect(newPageHref).toBeDefined() expect(newPageHref).toBeDefined()
expect(newPrevSiblingHref).toBe(prevSiblingHref)
expect(newPageHref).not.toBe(currentPageHref) expect(newPageHref).not.toBe(currentPageHref)
// Navigate to home:
await browser.waitForElementByCss('#link-index').click()
await checkBlackTitle(browser)
const newPrevSibling2 = await browser.eval(
`document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
)
const newPageHref2 = await browser.eval(
`document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
)
expect(newPrevSibling2).toBeTruthy()
expect(newPageHref2).toBeDefined()
expect(newPageHref2).toBe(currentPageHref)
} finally { } finally {
await browser.close() await browser.close()
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册