Skip to content

Commit

Permalink
Merge branch 'master' of github.com:storyofams/storyblok-toolkit
Browse files Browse the repository at this point in the history
  • Loading branch information
BJvdA committed May 3, 2021
2 parents c3559ed + fb9031b commit c8c050a
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 16 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://storyofams.com/" target="_blank" align="center">
<img src="https://storyofams.com/public/[email protected]" alt="Story of AMS" width="120">
<img src="https://avatars.githubusercontent.com/u/19343504" alt="Story of AMS" width="100">
</a>
<h1 align="center">@storyofams/storyblok-toolkit</h1>
<p align="center">
Expand All @@ -17,7 +17,7 @@
<img src="https://img.shields.io/github/stars/storyofams/storyblok-toolkit.svg?style=social&label=Star&maxAge=86400" />
</a>
</p>
<p align="center">Batteries-included toolset for efficient development of React frontends with Storyblok as a headless CMS. <a href="https://storyblok-toolkit.vercel.app/" target="_blank">View docs</a></p>
<p align="center">Batteries-included toolset for efficient development of React frontends with Storyblok as a headless CMS. <br><a href="https://storyblok-toolkit.vercel.app/" target="_blank">View docs</a></p>
</p>

---
Expand Down
17 changes: 10 additions & 7 deletions src/image/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ interface ImageProps
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture#the_media_attribute
*/
media?: string;
/**
* Show a Low-Quality Image Placeholder.
*
* @default true
*/
showPlaceholder?: boolean;
}

