diff --git a/builtins/amp-img.js b/builtins/amp-img.js index 1986cc91a5d1..715ad0aca51a 100644 --- a/builtins/amp-img.js +++ b/builtins/amp-img.js @@ -15,20 +15,17 @@ */ import {BaseElement} from '../src/base-element'; -import {isExperimentOn} from '../src/experiments'; +import {dev} from '../src/log'; import {isLayoutSizeDefined} from '../src/layout'; +import {listen} from '../src/event-helper'; import {registerElement} from '../src/service/custom-element-registry'; -import {srcsetFromElement, srcsetFromSrc} from '../src/srcset'; /** * Attributes to propagate to internal image when changed externally. * @type {!Array} */ const ATTRIBUTES_TO_PROPAGATE = ['alt', 'title', 'referrerpolicy', 'aria-label', - 'aria-describedby', 'aria-labelledby']; - -const EXPERIMENTAL_ATTRIBUTES_TO_PROPAGATE = ATTRIBUTES_TO_PROPAGATE - .concat(['srcset', 'src', 'sizes']); + 'aria-describedby', 'aria-labelledby','srcset', 'src', 'sizes']; export class AmpImg extends BaseElement { @@ -45,46 +42,21 @@ export class AmpImg extends BaseElement { /** @private {?Element} */ this.img_ = null; - /** @private {?../src/srcset.Srcset} */ - this.srcset_ = null; + /** @private {?UnlistenDef} */ + this.unlistenLoad_ = null; - /** @private @const {boolean} */ - this.useNativeSrcset_ = isExperimentOn(this.win, 'amp-img-native-srcset'); + /** @private {?UnlistenDef} */ + this.unlistenError_ = null; } /** @override */ mutatedAttributesCallback(mutations) { - let mutated = false; - if (!this.useNativeSrcset_) { - if (mutations['srcset'] !== undefined) { - // `srcset` mutations take precedence over `src` mutations. - this.srcset_ = srcsetFromElement(this.element); - mutated = true; - } else if (mutations['src'] !== undefined) { - // If only `src` is mutated, then ignore the existing `srcset` attribute - // value (may be set automatically as cache optimization). - this.srcset_ = srcsetFromSrc(this.element.getAttribute('src')); - mutated = true; - } - // This element may not have been laid out yet. - if (mutated && this.img_) { - this.updateImageSrc_(); - } - } - if (this.img_) { - const propAttrs = this.useNativeSrcset_ ? - EXPERIMENTAL_ATTRIBUTES_TO_PROPAGATE : - ATTRIBUTES_TO_PROPAGATE; - const attrs = propAttrs.filter( + const attrs = ATTRIBUTES_TO_PROPAGATE.filter( value => mutations[value] !== undefined); this.propagateAttributes( attrs, this.img_, /* opt_removeMissingAttrs */ true); - - if (this.useNativeSrcset_) { - this.guaranteeSrcForSrcsetUnsupportedBrowsers_(); - } - + this.guaranteeSrcForSrcsetUnsupportedBrowsers_(); } } @@ -102,7 +74,7 @@ export class AmpImg extends BaseElement { return; } // We try to find the first url in the srcset - const srcseturl = /https?:\/\/\S+/.exec(srcset); + const srcseturl = /\S+/.exec(srcset); // Connect to the first url if it exists if (srcseturl) { this.preconnect.url(srcseturl[0], onLayout); @@ -128,9 +100,6 @@ export class AmpImg extends BaseElement { if (this.img_) { return; } - if (!this.useNativeSrcset_ && !this.srcset_) { - this.srcset_ = srcsetFromElement(this.element); - } // If this amp-img IS the fallback then don't allow it to have its own // fallback to stop from nested fallback abuse. this.allowImgLoadFallback_ = !this.element.hasAttribute('fallback'); @@ -156,17 +125,9 @@ export class AmpImg extends BaseElement { 'be correctly propagated for the underlying element.'); } - - if (this.useNativeSrcset_) { - this.propagateAttributes(EXPERIMENTAL_ATTRIBUTES_TO_PROPAGATE, - this.img_); - this.guaranteeSrcForSrcsetUnsupportedBrowsers_(); - } else { - this.propagateAttributes(ATTRIBUTES_TO_PROPAGATE, this.img_); - } - + this.propagateAttributes(ATTRIBUTES_TO_PROPAGATE, this.img_); + this.guaranteeSrcForSrcsetUnsupportedBrowsers_(); this.applyFillContent(this.img_, true); - this.element.appendChild(this.img_); } @@ -175,11 +136,6 @@ export class AmpImg extends BaseElement { return this.isPrerenderAllowed_; } - /** @override */ - isRelayoutNeeded() { - return true; - } - /** @override */ reconstructWhenReparented() { return false; @@ -188,18 +144,26 @@ export class AmpImg extends BaseElement { /** @override */ layoutCallback() { this.initialize_(); - let promise = this.updateImageSrc_(); + const img = dev().assertElement(this.img_); + this.unlistenLoad_ = listen(img, 'load', () => this.hideFallbackImg_()); + this.unlistenError_ = listen(img, 'error', () => this.onImgLoadingError_()); + if (this.getLayoutWidth() <= 0) { + return Promise.resolve(); + } + return this.loadPromise(img); + } - // We only allow to fallback on error on the initial layoutCallback - // or else this would be pretty expensive. - if (this.allowImgLoadFallback_) { - promise = promise.catch(e => { - this.onImgLoadingError_(); - throw e; - }); - this.allowImgLoadFallback_ = false; + /** @override */ + unlayoutCallback() { + if (this.unlistenError_) { + this.unlistenError_(); + this.unlistenError_ = null; } - return promise; + if (this.unlistenLoad_) { + this.unlistenLoad_(); + this.unlistenLoad_ = null; + } + return true; } /** @@ -221,51 +185,33 @@ export class AmpImg extends BaseElement { } /** - * @return {!Promise} * @private */ - updateImageSrc_() { - if (this.getLayoutWidth() <= 0) { - return Promise.resolve(); - } - - if (!this.useNativeSrcset_) { - const src = this.srcset_.select( - // The width should never be 0, but we fall back to the screen width - // just in case. - this.getViewport().getWidth() || this.win.screen.width, - this.getDpr()); - if (src == this.img_.getAttribute('src')) { - return Promise.resolve(); - } - - this.img_.setAttribute('src', src); + hideFallbackImg_() { + if (!this.allowImgLoadFallback_ + && this.img_.classList.contains('i-amphtml-ghost')) { + this.getVsync().mutate(() => { + this.img_.classList.remove('i-amphtml-ghost'); + this.toggleFallback(false); + }); } - - return this.loadPromise(this.img_).then(() => { - // Clean up the fallback if the src has changed. - if (!this.allowImgLoadFallback_ && - this.img_.classList.contains('i-amphtml-ghost')) { - this.getVsync().mutate(() => { - this.img_.classList.remove('i-amphtml-ghost'); - this.toggleFallback(false); - }); - } - }); } /** - * If the image fails to load, show a placeholder instead. + * If the image fails to load, show a fallback or placeholder instead. * @private */ onImgLoadingError_() { - this.getVsync().mutate(() => { - this.img_.classList.add('i-amphtml-ghost'); - this.toggleFallback(true); - // Hide placeholders, as browsers that don't support webp - // Would show the placeholder underneath a transparent fallback - this.togglePlaceholder(false); - }); + if (this.allowImgLoadFallback_) { + this.getVsync().mutate(() => { + this.img_.classList.add('i-amphtml-ghost'); + this.toggleFallback(true); + // Hide placeholders, as browsers that don't support webp + // Would show the placeholder underneath a transparent fallback + this.togglePlaceholder(false); + }); + this.allowImgLoadFallback_ = false; + } } } diff --git a/builtins/amp-img.md b/builtins/amp-img.md index 8717c8f38391..509ac16e63f3 100644 --- a/builtins/amp-img.md +++ b/builtins/amp-img.md @@ -54,7 +54,7 @@ In the following example, we display an image that responds to the size of the v resizable src="https://ampproject-b5f4c.firebaseapp.com/examples/ampimg.basic.embed.html">
Show full code
-
+
@@ -74,7 +74,7 @@ In the following example, if the browser doesn't support WebP, the fallback JPG resizable src="https://ampproject-b5f4c.firebaseapp.com/examples/ampimg.fallback.embed.html">
Show full code
-
+
@@ -97,11 +97,11 @@ This attribute is similar to the `src` attribute on the `img` tag. The value mus **srcset** -Same as `srcset` attribute on the `img` tag. The behavior will be polyfilled where not natively supported. +Same as `srcset` attribute on the `img` tag. For browsers that do not support `srcset`, `` will default to using `src`. If only `srcset` and no `src` is provided, the first url in the `srcset` will be selected. **sizes** -Same as `sizes` attribute on the `img` tag. +Same as `sizes` attribute on the `img` tag. {% call callout('Read on', type='read') %} See [Responsive images with srcset, sizes & heights](https://www.ampproject.org/docs/design/responsive/art_direction) for usage of `sizes` and `srcset`. @@ -173,13 +173,13 @@ For example, instead of specifying `width="900"` and `height="675"`, you can jus resizable src="https://ampproject-b5f4c.firebaseapp.com/examples/ampimg.aspectratio.embed.html">
Show full code
-
+
#### Setting multiple source files for different screen resolutions -The [`srcset`](#attributes) attribute should be used to provide different resolutions of the same image, that all have the same aspect ratio. The AMP runtime will automatically choose the most appropriate file from `srcset` based on the screen resolution and width of the user's device. +The [`srcset`](#attributes) attribute should be used to provide different resolutions of the same image, that all have the same aspect ratio. The browser will automatically choose the most appropriate file from `srcset` based on the screen resolution and width of the user's device. In contrast, the [`media`](https://www.ampproject.org/docs/reference/common_attributes#media) attribute shows or hides AMP components, and should be used when designing responsive layouts. The appropriate way to display images with differing aspect ratios is to use multiple `` components, each with a `media` attribute that matches the screen widths in which to show each instance. diff --git a/examples/img/hero@1x.jpg b/examples/img/hero@1x.jpg index c3064ea39240..248b32b19d82 100644 Binary files a/examples/img/hero@1x.jpg and b/examples/img/hero@1x.jpg differ diff --git a/examples/img/hero@1x.webp b/examples/img/hero@1x.webp new file mode 100644 index 000000000000..8e1f7a79d4be Binary files /dev/null and b/examples/img/hero@1x.webp differ diff --git a/examples/img/hero@2x.jpg b/examples/img/hero@2x.jpg index 0e421ab1e409..9f360882f67c 100644 Binary files a/examples/img/hero@2x.jpg and b/examples/img/hero@2x.jpg differ diff --git a/examples/img/hero@2x.webp b/examples/img/hero@2x.webp new file mode 100644 index 000000000000..c32769ccd223 Binary files /dev/null and b/examples/img/hero@2x.webp differ diff --git a/test/functional/test-amp-img.js b/test/functional/test-amp-img.js index e670ed4e27ab..ce35f6249a2e 100644 --- a/test/functional/test-amp-img.js +++ b/test/functional/test-amp-img.js @@ -19,8 +19,8 @@ import {AmpImg, installImg} from '../../builtins/amp-img'; import {BaseElement} from '../../src/base-element'; import {LayoutPriority} from '../../src/layout'; import {Services} from '../../src/services'; +import {createCustomEvent} from '../../src/event-helper'; import {createIframePromise} from '../../testing/iframe'; -import {isExperimentOn, toggleExperiment} from '../../src/experiments'; describe('amp-img', () => { let sandbox; @@ -125,54 +125,20 @@ describe('amp-img', () => { windowWidth = 320; screenWidth = 4000; return getImg({ - srcset: 'bad.jpg 2000w, /examples/img/sample.jpg 1000w', - width: 300, - height: 200, - }).then(ampImg => { - const img = ampImg.querySelector('img'); - expect(img.tagName).to.equal('IMG'); - expect(img.getAttribute('src')).to.equal('/examples/img/sample.jpg'); - expect(img.hasAttribute('referrerpolicy')).to.be.false; - }); - }); - - it('should load larger image on larger screen', () => { - windowWidth = 3000; - screenWidth = 300; - return getImg({ - srcset: '/examples/img/sample.jpg?large 2000w, ' + - '/examples/img/small.jpg?small 1000w', - width: 300, - height: 200, - }).then(ampImg => { - const img = ampImg.querySelector('img'); - expect(img.tagName).to.equal('IMG'); - expect(img.getAttribute('src')).to.equal( - '/examples/img/sample.jpg?large'); - expect(img.hasAttribute('referrerpolicy')).to.be.false; - }); - }); - - it('should fall back to screen width for srcset', () => { - windowWidth = 0; - screenWidth = 3000; - return getImg({ - srcset: '/examples/img/sample.jpg?large 2000w, ' + - '/examples/img/small.jpg?small 1000w', + srcset: SRCSET_STRING, width: 300, height: 200, }).then(ampImg => { const img = ampImg.querySelector('img'); expect(img.tagName).to.equal('IMG'); - expect(img.getAttribute('src')).to.equal( - '/examples/img/sample.jpg?large'); + expect(img.getAttribute('srcset')).to.equal(SRCSET_STRING); expect(img.hasAttribute('referrerpolicy')).to.be.false; }); }); it('should preconnect to the the first srcset url if src is not set', () => { return getImg({ - srcset: 'http://google.com/bad.jpg 2000w, /examples/img/sample.jpg 1000w', + srcset: SRCSET_STRING, width: 300, height: 200, }).then(ampImg => { @@ -181,7 +147,7 @@ describe('amp-img', () => { impl.preconnectCallback(true); expect(impl.preconnect.url.called).to.be.true; expect(impl.preconnect.url).to.have.been.calledWith( - 'http://google.com/bad.jpg' + '/examples/img/hero@1x.jpg' ); }); }); @@ -212,9 +178,7 @@ describe('amp-img', () => { }); }); - // This test is relevant to the amp-img-native-srcset experiment it('should propagate srcset and sizes', () => { - toggleExperiment(iframe.win, 'amp-img-native-srcset', true, true); return getImg({ src: '/examples/img/sample.jpg', srcset: SRCSET_STRING, @@ -222,8 +186,6 @@ describe('amp-img', () => { width: 320, height: 240, }).then(ampImg => { - expect(isExperimentOn(iframe.win, 'amp-img-native-srcset')) - .to.equal(true); const img = ampImg.querySelector('img'); expect(img.getAttribute('srcset')).to.equal(SRCSET_STRING); expect(img.getAttribute('sizes')).to @@ -234,11 +196,14 @@ describe('amp-img', () => { describe('#fallback on initial load', () => { let el; let impl; - let toggleElSpy; + let toggleFallbackSpy; + let togglePlaceholderSpy; + let errorSpy; + let toggleSpy; beforeEach(() => { el = document.createElement('amp-img'); - el.setAttribute('src', 'test.jpg'); + el.setAttribute('src', '/examples/img/sample.jpg'); el.setAttribute('width', 100); el.setAttribute('height', 100); el.getResources = () => Services.resourcesForDoc(document); @@ -246,7 +211,11 @@ describe('amp-img', () => { impl.createdCallback(); sandbox.stub(impl, 'getLayoutWidth').returns(100); el.toggleFallback = function() {}; - toggleElSpy = sandbox.spy(el, 'toggleFallback'); + el.togglePlaceholder = function() {}; + toggleFallbackSpy = sandbox.spy(el, 'toggleFallback'); + togglePlaceholderSpy = sandbox.spy(el, 'togglePlaceholder'); + errorSpy = sandbox.spy(impl, 'onImgLoadingError_'); + toggleSpy = sandbox.spy(impl, 'toggleFallback'); impl.getVsync = function() { return { @@ -262,149 +231,103 @@ describe('amp-img', () => { }; }); + afterEach(() => { + impl.unlayoutCallback(); + }); + it('should not display fallback if loading succeeds', () => { - sandbox.stub(impl, 'loadPromise').returns(Promise.resolve()); - const errorSpy = sandbox.spy(impl, 'onImgLoadingError_'); - const toggleSpy = sandbox.spy(impl, 'toggleFallback'); impl.buildCallback(); - expect(errorSpy).to.have.not.been.called; expect(toggleSpy).to.have.not.been.called; - expect(toggleElSpy).to.have.not.been.called; + expect(toggleFallbackSpy).to.have.not.been.called; return impl.layoutCallback().then(() => { expect(errorSpy).to.have.not.been.called; expect(toggleSpy).to.have.not.been.called; - expect(toggleElSpy).to.have.not.been.called; + expect(toggleFallbackSpy).to.have.not.been.called; + expect(togglePlaceholderSpy).to.have.not.been.called; }); }); it('should display fallback if loading fails', () => { - sandbox.stub(impl, 'loadPromise').returns(Promise.reject()); - const errorSpy = sandbox.spy(impl, 'onImgLoadingError_'); - const toggleSpy = sandbox.spy(impl, 'toggleFallback'); + el.setAttribute('src', 'non-existent.jpg'); impl.buildCallback(); expect(errorSpy).to.have.not.been.called; expect(toggleSpy).to.have.not.been.called; - expect(toggleElSpy).to.have.not.been.called; - + expect(toggleFallbackSpy).to.have.not.been.called; return impl.layoutCallback().catch(() => { expect(errorSpy).to.be.calledOnce; expect(toggleSpy).to.be.calledOnce; expect(toggleSpy.firstCall.args[0]).to.be.true; - expect(toggleElSpy.firstCall.args[0]).to.be.true; + expect(toggleFallbackSpy.firstCall.args[0]).to.be.true; }); }); it('should hide child placeholder elements if loading fails', () => { - sandbox.stub(impl, 'loadPromise').returns(Promise.reject()); - const errorSpy = sandbox.spy(impl, 'onImgLoadingError_'); - const toggleSpy = sandbox.spy(impl, 'toggleFallback'); - const togglePlaceholderSpy = sandbox.spy(impl, 'togglePlaceholder'); + el.setAttribute('src', 'non-existent.jpg'); impl.buildCallback(); expect(errorSpy).to.have.not.been.called; expect(toggleSpy).to.have.not.been.called; expect(togglePlaceholderSpy).to.have.not.been.called; - expect(toggleElSpy).to.have.not.been.called; - + expect(toggleFallbackSpy).to.have.not.been.called; return impl.layoutCallback().catch(() => { expect(errorSpy).to.be.calledOnce; expect(toggleSpy).to.be.calledOnce; expect(toggleSpy.firstCall.args[0]).to.be.true; expect(togglePlaceholderSpy).to.be.calledOnce; expect(togglePlaceholderSpy.firstCall.args[0]).to.be.false; - expect(toggleElSpy.firstCall.args[0]).to.be.true; + expect(toggleFallbackSpy.firstCall.args[0]).to.be.true; }); }); - it('should fallback only once', () => { - const loadStub = sandbox.stub(impl, 'loadPromise'); - loadStub - .onCall(0).returns(Promise.reject()) - .onCall(1).returns(Promise.resolve()); - loadStub.returns(Promise.resolve()); - const errorSpy = sandbox.spy(impl, 'onImgLoadingError_'); - const toggleSpy = sandbox.spy(impl, 'toggleFallback'); + it('should fallback once and remove fallback once image loads', () => { + el.setAttribute('src', 'non-existent.jpg'); impl.buildCallback(); expect(errorSpy).to.have.not.been.called; expect(toggleSpy).to.have.not.been.called; - expect(toggleElSpy).to.have.not.been.called; - + expect(toggleFallbackSpy).to.have.not.been.called; return impl.layoutCallback().catch(() => { expect(errorSpy).to.be.calledOnce; expect(toggleSpy).to.be.calledOnce; expect(toggleSpy.firstCall.args[0]).to.be.true; - expect(toggleElSpy).to.be.calledOnce; - expect(toggleElSpy.firstCall.args[0]).to.be.true; - expect(errorSpy).to.be.calledOnce; - return impl.layoutCallback(); - }).then(() => { - expect(errorSpy).to.be.calledOnce; - expect(toggleSpy).to.be.calledOnce; - expect(toggleElSpy).to.be.calledOnce; - return impl.layoutCallback(); - }).then(() => { - expect(errorSpy).to.be.calledOnce; - expect(toggleSpy).to.be.calledOnce; - expect(toggleElSpy).to.be.calledOnce; - }); - }); - - it('should remove the fallback if src is successfully updated', () => { - const loadStub = sandbox.stub(impl, 'loadPromise'); - loadStub.onCall(0).returns(Promise.reject()); - loadStub.returns(Promise.resolve()); - impl.buildCallback(); - - expect(toggleElSpy).to.have.not.been.called; - - return impl.layoutCallback().catch(() => { - expect(toggleElSpy).to.be.calledOnce; - expect(toggleElSpy.getCall(0).args[0]).to.be.true; + expect(toggleFallbackSpy).to.be.calledOnce; + expect(toggleFallbackSpy.firstCall.args[0]).to.be.true; expect(impl.img_).to.have.class('i-amphtml-ghost'); - impl.img_.setAttribute('src', 'test-1000.jpg'); - return impl.layoutCallback().then(() => { - expect(toggleElSpy).to.have.callCount(2); - expect(toggleElSpy.getCall(1).args[0]).to.be.false; - expect(impl.img_).to.not.have.class('i-amphtml-ghost'); - }); - }); - }); - it('should not remove the fallback if src is not updated', () => { - const loadStub = sandbox.stub(impl, 'loadPromise'); - loadStub.onCall(0).returns(Promise.reject()); - loadStub.returns(Promise.resolve()); - impl.buildCallback(); + // On load, remove fallback + const loadEvent = createCustomEvent(iframe.win, 'load'); + impl.img_.dispatchEvent(loadEvent); - expect(el).to.not.have.class('i-amphtml-ghost'); - expect(toggleElSpy).to.have.not.been.called; - return impl.layoutCallback().catch(() => { - expect(toggleElSpy).to.be.calledOnce; - expect(toggleElSpy.getCall(0).args[0]).to.be.true; - expect(impl.img_).to.have.class('i-amphtml-ghost'); - return impl.layoutCallback().then(() => { - expect(toggleElSpy).to.be.calledOnce; - expect(impl.img_).to.have.class('i-amphtml-ghost'); - }); + expect(errorSpy).to.be.calledOnce; + expect(toggleSpy).to.have.callCount(2); + expect(toggleSpy.getCall(1).args[0]).to.be.false; + expect(toggleFallbackSpy).to.have.callCount(2); + expect(toggleFallbackSpy.getCall(1).args[0]).to.be.false; + expect(impl.img_).to.not.have.class('i-amphtml-ghost'); + + // On further error, do not bring back the fallback image + const errorEvent = createCustomEvent(iframe.win, 'error'); + impl.img_.dispatchEvent(errorEvent); + + expect(errorSpy).to.be.calledTwice; + expect(toggleSpy).to.have.callCount(2); + expect(toggleFallbackSpy).to.have.callCount(2); + expect(impl.img_).to.not.have.class('i-amphtml-ghost'); }); }); - it('should not remove the fallback if src is updated but ' + - 'fails fetching', () => { - const loadStub = sandbox.stub(impl, 'loadPromise'); - loadStub.returns(Promise.reject()); + it('should not remove the fallback if fetching fails', () => { + el.setAttribute('src', 'non-existent.jpg'); impl.buildCallback(); - expect(el).to.not.have.class('i-amphtml-ghost'); - expect(toggleElSpy).to.have.not.been.called; + expect(toggleFallbackSpy).to.have.not.been.called; return impl.layoutCallback().catch(() => { - expect(toggleElSpy).to.be.calledOnce; - expect(toggleElSpy.getCall(0).args[0]).to.be.true; + expect(toggleFallbackSpy).to.be.calledOnce; + expect(toggleFallbackSpy.getCall(0).args[0]).to.be.true; expect(impl.img_).to.have.class('i-amphtml-ghost'); impl.img_.setAttribute('src', 'test-1000.jpg'); return impl.layoutCallback().catch(() => { - expect(toggleElSpy).to.be.calledOnce; + expect(toggleFallbackSpy).to.be.calledOnce; expect(impl.img_).to.have.class('i-amphtml-ghost'); }); }); @@ -448,5 +371,7 @@ describe('amp-img', () => { expect(img.getAttribute('aria-label')).to.equal('Hello'); expect(img.getAttribute('aria-labelledby')).to.equal('id2'); expect(img.getAttribute('aria-describedby')).to.equal('id3'); + impl.unlayoutCallback(); }); + }); diff --git a/test/integration/test-amp-img.js b/test/integration/test-amp-img.js index d2413a83565b..bf93caa6f8ef 100644 --- a/test/integration/test-amp-img.js +++ b/test/integration/test-amp-img.js @@ -98,11 +98,9 @@ describe.configure().retryOnSaucelabs().run('Rendering of amp-img', () => { /examples/img/hero@2x.jpg 1282w" width=641 height=480 layout=responsive> `; - const experiments = ['amp-img-native-srcset']; describes.integration('Internet Explorer edge cases', { body, - experiments, }, env => { let win; diff --git a/test/manual/amp-img-fallback.amp.html b/test/manual/amp-img-fallback.amp.html new file mode 100644 index 000000000000..0da02fd3fce3 --- /dev/null +++ b/test/manual/amp-img-fallback.amp.html @@ -0,0 +1,59 @@ + + + + + AMP #0 + + + + + + + + +

amp-image

+

Unsupported formats

+

Smaller image is webp

+ +
Placeholder
+ +
+ +

Larger image is webp

+ +
Placeholder
+ +
+ +

404

+

Smaller image is 404

+ +
Placeholder
+ +
+ +

Larger image is 404

+ +
Placeholder
+ +
+ +