diff --git a/packages/library/components/image/src/image-component.js b/packages/library/components/image/src/image-component.js index a49489ff..291b290c 100644 --- a/packages/library/components/image/src/image-component.js +++ b/packages/library/components/image/src/image-component.js @@ -1,12 +1,16 @@ -import { MuonElement, css, html, unsafeCSS } from '@muon/library'; +import { MuonElement, css, html, unsafeCSS, styleMap, classMap } from '@muon/library'; +import { imageInlineLoader, imageBackgroundLoader } from '@muon/library/directives/image-loader'; import { - IMAGE_TYPE + IMAGE_TYPE, + IMAGE_RATIOS, + IMAGE_RATIO, + IMAGE_PLACEHOLDER } from '@muon/library/build/tokens/es6/muon-tokens'; import styles from './styles.css'; /** - * Lazy loading images + * Loading images with default lazy loading * * @element image * @@ -17,10 +21,13 @@ export class Image extends MuonElement { static get properties() { return { background: { type: Boolean }, - backgroundsize: { type: String }, - src: { type: String, reflect: true }, + backgroundsize: { type: String, attribute: 'background-size' }, + src: { type: String }, alt: { type: String }, - ratio: { type: String } // 1x1, 4x3, 16x9: + ratio: { type: String }, + placeholder: { type: String }, + loading: { type: String }, + _ratios: { type: Array, state: true } }; } @@ -34,120 +41,57 @@ export class Image extends MuonElement { this.type = IMAGE_TYPE; this.background = false; this.backgroundsize = 'cover'; // cover, contain - this.ratio = ''; - } - - backgroundStyles() { - const preImg = `${this.src}.thumb.48.48.png`; + this.alt = ''; + this.ratio = IMAGE_RATIO; + this.placeholder = IMAGE_PLACEHOLDER; + this.loading = 'lazy'; // eager|lazy + this._ratios = IMAGE_RATIOS; - return html` - - `; } - updated() { - const src = this.src; - - if (src) { - const options = { - threshold: 0.01, - rootMargin: '150px' - }; - - const fetchImage = (url) => { - return new Promise((resolve, reject) => { - const image = new Image(); - - image.src = url; - image.onload = resolve; - image.onerror = reject; - - return resolve(); - }); - }; - - const elementView = (target, image) => { - target.style.backgroundImage = `url(${image})`; - target.style.backgroundSize = `${this.backgroundsize}`; - }; - - const imgView = (target, image) => { - target.src = image; - target.alt = this.alt || ''; - }; - - const switchImage = (target, image, io) => { - if (this.background) { - elementView(target, image); - } else { - imgView(target, image); - } - - const blurElem = this.shadowRoot.querySelector('.blur'); - - if (io && target) { - io.unobserve(target); // only unobserve after the image has loaded - } - - if (blurElem) { - blurElem.classList.remove('blur'); - } - }; - - const io = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.intersectionRatio > 0) { - const target = entry.target; - - fetchImage(src).then(() => { - switchImage(target, src, io); - }).catch(() => { - fetchImage(src).then(() => { - switchImage(target, src, io); - }).catch((e) => { - console.info(e); // to stop headless builds from breaking - }); - }); - } - }); - }, options); - - const targetElements = this.shadowRoot.querySelectorAll('.image-lazy, .image-holder'); - for (const element of targetElements) { - io.observe(element); - } - } + get placeholderImage() { + return this.placeholder.replace('(src)', this.src); // @TODO: test alternative ways for this } get standardTemplate() { const isBackground = this.background; - let ratioClass = 'no-ratio'; - if (isBackground) { - ratioClass = 'ar16x9'; // without a default size background images won't show + if (!this._ratios.includes(this.ratio)) { + this.ratio = IMAGE_RATIO; // @TODO: add fallback `|| this._ratios[0]` } - if (this.ratio.length !== 0) { - ratioClass = `ar${this.ratio}`; + if (isBackground) { + this.ratio = this.ratio?.length > 0 ? this.ratio : '16 / 9'; // without a default size background images won't show } + const [x, y] = this.ratio.split(' / '); + const styles = { + '--image-ratio': CSS?.supports('aspect-ratio', '1 / 1') && this.ratio ? this.ratio : undefined, + '--image-padding': CSS?.supports('aspect-ratio', '1 / 1') || !x && !y ? undefined : `${y / x * 100}%`, + '--background-size': isBackground ? this.backgroundsize : undefined + }; + + const classes = { + image: true, + 'no-ratio': !this.ratio || this.ratio?.length < 1, + 'is-background': isBackground + }; + if (this.src && this.src.length > 0) { - const preImg = `${this.src}.thumb.48.48.png`; - const lazyLoading = window.chrome ? `loading="lazy"` : ``; + const imageObj = { + src: this.src, + alt: this.alt, + placeholder: this.placeholderImage, + loading: this.loading + }; return html` - ${isBackground ? this.backgroundStyles() : ''} -
- ${isBackground ? html`
` : html``} +
+ ${isBackground ? imageBackgroundLoader(imageObj) : imageInlineLoader(imageObj)}
`; } else { - return html`
`; + return html`
`; } } } diff --git a/packages/library/components/image/src/styles.css b/packages/library/components/image/src/styles.css index 3b114703..53ada548 100644 --- a/packages/library/components/image/src/styles.css +++ b/packages/library/components/image/src/styles.css @@ -10,35 +10,26 @@ width: 100%; overflow: hidden; box-sizing: border-box; - min-width: 1px; - min-height: 1px; + min-width: 1px; /* Enforces a size to be observable */ + min-height: 1px; /* Enforces a size to be observable */ } & .image { - transition: all 0.5s; position: relative; + aspect-ratio: var(--image-ratio); - &.ar16x9 { - padding-bottom: 56.25%; + @supports not (aspect-ratio: 1/1) { + padding-top: var(--image-padding); } - &.ar1x1 { - padding-bottom: 100%; - } - - &.ar4x3 { - padding-bottom: 75%; - } - - &.blur { + & .blur { filter: blur(0.7em); transform: scale(1.03); transition-timing-function: ease-out; } - & .image-holder { - background-repeat: no-repeat; - background-position: center center; + & .blur-out { + animation: blurOut 0.5s ease-out; } & .image-holder, @@ -50,6 +41,15 @@ right: 0; } + &.is-background { + & .image-holder { + background-repeat: no-repeat; + background-position: center center; + background-image: var(--background-image); + background-size: var(--background-size); + } + } + &.no-ratio { padding: 0; @@ -59,3 +59,13 @@ } } } + +@keyframes blurOut { + 0% { + filter: blur(0.7em); + } + + 100% { + filter: blur(0); + } +} diff --git a/packages/library/components/image/src/token.json b/packages/library/components/image/src/token.json index 7c240877..b72c9dca 100644 --- a/packages/library/components/image/src/token.json +++ b/packages/library/components/image/src/token.json @@ -1,7 +1,23 @@ { "image": { "type": { - "value": "standard" + "description": "Default value for `type` property which renders the template", + "value": "standard", + "type": "string" + }, + "ratios": { + "description": "Default value for `ratios` available for ratio", + "value": ["1 / 1", "4 / 3", "16 / 9"] + }, + "ratio": { + "description": "Default value for `ratio` property which decides the ratio size for the image", + "value": "16 / 9", + "type": "string" + }, + "placeholder": { + "description": "Default value for `placeholder` property which creates the thumbnail placeholder image. (src) can be used to bring in the src property", + "value": "", + "type": "string" } } } \ No newline at end of file diff --git a/packages/library/components/image/story.js b/packages/library/components/image/story.js index 4b397071..fcdb7f90 100644 --- a/packages/library/components/image/story.js +++ b/packages/library/components/image/story.js @@ -6,7 +6,7 @@ const details = setup('image', Image); export default details.defaultValues; export const Standard = (args) => details.template(args, (args) => args.text); -Standard.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg' }; +Standard.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/smart-meters/Technology/Lockup%202.png', placeholder: '(src).thumb.48.48.png' }; export const Background = (args) => details.template(args, (args) => args.text); -Background.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg', background: true }; +Background.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg', placeholder: '(src).thumb.48.48.png', background: true }; diff --git a/packages/library/directives/image-loader.js b/packages/library/directives/image-loader.js new file mode 100644 index 00000000..e078e955 --- /dev/null +++ b/packages/library/directives/image-loader.js @@ -0,0 +1,114 @@ +import { AsyncDirective, directive, html, until, styleMap, ifDefined } from '@muon/library'; +export class ImageLoaderDirective extends AsyncDirective { + constructor(partInfo) { + super(partInfo); + + this.src = ''; + this.alt = ''; + this.placeholder = ''; + this.loading = ''; + this.image = undefined; + } + + async fetchImage() { + return new Promise((resolve, reject) => { + this.image = new Image(); + + this.image.src = this.src; + this.image.alt = this.alt; + this.image.classList.add('blur-out', 'image-lazy'); + this.image.onload = () => resolve(this.image); + this.image.onerror = () => reject(); + }); + } + + observer(parts, attributes) { + return new Promise((resolve) => { + const options = { + threshold: 0.01, + rootMargin: '150px' + }; + + const io = new IntersectionObserver((entries) => { + /* eslint-disable consistent-return */ + return entries.forEach((entry) => { + if (!this.image && entry.intersectionRatio > 0) { + return resolve(this.render(attributes[0])); + } + }); + }, options); + + const observe = parts.parentNode; + io.observe(observe); + }); + } + + update(parts, attributes) { + if (attributes?.[0]?.loading === 'lazy') { + return html`${until(this.observer(parts, attributes), undefined)}`; + } + + return this.render(attributes[0]); + } +} + +export class ImageInlineLoaderDirective extends ImageLoaderDirective { + render({ src, alt, placeholder, loading = 'lazy' }) { + const loadingAttribute = window.chrome ? loading : undefined; + + this.src = src; + this.alt = alt; + this.placeholder = placeholder; + this.loading = loading; + + if (this.placeholder) { + this.setValue(html``); + } + + Promise.resolve(this.fetchImage()).then((image) => { + if (image) { + dispatchEvent(new CustomEvent('image-loaded', { bubbles: true })); + this.setValue(image); + } + }).catch(() => { + console.error(`Image (${this.src}) failed to load`); + }); + + return undefined; + } +} + +export class ImageBackgroundLoaderDirective extends ImageLoaderDirective { + render({ src, alt, placeholder, loading = 'lazy' }) { + this.src = src; + this.alt = alt; + this.placeholder = placeholder; + this.loading = loading; + + const styles = { + '--background-image': `url("${this.placeholder}")` + }; + + if (this.placeholder) { + this.setValue(html`
`); + } + + Promise.resolve(this.fetchImage()).then((image) => { + if (image) { + const styles = { + '--background-image': `url("${this.src}")` + }; + + dispatchEvent(new CustomEvent('image-loaded')); + this.setValue(html`
`); + } + }).catch(() => { + console.error(`Image (${this.src}) failed to load`); + }); + + return undefined; + } +} + +export const imageInlineLoader = directive(ImageInlineLoaderDirective); +export const imageBackgroundLoader = directive(ImageBackgroundLoaderDirective); diff --git a/packages/library/tests/components/image/__snapshots__/image.test.snap.js b/packages/library/tests/components/image/__snapshots__/image.test.snap.js new file mode 100644 index 00000000..14975adf --- /dev/null +++ b/packages/library/tests/components/image/__snapshots__/image.test.snap.js @@ -0,0 +1,195 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["icon implements standard self"] = +`
+
+`; +/* end snapshot icon implements standard self */ + +snapshots["image implements standard self"] = +`
+
+`; +/* end snapshot image implements standard self */ + +snapshots["image implements src image"] = +`
+ +
+`; +/* end snapshot image implements src image */ + +snapshots["image implements ratio"] = +`
+ +
+`; +/* end snapshot image implements ratio */ + +snapshots["image implements background"] = +`
+
+
+
+`; +/* end snapshot image implements background */ + +snapshots["image implements placeholder image"] = +`
+
+`; +/* end snapshot image implements placeholder image */ + +snapshots["image implements alt"] = +`
+ alternative text for the image +
+`; +/* end snapshot image implements alt */ + +snapshots["image fallsback on ratio if not correct"] = +`
+ +
+`; +/* end snapshot image fallsback on ratio if not correct */ + +snapshots["image fallsback on image padding if aspect-ratio not available"] = +`
+ +
+`; +/* end snapshot image fallsback on image padding if aspect-ratio not available */ + +snapshots["image image fails to loads"] = +`
+
+`; +/* end snapshot image image fails to loads */ + +snapshots["image image fails to load"] = +`
+
+`; +/* end snapshot image image fails to load */ + +snapshots["image image is eager"] = +`
+ +
+`; +/* end snapshot image image is eager */ + +snapshots["image image fails to load for background"] = +`
+
+`; +/* end snapshot image image fails to load for background */ + +snapshots["image implements placeholder image for background"] = +`
+
+`; +/* end snapshot image implements placeholder image for background */ + +snapshots["image implements placeholder image for chrome"] = +`
+
+`; +/* end snapshot image implements placeholder image for chrome */ + +snapshots["image fallsback for ratios"] = +`
+ +
+`; +/* end snapshot image fallsback for ratios */ + +snapshots["image fallback for ratio"] = +`
+
+
+
+`; +/* end snapshot image fallback for ratio */ + diff --git a/packages/library/tests/components/image/image.test.js b/packages/library/tests/components/image/image.test.js new file mode 100644 index 00000000..baaa4a68 --- /dev/null +++ b/packages/library/tests/components/image/image.test.js @@ -0,0 +1,279 @@ +/* eslint-disable no-undef */ +import { expect, fixture, html, defineCE, unsafeStatic, nextFrame } from '@open-wc/testing'; +import sinon from 'sinon'; +import { defaultChecks } from '../../helpers'; +import { Image } from '@muon/library/components/image'; + +const tagName = defineCE(Image); +const tag = unsafeStatic(tagName); + +const awaitLoading = () => { + return new Promise((resolve) => { + window.addEventListener('image-loaded', resolve); + }); +}; + +describe('image', () => { + + afterEach(() => { + sinon.restore(); + }); + + it('implements standard self', async () => { + const el = await fixture(html`<${tag}>`); + + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + + expect(elementImage.innerHTML).to.equal('', 'has empty image div'); + expect(el.src).to.equal(undefined, '`src` has no default property'); + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el._ratios).to.deep.equal(['1 / 1', '4 / 3', '16 / 9'], '`ratios` property has default token value'); + expect(el.placeholder).to.equal('', '`placeholder` has default token value'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(el.loading).to.equal('lazy', '`loading` has default value'); + expect(el.alt).to.equal('', 'alt has no value'); + + }); + + it('implements src image', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + + }); + + it('implements ratio', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" ratio="1 / 1">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('1 / 1', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('1 / 1', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('1 / 1', 'computed style value added for aspect-ratio'); + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + }); + + it('implements background', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" background>`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + const holder = elementImage.querySelector('.image-holder'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(img).to.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(Array.from(holder.classList)).to.deep.equal(['image-holder', 'blur-out']); + expect(getComputedStyle(holder).backgroundImage).to.equal('url("https://via.placeholder.com/150")', 'computed style value added for background image'); + expect(holder.style.getPropertyValue('--background-image')).to.equal('url("https://via.placeholder.com/150")', 'image passed as custom css variable'); + }); + + it('implements placeholder image', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15">`); + + await nextFrame(); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/15', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(Array.from(img.classList)).to.deep.equal(['image-lazy', 'blur']); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + }); + + it('implements placeholder image for background', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15" background>`); + + await nextFrame(); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const holder = elementImage.querySelector('.image-holder'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(getComputedStyle(holder).backgroundImage).to.equal('url("https://via.placeholder.com/15")', 'computed style value added for background image'); + expect(holder.style.getPropertyValue('--background-image')).to.equal('url("https://via.placeholder.com/15")', 'image passed as custom css variable'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + }); + + it('implements alt', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" alt="alternative text for the image">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('alternative text for the image', 'alt has a value'); + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + }); + + it('fallsback on ratio if not correct', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" ratio="not-a-ratio">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + }); + + it('image is eager', async () => { + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" loading="eager">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + }); + + it('fallsback on image padding if aspect-ratio not available', async () => { + sinon.replace( + CSS, + 'supports', + sinon.fake.returns(false) + ); + + const el = await fixture(html`<${tag} src="https://via.placeholder.com/150">`); + + await awaitLoading(el); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(elementImage.style.getPropertyValue('--image-padding')).to.equal('56.25%', 'ratio percentage passed as custom css variable'); + // expect(getComputedStyle(elementImage).paddingTop).to.equal('56.25%', 'computed style value added padding top'); + // @TODO: work out how to fake @supports in CSS + expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']); + }); + + it('image fails to load', async () => { + const consoleError = sinon.stub(console, 'error'); + const el = await fixture(html`<${tag} src="this-is-not-an-image">`); + + await nextFrame(); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(img).to.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + expect(consoleError.args[0]).to.deep.equal(['Image (this-is-not-an-image) failed to load']); + }); + + it('image fails to load for background', async () => { + const consoleError = sinon.stub(console, 'error'); + const el = await fixture(html`<${tag} src="this-is-not-an-image">`); + + await nextFrame(); + await defaultChecks(el); + + expect(consoleError.args[0]).to.deep.equal(['Image (this-is-not-an-image) failed to load']); + }); + + it('implements placeholder image for chrome', async () => { + window.chrome = true; + const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15">`); + + await nextFrame(); + await defaultChecks(el); + + const shadowRoot = el.shadowRoot; + const elementImage = shadowRoot.querySelector('.image'); + const img = elementImage.querySelector('img'); + + expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions + expect(el.type).to.equal('standard', '`type` property has default value `standard`'); + expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value'); + expect(img.src).to.equal('https://via.placeholder.com/15', 'has `src` value from el'); + expect(img.alt).to.equal('', 'alt has not value'); + expect(img.loading).to.equal('lazy', 'loading has a value'); + expect(Array.from(img.classList)).to.deep.equal(['image-lazy', 'blur']); + expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable'); + expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio'); + }); + +});