From 8c72bcebcd13a23d7baa1b1668d49cb5b4ba9c44 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Wed, 14 Aug 2024 16:54:19 -0400 Subject: [PATCH] [templates/nextjs-xmcloud] Disable image optimization for edit/preview (#1887) --- CHANGELOG.md | 1 + .../src/components/NextImage.test.tsx | 206 +++++++++++++++--- .../src/components/NextImage.tsx | 10 +- 3 files changed, 185 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5499ba87d6..2cbe67eb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Our versioning strategy is as follows: * `[templates/nextjs-sxa]` The background image in the Container component was being generated from the image ID instead of the mediaUrl parameter. This fix changes that behavior. ([#1879](https://github.com/Sitecore/jss/pull/1879)) * `[templates/nextjs-sxa]` The caption of image component has been fixed. ([#1874](https://github.com/Sitecore/jss/pull/1874)) * `[sitecore-jss-nextjs]` A bug has been fixed in the redirect middleware that occurred when a user clicked on a link rendered by the Link component from the Next.js library(next/link). ([#1876](https://github.com/Sitecore/jss/pull/1876)) +* `[sitecore-jss-nextjs]` Disable nextjs image optimization in edit and preview modes. This prevents rendering issues in XM Cloud Pages Edit and Preview. ### 🎉 New Features & Improvements diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx index e39b28c793..21c2a2bfc3 100644 --- a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx +++ b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx @@ -7,14 +7,23 @@ import { NextImage } from './NextImage'; import { ImageField, DefaultEmptyFieldEditingComponentImage, + LayoutServicePageState, + SitecoreContextReactContext, } from '@sitecore-jss/sitecore-jss-react'; -import { ImageLoader } from 'next/image'; +import Image, { ImageLoader } from 'next/image'; import { spy, match } from 'sinon'; import sinonChai from 'sinon-chai'; import { SinonSpy } from 'sinon'; use(sinonChai); +const setContext = spy(); const expect = chai.use(chaiString).expect; +const testContextProps = { + context: { + pageState: LayoutServicePageState.Normal, + }, + setContext, +}; describe('', () => { const HOSTNAME = 'https://cm.jss.localhost'; @@ -39,7 +48,11 @@ describe('', () => { height: 10, }; - const mounted = mount(); + const mounted = mount( + + + + ); const rendered = mounted.find('img'); it('should render image with url', () => { expect(rendered).to.have.lengthOf(1); @@ -64,7 +77,11 @@ describe('', () => { className: 'the-dude-abides', }; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); it('should render image with needed props', () => { expect(rendered).to.have.length(1); @@ -88,9 +105,11 @@ describe('', () => { const field = { value: { src: '/assets/img/test0.png', alt: 'my image', width: 200, height: 400 }, }; - const rendered = mount().find( - 'img' - ); + const rendered = mount( + + + + ).find('img'); expect(rendered).to.have.length(1); expect(rendered.prop('src')).to.equal(`${HOSTNAME}${props.field.value.src}?w=${props.width}`); @@ -112,7 +131,11 @@ describe('', () => { id: 'some-id', className: 'the-dude-abides', }; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); it('should render image component with "value" properties', () => { expect(rendered).to.have.length(1); @@ -141,7 +164,11 @@ describe('', () => { className: 'the-dude-abides', }; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); expect(rendered).to.have.length(1); expect(rendered.prop('src')).to.eql(`${HOSTNAME}${props.field.value.src}?w=${props.width}`); @@ -162,7 +189,11 @@ describe('', () => { editable: false, className: 'the-dude-abides w-100', }; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); it('should render image component with "value" properties', () => { expect(rendered).to.have.length(1); @@ -191,16 +222,25 @@ describe('', () => { imageParams: { foo: 'bar' }, mediaUrlPrefix: /\/([-~]{1})assets\//i, }; - const rendered = mount(); + const rendered = mount( + + + + ); expect(rendered.find('img').prop('src')).to.equal( `${HOSTNAME}/~/jssmedia/img/test0.png?foo=bar&w=8` ); - rendered.setProps({ + const props2 = { ...props, field: { src: '/-assets/img/test0.png' }, - }); - expect(rendered.find('img').prop('src')).to.equal( + }; + const rendered2 = mount( + + + + ); + expect(rendered2.find('img').prop('src')).to.equal( `${HOSTNAME}/-/jssmedia/img/test0.png?foo=bar&w=8` ); expect(mockLoader.called).to.be.true; @@ -223,17 +263,26 @@ describe('', () => { imageParams: { foo: 'bar' }, mediaUrlPrefix: /\/([-~]{1})assets\//i, }; - const rendered = mount(); + const rendered = mount( + + + + ); expect(rendered.find('img').prop('src')).to.equal( `${HOSTNAME}/~/jssmedia/img/test0.png?foo=bar&w=8` ); - rendered.setProps({ + const props2 = { ...props, field: { src: '/-assets/img/test0.png' }, width, height: 10, - }); - expect(rendered.find('img').prop('src')).to.equal( + }; + const rendered2 = mount( + + + + ); + expect(rendered2.find('img').prop('src')).to.equal( `${HOSTNAME}/-/jssmedia/img/test0.png?foo=bar&w=8` ); expect(mockLoader.called).to.be.true; @@ -247,7 +296,11 @@ describe('', () => { it('should render no image when field prop is empty', () => { const img = '' as ImageField; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); expect(rendered).to.have.length(0); }); }); @@ -258,9 +311,13 @@ describe('', () => { const field = { src: '/assets/img/test0.png', }; - expect(() => mount()).to.throw( - 'Detected src prop. If you wish to use src, use next/image directly.' - ); + expect(() => + mount( + + + + ) + ).to.throw('Detected src prop. If you wish to use src, use next/image directly.'); }); }); @@ -276,7 +333,11 @@ describe('', () => { loader: userMockLoader, }; - const rendered = mount().find('img'); + const rendered = mount( + + + + ).find('img'); it('should render image with url', () => { expect(rendered).to.have.lengthOf(1); @@ -291,6 +352,12 @@ describe('', () => { }); describe('editMode metadata', () => { + const testEditingContext = { + ...testContextProps, + context: { + pageState: LayoutServicePageState.Edit, + }, + }; const testMetadata = { contextItem: { id: '{09A07660-6834-476C-B93B-584248D3003B}', @@ -309,14 +376,18 @@ describe('', () => { metadata: testMetadata, }; - const rendered = mount(); - + const rendered = mount( + + + + ); + // we expect imgSrc from nextjs optimizations to be absent in editing/metadata mode expect(rendered.html()).to.equal( [ `${JSON.stringify( testMetadata )}`, - 'my image', + 'my image', '', ].join('') ); @@ -328,7 +399,11 @@ describe('', () => { metadata: testMetadata, }; - const rendered = mount(); + const rendered = mount( + + + + ); const defaultEmptyImagePlaceholder = mount(); expect(rendered.html()).to.equal( [ @@ -347,7 +422,11 @@ describe('', () => { metadata: testMetadata, }; - const rendered = mount(); + const rendered = mount( + + + + ); const defaultEmptyImagePlaceholder = mount(); expect(rendered.html()).to.equal( [ @@ -371,7 +450,9 @@ describe('', () => { ); const rendered = mount( - + + + ); expect(rendered.html()).to.equal( @@ -396,7 +477,9 @@ describe('', () => { ); const rendered = mount( - + + + ); expect(rendered.html()).to.equal( @@ -416,7 +499,11 @@ describe('', () => { metadata: testMetadata, }; - const rendered = mount(); + const rendered = mount( + + + + ); expect(rendered.html()).to.equal(''); }); @@ -427,9 +514,66 @@ describe('', () => { metadata: testMetadata, }; - const rendered = mount(); + const rendered = mount( + + + + ); expect(rendered.html()).to.equal(''); }); }); + + describe('unoptimized property manipulation', () => { + const props = { + field: { value: { src: '/assets/img/test0.png' } }, + width, + height: 10, + id: 'some-id', + className: 'the-dude-abides', + }; + + it('should render unoptimized image in edit mode', () => { + const testEditingContext = { + ...testContextProps, + context: { + pageState: LayoutServicePageState.Edit, + }, + }; + const rendered = mount( + + + + ).find(Image); + expect(rendered.prop('unoptimized')).to.equal(true); + }); + + it('should render unoptimized image in preview mode', () => { + const testEditingContext = { + ...testContextProps, + context: { + pageState: LayoutServicePageState.Preview, + }, + }; + const rendered = mount( + + + + ).find(Image); + expect(rendered.prop('unoptimized')).to.equal(true); + }); + + it('should render respect original unoptimized value in normal mode', () => { + const modifiedProps = { + ...props, + unoptimized: true, + }; + const rendered = mount( + + + + ).find(Image); + expect(rendered.prop('unoptimized')).to.equal(true); + }); + }); }); diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx index 96b4a31c13..9723645e44 100644 --- a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx +++ b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx @@ -7,16 +7,18 @@ import { ImageField, ImageFieldValue, withFieldMetadata, + SitecoreContextReactContext, } from '@sitecore-jss/sitecore-jss-react'; import Image, { ImageProps as NextImageProperties } from 'next/image'; import { withEmptyFieldEditingComponent } from '@sitecore-jss/sitecore-jss-react'; import { DefaultEmptyFieldEditingComponentImage } from '@sitecore-jss/sitecore-jss-react'; -import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; +import { isFieldValueEmpty, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout'; type NextImageProps = ImageProps & Partial; export const NextImage: React.FC = withFieldMetadata( withEmptyFieldEditingComponent( ({ editable = true, imageParams, field, mediaUrlPrefix, fill, priority, ...otherProps }) => { + const sitecoreContext = React.useContext(SitecoreContextReactContext); // next handles src and we use a custom loader, // throw error if these are present if (otherProps.src) { @@ -49,6 +51,11 @@ export const NextImage: React.FC = withFieldMetadata = withFieldMetadata