Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dotcom-rendering/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ if (isChromatic()) {
MockDate.set('Sat Jan 1 2022 12:00:00 GMT+0000 (Greenwich Mean Time)');
}

window.IS_STORYBOOK = true;

mockRESTCalls();

setABTests(
Expand Down
4 changes: 2 additions & 2 deletions dotcom-rendering/cypress/e2e/parallel-1/article.e2e.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('E2E Page rendering', function () {

cy.get('gu-island[name=MostViewedFooterData]', { timeout: 30000 })
.scrollIntoView({ duration: 100 })
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');

cy.get('[data-cy-ab-user-in-variant=ab-test-variant]').should(
'be.visible',
Expand All @@ -104,7 +104,7 @@ describe('E2E Page rendering', function () {

cy.get('gu-island[name=MostViewedFooterData]', { timeout: 30000 })
.scrollIntoView({ duration: 100 })
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');

cy.get('[data-cy-ab-user-in-variant=ab-test-not-in-test]').should(
'be.visible',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe('Interactivity', function () {
// Wait for hydration
cy.get('gu-island[name=MostViewedFooterData]')
.last()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.wait('@getMostRead');
cy.wait('@getMostReadGeo');
cy.get('[data-cy=mostviewed-footer]').should('exist');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('Epics', function () {
// Wait for hydration of the Epic
cy.get('gu-island[name=LiveBlogEpic]')
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.get('[data-cy=contributions-liveblog-epic]').scrollIntoView();
cy.get('[data-cy=contributions-liveblog-epic]').should('be.visible');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]')
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.scrollTo('center');
cy.get(`[data-cy="toast"]`).should('not.exist');
cy.window().then(function (win) {
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]')
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.window().then(function (win) {
win.mockLiveUpdate({
numNewBlocks: 1,
Expand All @@ -92,7 +92,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]', { timeout: 30000 })
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.scrollTo('bottom');
cy.get(`[data-cy="toast"]`).should('not.exist');
cy.window().then(function (win) {
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]')
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.window().then(function (win) {
win.mockLiveUpdate({
numNewBlocks: 1,
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]', { timeout: 30000 })
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.scrollTo('bottom');
cy.get(`[data-cy="toast"]`).should('not.exist');
cy.window().then(function (win) {
Expand All @@ -198,7 +198,7 @@ describe('Liveblogs', function () {
// Wait for hydration
cy.get('gu-island[name=Liveness]')
.first()
.should('have.attr', 'data-island-status', 'hydrated');
.should('have.attr', 'data-island-status', 'rendered');
cy.scrollTo('bottom', { duration: 1000 });
cy.window().then(function (win) {
win.mockLiveUpdate({
Expand Down
5 changes: 4 additions & 1 deletion dotcom-rendering/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ Cypress.Commands.add('hydrate', () => {
const islandMeta = `island: ${name} defer: ${deferuntil}`;

if (['idle', 'visible', undefined].includes(deferuntil)) {
const action = !!el.attr('clientOnly')
? 'rendered'
: 'hydrated';
cy.log(`Scrolling to ${islandMeta}`);
cy.wrap(el)
.scrollIntoView({ duration: 1000, timeout: 30000 })
.should('have.attr', 'data-island-status', 'hydrated', {
.should('have.attr', 'data-island-status', action, {
timeout: 30000,
});
// Additional wait to ensure island defer=visible has triggered
Expand Down
3 changes: 3 additions & 0 deletions dotcom-rendering/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ type RichLinkCardType =
// 3rd party type declarations //
// ------------------------------

declare module 'chromatic/isChromatic';

declare module 'dynamic-import-polyfill' {
export const initialize: ({
modulePath,
Expand Down Expand Up @@ -516,6 +518,7 @@ declare namespace JSX {
name: string;
deferUntil?: 'idle' | 'visible' | 'interaction' | 'hash';
rootMargin?: string;
clientOnly?: boolean;
props: any;
children: React.ReactNode;
/**
Expand Down
45 changes: 31 additions & 14 deletions dotcom-rendering/src/client/islands/doHydration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { EmotionCache } from '@emotion/react';
import { CacheProvider } from '@emotion/react';
import { log, startPerformanceMeasure } from '@guardian/libs';
import { createElement } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { ConfigProvider } from '../../components/ConfigContext';
import type { Config } from '../../types/configContext';

Expand All @@ -13,14 +13,17 @@ declare global {
* Defines the current state of the Island.
* `undefined` at server-side rendering
*/
islandStatus?: 'identified' | 'imported' | 'hydrated';
islandStatus?: 'identified' | 'imported' | 'rendered' | 'hydrated';
}
}

/**
* This function dynamically imports and then hydrates a specific component in
* a specific part of the page
*
* If the content being hydrated is not present in the dom then React renders
* it. This is how portals (non server side rendered content) work
*
* @param name The name of the component we want to hydrate
* @param data The deserialised props we want to use for hydration
* @param element The location on the DOM where the component to hydrate exists
Expand Down Expand Up @@ -49,33 +52,47 @@ export const doHydration = async (
.then((module) => {
/** The duration of importing the module for this island */
const importDuration = endImportPerformanceMeasure();
const clientOnly = element.hasAttribute('clientonly');
element.dataset.islandStatus = 'imported';

const { endPerformanceMeasure: endIslandPerformanceMeasure } =
startPerformanceMeasure('dotcom', name, 'island');

hydrateRoot(
element,
<ConfigProvider value={config}>
<CacheProvider value={emotionCache}>
{createElement(module[name], data)}
</CacheProvider>
</ConfigProvider>,
);
if (clientOnly) {
element.querySelector('[data-name="placeholder"]')?.remove();
const root = createRoot(element);
root.render(
<ConfigProvider value={config}>
<CacheProvider value={emotionCache}>
{createElement(module[name], data)}
</CacheProvider>
</ConfigProvider>,
);
} else {
hydrateRoot(
element,
<ConfigProvider value={config}>
<CacheProvider value={emotionCache}>
{createElement(module[name], data)}
</CacheProvider>
</ConfigProvider>,
);
}

/** The duration of rendering or hydrating this island */
const islandDuration = endIslandPerformanceMeasure();

return { importDuration, islandDuration };
return { clientOnly, importDuration, islandDuration };
})
.then(({ importDuration, islandDuration }) => {
.then(({ clientOnly, importDuration, islandDuration }) => {
if (!('getEntriesByType' in window.performance)) return;

element.dataset.islandStatus = 'hydrated';
const action = clientOnly ? 'rendered' : 'hydrated';
element.dataset.islandStatus = action;

log(
'dotcom',
`🏝 hydrated <${name} /> in ${islandDuration}ms (imported in ${importDuration}ms)`,
`🏝 <${name} /> ${action} in ${islandDuration}ms (imported in ${importDuration}ms)`,
);
})
.catch((error) => {
Expand Down
55 changes: 55 additions & 0 deletions dotcom-rendering/src/client/islands/doStorybookHydration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { ConfigProvider } from '../../components/ConfigContext';
import { getConfig } from './getConfig';
import { getName } from './getName';
import { getProps } from './getProps';

/**
* This is a cut down version of the islands/doHydration function that is
* used as part of the Islands flow for hydrating DCR pages.
*
* We don't use that specific function directly for two reasons:
*
* 1) We need to use React here, not Preact. Otherwise we get errors about
* conflicting versions of React
*
* 2) We don't want to defer hydration as this could affect our Chromatic
* snapshots
*/
export const doStorybookHydration = () => {
for (const element of document.querySelectorAll('gu-island')) {
if (element instanceof HTMLElement) {
const name = getName(element);
const props = getProps(element);
const config = getConfig(element);

if (!name) continue;
if (element.getAttribute('deferuntil') === 'hash') continue;

import(
/* webpackInclude: /\.importable\.tsx$/ */
/* webpackChunkName: "[request]" */
`../../components/${name}.importable`
)
.then((module) => {
element
.querySelector('[data-name="placeholder"]')
?.remove();
const root = createRoot(element);
root.render(
<ConfigProvider value={config}>
{createElement(module[name], props)}
</ConfigProvider>,
);
})
.catch((e) =>
// eslint-disable-next-line no-console -- We want to log here
console.error(
'Failed hydration for storybook environment',
e,
),
);
}
}
};
17 changes: 8 additions & 9 deletions dotcom-rendering/src/client/ophan/ophan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
OphanComponentEvent,
} from '@guardian/libs';
import { log } from '@guardian/libs';
import { isServer } from '../../lib/isServer';
import type { ServerSideTests } from '../../types/config';

export type OphanRecordFunction = (
Expand Down Expand Up @@ -56,15 +55,15 @@ export const getOphan = async (): Promise<
* @deprecated use `getOphan` instead
*/
export const getOphanRecordFunction = (): OphanRecordFunction => {
if (isServer || window.guardian.ophan === undefined) {
// eslint-disable-next-line no-console -- worth informing all users
console.warn('window.guardian.ophan.record is not available');
return () => {
/* do nothing */
};
}
const record = window.guardian.ophan?.record;

return window.guardian.ophan.record;
if (record) return record;

// eslint-disable-next-line no-console -- worth informing all users
console.warn('window.guardian.ophan.record is not available');
return () => {
/* do nothing */
};
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ export const AllEditorialNewslettersPage = ({
/>
<SkipTo id="maincontent" label="Skip to main content" />
<SkipTo id="navigation" label="Skip to navigation" />
<Island deferUntil="idle">
<Island clientOnly={true} deferUntil="idle">
<AlreadyVisited />
</Island>
<Island deferUntil="idle">
<Island clientOnly={true} deferUntil="idle">
<FocusStyles />
</Island>
<Island deferUntil="idle">
<Island clientOnly={true} deferUntil="idle">
<Metrics
commercialMetricsEnabled={
!!newslettersPage.config.switches.commercialMetrics
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isServer } from '../lib/isServer';
import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible';
import type { EmailSignUpProps } from './EmailSignup';
import { EmailSignup } from './EmailSignup';
Expand All @@ -20,7 +19,7 @@ export const AppEmailSignUp = ({
}: AppEmailSignupProps) => {
const isCompatible = useIsBridgetCompatible();

if (!isCompatible || isServer) {
if (!isCompatible) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/ArticleBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export const ArticleBody = ({
dir={languageDirection}
>
{isRecipe(tags) && (
<Island deferUntil="hash">
<Island deferUntil="hash" clientOnly={true}>
<RecipeMultiplier />
</Island>
)}
Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/ArticleMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ export const ArticleMeta = ({
>
<div>
{isCommentable && (
<Island deferUntil="idle">
<Island clientOnly={true} deferUntil="idle">
<CommentCount
discussionApiUrl={discussionApiUrl}
shortUrlId={shortUrlId}
Expand Down
Loading