export const Image = ({
fixed,
fluid,
height,
showPlaceholder = true,
smart,
width,
ref,
Expand Down Expand Up @@ -62,18 +69,12 @@ export const Image = ({
return;
} else {
// Use IntersectionObserver as fallback
if (observer.current) observer.current.disconnect();

if (imgRef.current) {
addIntersectionObserver();
}

return () => {
if (observer.current) {
if (imgRef.current) {
observer.current.unobserve(imgRef.current);
}

observer.current.disconnect();
}
};
Expand Down Expand Up @@ -113,7 +114,9 @@ export const Image = ({
}}
/>

<Placeholder src={props.src} shouldShow={!isLoaded} />
{showPlaceholder && (
<Placeholder src={props.src} shouldShow={!isLoaded} />
)}

<Picture
{...pictureProps}
Expand Down
10 changes: 3 additions & 7 deletions src/image/Picture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,9 @@ export const Picture = forwardRef(
) => {
const splitSrc = src?.split('/f/');
const webpSrc = `${splitSrc[0]}/filters:format(webp)/f/${splitSrc[1]}`;
let webpSrcset = srcSet || webpSrc;

if (webpSrcset) {
webpSrcset = webpSrcset
.replace(/\/filters:(.*)\/f\//gm, '/filters:$1:format(webp)/f/')
.replace(/\/(?!filters:)([^/]*)\/f\//gm, '/$1/filters:format(webp)/f/');
}
const webpSrcset = (srcSet || webpSrc)
.replace(/\/filters:(.*)\/f\//gm, '/filters:$1:format(webp)/f/')
.replace(/\/(?!filters:)([^/]*)\/f\//gm, '/$1/filters:format(webp)/f/');

return (
<picture>
Expand Down
213 changes: 213 additions & 0 deletions src/image/__tests__/Image.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import * as React from 'react';
import { cleanup, render, act, screen, waitFor } from '@testing-library/react';
import { unmountComponentAtNode } from 'react-dom';

import { createIntersectionObserver } from '../createIntersectionObserver';
import * as helpers from '../helpers';
import { Image } from '../Image';

const storyblokImage =
'https://a.storyblok.com/f/39898/3310x2192/e4ec08624e/demo-image.jpeg';

jest.mock('../createIntersectionObserver');

describe('[image] Image', () => {
afterEach(() => {
cleanup();
jest.restoreAllMocks();
});

it('should render an image with the src to load', async () => {
act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

expect(screen.getByAltText('')).toHaveStyle('opacity: 1');

expect(screen.getByAltText('flowers')).not.toHaveAttribute('src');
expect(screen.getByAltText('flowers')).toHaveAttribute('data-src');
});

it('should let native loading handle loading if supported', async () => {
global.HTMLImageElement.prototype.loading = 'lazy';

act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

expect(screen.getByAltText('flowers')).toHaveAttribute('src');
});

it('should use io as loading fallback', async () => {
global.HTMLImageElement.prototype.loading = undefined;
delete global.HTMLImageElement.prototype.loading;

const setLoadingMock = jest.fn();

jest
.spyOn(React, 'useState')
.mockImplementationOnce(() => [false, setLoadingMock]);

const disconnect = jest.fn();
const createIoMock = jest.fn(() => ({
disconnect,
}));

(createIntersectionObserver as jest.Mock).mockReset();
(createIntersectionObserver as jest.Mock).mockImplementation(createIoMock);

act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

await waitFor(() =>
expect(createIntersectionObserver as jest.Mock).toHaveBeenCalled(),
);

act(() => {
((createIntersectionObserver as jest.Mock).mock as any).calls[0][1]();
});

expect(setLoadingMock).toHaveBeenCalledWith(true);
});

it('should disconnect io on unmount', async () => {
global.HTMLImageElement.prototype.loading = undefined;
delete global.HTMLImageElement.prototype.loading;

const container = document.createElement('div');
document.body.appendChild(container);

jest.spyOn(React, 'useRef').mockReturnValueOnce({
current: { src: storyblokImage },
});

const disconnect = jest.fn();

(createIntersectionObserver as jest.Mock).mockImplementation(() => ({
disconnect,
}));

act(() => {
render(<Image alt="flowers" src={storyblokImage} />, { container });
});

expect(disconnect).not.toHaveBeenCalled();

unmountComponentAtNode(container);

await waitFor(() => expect(disconnect).toHaveBeenCalled());
});

it('should not add io if already loading', async () => {
global.HTMLImageElement.prototype.loading = undefined;

const disconnect = jest.fn();
const createIoMock = jest.fn(() => ({
disconnect,
}));

(createIntersectionObserver as jest.Mock).mockImplementation(createIoMock);

act(() => {
render(<Image alt="flowers" src={storyblokImage} lazy={false} />);
});

expect(createIoMock).not.toHaveBeenCalled();
});

it('should not add io if no image ref', async () => {
global.HTMLImageElement.prototype.loading = undefined;
delete global.HTMLImageElement.prototype.loading;

const disconnect = jest.fn();
const createIoMock = jest.fn(() => ({
disconnect,
}));

(createIntersectionObserver as jest.Mock).mockReset();
(createIntersectionObserver as jest.Mock).mockImplementation(createIoMock);

let ref = {} as any;
Object.defineProperty(ref, 'current', {
get: jest.fn(() => false),
set: jest.fn(),
});
jest.spyOn(React, 'useRef').mockReturnValue(ref);

act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

await waitFor(() =>
expect(createIntersectionObserver as jest.Mock).not.toHaveBeenCalled(),
);

ref = {};
Object.defineProperty(ref, 'current', {
get: jest.fn(() => true),
set: jest.fn(),
});
jest.spyOn(React, 'useRef').mockReturnValueOnce(ref);

act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

await waitFor(() =>
expect(createIntersectionObserver as jest.Mock).toHaveBeenCalled(),
);
});

it('should hide placeholder on load', async () => {
global.HTMLImageElement.prototype.loading = 'lazy';

jest.spyOn(helpers, 'useImageLoader').mockImplementation(() => ({
onLoad: jest.fn(),
isLoaded: true,
setLoaded: jest.fn(),
}));

act(() => {
render(<Image alt="flowers" src={storyblokImage} />);
});

expect(screen.getByAltText('')).toHaveStyle('opacity: 0');
expect(screen.getByAltText('flowers')).toHaveAttribute('src');
expect(screen.getByAltText('flowers')).not.toHaveAttribute('data-src');
});

it('should set loaded if img complete', async () => {
global.HTMLImageElement.prototype.loading = 'lazy';

const setLoaded = jest.fn();

jest.spyOn(console, 'error').mockImplementation(jest.fn());

jest.spyOn(helpers, 'useImageLoader').mockImplementation(() => ({
onLoad: jest.fn(),
isLoaded: false,
setLoaded,
}));

jest.spyOn(React, 'useRef').mockImplementation(() => ({
current: { src: 'image.png', complete: true },
}));

act(() => {
render(<Image alt="flowers" src="image.png" />);
});

expect(setLoaded).toHaveBeenCalled();
});

it('should render null if src is not a storyblok asset', async () => {
jest.spyOn(console, 'error').mockImplementation(jest.fn());

act(() => {
render(<Image data-testid="img" src="http://localhost/test.png" />);
});

expect(screen.queryByTestId('img')).toBeNull();
});
});
53 changes: 53 additions & 0 deletions src/image/__tests__/Picture.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from 'react';
import { cleanup, render, act, screen } from '@testing-library/react';

import { Picture } from '../Picture';

const storyblokImage =
'https://a.storyblok.com/f/39898/3310x2192/e4ec08624e/demo-image.jpeg';

describe('[image] Picture', () => {
afterEach(() => {
cleanup();
jest.restoreAllMocks();
});

it('should not load src initially', async () => {
act(() => {
render(<Picture alt="flowers" src={storyblokImage} />);
});

expect(screen.getByAltText('flowers')).not.toHaveAttribute('src');
expect(screen.getByAltText('flowers')).toHaveAttribute('data-src');
});

it('should set alt to empty string if undefined', async () => {
act(() => {
render(<Picture src={storyblokImage} />);
});

expect(screen.getByAltText('')).toBeInTheDocument();
});

it('should add webp srcset', async () => {
act(() => {
render(<Picture alt="flowers" src={storyblokImage} />);
});

expect(
screen.getByAltText('flowers').parentElement.childNodes[0],
).toHaveAttribute('type', 'image/webp');
expect(
(screen.getByAltText('flowers').parentElement
.childNodes[0] as any).getAttribute('srcSet'),
).toMatch(/filters:format\(webp\)/);
});

it('should load eager if not lazy', async () => {
act(() => {
render(<Picture alt="flowers" src={storyblokImage} lazy={false} />);
});

expect(screen.getByAltText('flowers')).toHaveAttribute('loading', 'eager');
});
});
Loading

1 comment on commit c8c050a

@vercel
Copy link

@vercel vercel bot commented on c8c050a May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.