From e05338bffa8ad59337bb1d6d3378d45dbedfc1bb Mon Sep 17 00:00:00 2001 From: Daniel Rozenberg Date: Wed, 2 Aug 2023 13:49:57 -0400 Subject: [PATCH] Unskip some tests that no longer fail --- .../test-line-delimited-response-handler.js | 40 +- .../amp-form/amp-form-server-error.amp.html | 57 + .../amp-form/amp-form-server-error.js | 19 + examples/visual-tests/amp-form/amp-form.js | 17 - .../amp-3d-gltf/0.1/test/test-amp-3d-gltf.js | 4 +- extensions/amp-a4a/0.1/test/test-amp-a4a.js | 3 +- .../0.1/test/test-amp-access-server-jwt.js | 23 +- extensions/amp-access/0.1/test/test-jwt.js | 197 +- .../test/integration/test-amp-accordion.js | 65 +- .../test-amp-ad-xorigin-iframe-handler.js | 30 +- .../amp-ad/0.1/test/test-concurrent-load.js | 3 +- .../0.1/test/test-visibility-manager.js | 4 +- .../0.1/test/test-amp-app-banner.js | 12 +- .../0.1/test-e2e/test-vertical.js | 3 +- .../amp-carousel/0.1/test/test-slidescroll.js | 9 +- .../test/test-e2e/test-arrows-in-lightbox.js | 6 +- .../test/integration/test-integration-form.js | 9 +- .../test/integration/test-amp-fx-fly-in.js | 16 +- .../integration/test-amp-image-lightbox.js | 93 +- .../0.1/test-e2e/test-open-close.js | 3 +- .../1.0/test-e2e/test-custom-close-button.js | 3 +- .../test-e2e/test-amp-lightbox.js | 3 +- .../0.1/test-e2e/test-load-more-auto.js | 2 +- .../0.1/test/test-amp-mowplayer.js | 3 +- .../0.1/test/test-amp-powr-player.js | 23 +- .../0.1/test/integration/test-amp-script.js | 381 +- .../0.1/test/integration/test-amp-sidebar.js | 125 +- .../0.2/test/integration/test-amp-sidebar.js | 125 +- .../amp-sidebar/0.2/test/test-amp-sidebar.js | 2 +- .../1.0/test/test-amp-sticky-ad.js | 3 +- .../0.1/test-e2e/test-amp-story-auto-ads.js | 1 - .../amp-story/1.0/test/test-media-tasks.js | 3 +- .../0.1/test/test-amp-video-docking.js | 4 +- .../test-amp-video-flexible-bitrate.js | 6 +- .../test-amp-viewer-integration.js | 15 +- .../0.1/test/test-amp-viqeo-player.js | 4 +- test/e2e/test-amp-bind-iframe.js | 3 +- test/integration/test-amp-ad-type-custom.js | 1 - test/integration/test-amp-analytics.js | 51 +- test/integration/test-amp-bind.js | 595 ++- test/integration/test-amp-recaptcha-input.js | 264 +- test/integration/test-amp-skimlinks.js | 7 +- test/integration/test-amp-story-analytics.js | 4 +- test/integration/test-errors.js | 9 +- test/integration/test-released.js | 46 +- test/integration/test-shadow-amp.js | 21 +- test/integration/test-user-error-reporting.js | 5 +- test/integration/test-video-players.js | 33 +- test/unit/core/types/string/test-base64.js | 4 +- test/unit/test-cache-cid-api.js | 213 +- test/unit/test-chunk.js | 99 +- test/unit/test-cid.js | 1248 +++--- test/unit/test-crypto.js | 132 +- test/unit/test-custom-element.js | 3693 ++++++++--------- test/unit/test-document-info.js | 646 ++- test/unit/test-fixed-layer.js | 1316 +++--- test/unit/test-font-stylesheet-timeout.js | 383 +- test/unit/test-iframe-helper.js | 5 +- test/unit/test-iframe-stub.js | 154 +- test/unit/test-mutator.js | 138 +- test/unit/test-navigation.js | 196 +- test/unit/test-origin-experiments.js | 258 +- test/unit/test-performance.js | 591 ++- test/unit/test-preconnect.js | 48 +- test/unit/test-purifier.js | 1518 ++++--- test/unit/test-resources.js | 307 +- test/unit/test-runtime.js | 94 +- test/unit/test-storage.js | 658 ++- test/unit/test-url-replacements.js | 3564 ++++++++-------- test/unit/test-url-rewrite.js | 22 +- test/unit/test-url.js | 57 +- test/unit/test-viewport-binding.js | 108 +- test/unit/test-viewport.js | 30 +- test/unit/test-xhr.js | 1915 +++++---- test/unit/utils/test-dom-writer.js | 207 +- test/visual-diff/visual-tests.jsonc | 6 + 76 files changed, 9781 insertions(+), 10184 deletions(-) create mode 100644 examples/visual-tests/amp-form/amp-form-server-error.amp.html create mode 100644 examples/visual-tests/amp-form/amp-form-server-error.js diff --git a/ads/google/a4a/test/test-line-delimited-response-handler.js b/ads/google/a4a/test/test-line-delimited-response-handler.js index 01db3e0d6774..582ca59fa9b3 100644 --- a/ads/google/a4a/test/test-line-delimited-response-handler.js +++ b/ads/google/a4a/test/test-line-delimited-response-handler.js @@ -154,38 +154,28 @@ describes.sandboxed('#line-delimited-response-handler', {}, (env) => { }; }); - // TODO(lannka, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should handle empty streamed response properly', - () => { - slotData = []; - setup(); - return executeAndVerifyResponse(); - } - ); + it('should handle empty streamed response properly', () => { + slotData = []; + setup(); + return executeAndVerifyResponse(); + }); - // TODO(lannka, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari('should handle no fill response properly', () => { + it('should handle no fill response properly', () => { slotData = [{headers: {}, creative: ''}]; setup(); return executeAndVerifyResponse(); }); - // TODO(lannka, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should handle multiple no fill responses properly', - () => { - slotData = [ - {headers: {}, creative: ''}, - {headers: {}, creative: ''}, - ]; - setup(); - return executeAndVerifyResponse(); - } - ); + it('should handle multiple no fill responses properly', () => { + slotData = [ + {headers: {}, creative: ''}, + {headers: {}, creative: ''}, + ]; + setup(); + return executeAndVerifyResponse(); + }); - // TODO(lannka, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari('should stream properly', () => { + it('should stream properly', () => { slotData = [ {headers: {}, creative: ''}, { diff --git a/examples/visual-tests/amp-form/amp-form-server-error.amp.html b/examples/visual-tests/amp-form/amp-form-server-error.amp.html new file mode 100644 index 000000000000..64b6fa7759e4 --- /dev/null +++ b/examples/visual-tests/amp-form/amp-form-server-error.amp.html @@ -0,0 +1,57 @@ + + + + + AMP Form + + + + + + + + + +
+
+ + + + Please enter your first and last name separated by a space (e.g. Jane Miller) + +
+
+ + + +
+ +
+ +
+
+ +
+
+ + diff --git a/examples/visual-tests/amp-form/amp-form-server-error.js b/examples/visual-tests/amp-form/amp-form-server-error.js new file mode 100644 index 000000000000..32258a547941 --- /dev/null +++ b/examples/visual-tests/amp-form/amp-form-server-error.js @@ -0,0 +1,19 @@ +'use strict'; + +const { + verifySelectorsVisible, +} = require('../../../build-system/tasks/visual-diff/verifiers'); + +module.exports = { + 'try to submit to a dead server': async (page, name) => { + await page.tap('#name'); + await page.keyboard.type('Jane Miller'); + await page.tap('#email'); + await page.keyboard.type('jane.miller@ampproject.org'); + await page.tap('#submit'); + await verifySelectorsVisible(page, name, [ + '#form.user-valid', + 'div[submit-error] div[id^="rendered-message-amp-form-"]', + ]); + }, +}; diff --git a/examples/visual-tests/amp-form/amp-form.js b/examples/visual-tests/amp-form/amp-form.js index 3bb76668024f..c04b10939cc2 100644 --- a/examples/visual-tests/amp-form/amp-form.js +++ b/examples/visual-tests/amp-form/amp-form.js @@ -38,21 +38,4 @@ module.exports = { 'div[submit-success] div[id^="rendered-message-amp-form-"]', ]); }, - - // TODO(danielrozenberg): fix and restore this test - /* - 'try to submit to a dead server': async (page, name) => { - page.on('request', interceptedRequest => interceptedRequest.abort()); - - await page.tap('#name'); - await page.keyboard.type('Jane Miller'); - await page.tap('#email'); - await page.keyboard.type('jane.miller@ampproject.org'); - await page.tap('#submit'); - await verifySelectorsVisible(page, name, [ - '#form.user-valid', - 'div[submit-error] div[id^="rendered-message-amp-form-"]', - ]); - } - */ }; diff --git a/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js b/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js index e28464650bf4..4c082dcebc44 100644 --- a/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js +++ b/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js @@ -57,9 +57,7 @@ describes.realWin( return amp3dGltf; }; - // TODO (#16080): this test keeps timing out for some reason. - // Unskip when we figure out root cause. - it.skip('renders iframe', async () => { + it('renders iframe', async () => { await createElement(); expect(!!doc.body.querySelector('amp-3d-gltf > iframe')).to.be.true; }); diff --git a/extensions/amp-a4a/0.1/test/test-amp-a4a.js b/extensions/amp-a4a/0.1/test/test-amp-a4a.js index 7d5897ab371b..eef8d09ec89e 100644 --- a/extensions/amp-a4a/0.1/test/test-amp-a4a.js +++ b/extensions/amp-a4a/0.1/test/test-amp-a4a.js @@ -1059,8 +1059,7 @@ describes.realWin('amp-a4a', {amp: true}, (env) => { ['', 'client_cache', 'safeframe', 'some_random_thing'].forEach( (headerVal) => { - // TODO(wg-monetization, #25690): Fails on CI. - it.skip(`should not attach a NameFrame when header is ${headerVal}`, async () => { + it(`should not attach a NameFrame when header is ${headerVal}`, async () => { const devStub = env.sandbox.stub(dev(), 'error'); // Make sure there's no signature, so that we go down the 3p // iframe path. diff --git a/extensions/amp-access/0.1/test/test-amp-access-server-jwt.js b/extensions/amp-access/0.1/test/test-amp-access-server-jwt.js index 3eb7c6e04e19..bde0fe61a75a 100644 --- a/extensions/amp-access/0.1/test/test-amp-access-server-jwt.js +++ b/extensions/amp-access/0.1/test/test-amp-access-server-jwt.js @@ -2,6 +2,8 @@ import * as fakeTimers from '@sinonjs/fake-timers'; import {isUserErrorMessage} from '#utils/log'; +import {getMode} from 'src/mode'; + import * as DocumentFetcher from '../../../../src/document-fetcher'; import {removeFragment, serializeQueryString} from '../../../../src/url'; import {AccessServerJwtAdapter} from '../amp-access-server-jwt'; @@ -352,17 +354,16 @@ describes.realWin('AccessServerJwtAdapter', {amp: true}, (env) => { }); }); - // TODO - Restore this test, setting the development mode is not allowed. - // it.skip('should disable validation by default', () => { - // const savedDevFlag = getMode().development; - // getMode().development = false; - // const shouldBeValidatedInProdMode = adapter.shouldBeValidated_(); - // getMode().development = true; - // const shouldBeValidatedInDevMode = adapter.shouldBeValidated_(); - // getMode().development = savedDevFlag; - // expect(shouldBeValidatedInProdMode).to.be.false; - // expect(shouldBeValidatedInDevMode).to.be.true; - // }); + it('should disable validation by default', () => { + const savedDevFlag = getMode().development; + getMode().development = false; + const shouldBeValidatedInProdMode = adapter.shouldBeValidated_(); + getMode().development = true; + const shouldBeValidatedInDevMode = adapter.shouldBeValidated_(); + getMode().development = savedDevFlag; + expect(shouldBeValidatedInProdMode).to.be.false; + expect(shouldBeValidatedInDevMode).to.be.true; + }); it('should fetch JWT', () => { const authdata = {}; diff --git a/extensions/amp-access/0.1/test/test-jwt.js b/extensions/amp-access/0.1/test/test-jwt.js index d1db372450c6..de7487b4a0f7 100644 --- a/extensions/amp-access/0.1/test/test-jwt.js +++ b/extensions/amp-access/0.1/test/test-jwt.js @@ -87,45 +87,38 @@ describes.sandboxed('JwtHelper', {}, (env) => { 'o2kQ+X5xK9cipRgEKwIDAQAB\n' + '-----END PUBLIC KEY-----'; - // TODO(aghassemi, 6292): Unskip for Safari after #6292 - it.configure() - .skipSafari() - .run('should decode and verify token correctly', () => { - // Skip on non-subtle browser. - if (!helper.isVerificationSupported()) { - return; - } - return helper - .decodeAndVerify(TOKEN, Promise.resolve(PEM)) - .then((tok) => { - expect(tok['name']).to.equal('John Do'); - }); + it.configure().run('should decode and verify token correctly', () => { + // Skip on non-subtle browser. + if (!helper.isVerificationSupported()) { + return; + } + return helper.decodeAndVerify(TOKEN, Promise.resolve(PEM)).then((tok) => { + expect(tok['name']).to.equal('John Do'); }); + }); - it.configure() - .skipSafari() - .run('should fail invalid signature', () => { - // Skip on non-subtle browser. - if (!helper.isVerificationSupported()) { - return; - } + it.configure().run('should fail invalid signature', () => { + // Skip on non-subtle browser. + if (!helper.isVerificationSupported()) { + return; + } - const token = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' + - '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG8iLCJhZG1pbiI6MH0' + - '.' + - SIG; + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' + + '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG8iLCJhZG1pbiI6MH0' + + '.' + + SIG; - return helper.decodeAndVerify(token, Promise.resolve(PEM)).then( - () => { - throw new Error('must have failed'); - }, - (error) => { - // Expected. - expect(error.message).to.match(/Signature verification failed/); - } - ); - }); + return helper.decodeAndVerify(token, Promise.resolve(PEM)).then( + () => { + throw new Error('must have failed'); + }, + (error) => { + // Expected. + expect(error.message).to.match(/Signature verification failed/); + } + ); + }); }); describe('decodeAndVerify with mock subtle', () => { @@ -233,78 +226,74 @@ describes.sandboxed('JwtHelper', {}, (env) => { }); }); -// TODO(amphtml, #25621): Cannot find atob / btoa on Safari. -describes.sandboxed - .configure() - .skipSafari() - .run('pemToBytes', {}, () => { - const PLAIN_TEXT = - 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd' + - 'UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs' + - 'HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D' + - 'o2kQ+X5xK9cipRgEKwIDAQAB'; - const PEM = - '-----BEGIN PUBLIC KEY-----\n' + - 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\n' + - 'UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\n' + - 'HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\n' + - 'o2kQ+X5xK9cipRgEKwIDAQAB\n' + - '-----END PUBLIC KEY-----'; +describes.sandboxed.configure().run('pemToBytes', {}, () => { + const PLAIN_TEXT = + 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd' + + 'UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs' + + 'HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D' + + 'o2kQ+X5xK9cipRgEKwIDAQAB'; + const PEM = + '-----BEGIN PUBLIC KEY-----\n' + + 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\n' + + 'UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\n' + + 'HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\n' + + 'o2kQ+X5xK9cipRgEKwIDAQAB\n' + + '-----END PUBLIC KEY-----'; - it('should convert a valid key', () => { - const binary = pemToBytes(PEM); - const plain = atob(PLAIN_TEXT); - const len = plain.length; - expect(binary.byteLength).to.equal(len); - expect(binary[0]).to.equal(plain.charCodeAt(0)); - expect(binary[1]).to.equal(plain.charCodeAt(1)); - expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); - expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); - }); + it('should convert a valid key', () => { + const binary = pemToBytes(PEM); + const plain = atob(PLAIN_TEXT); + const len = plain.length; + expect(binary.byteLength).to.equal(len); + expect(binary[0]).to.equal(plain.charCodeAt(0)); + expect(binary[1]).to.equal(plain.charCodeAt(1)); + expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); + expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); + }); - it('should convert without headers, footers, line breaks', () => { - const binary = pemToBytes(PLAIN_TEXT); - const plain = atob(PLAIN_TEXT); - const len = plain.length; - expect(binary.byteLength).to.equal(len); - expect(binary[0]).to.equal(plain.charCodeAt(0)); - expect(binary[1]).to.equal(plain.charCodeAt(1)); - expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); - expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); - }); + it('should convert without headers, footers, line breaks', () => { + const binary = pemToBytes(PLAIN_TEXT); + const plain = atob(PLAIN_TEXT); + const len = plain.length; + expect(binary.byteLength).to.equal(len); + expect(binary[0]).to.equal(plain.charCodeAt(0)); + expect(binary[1]).to.equal(plain.charCodeAt(1)); + expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); + expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); + }); - it('should convert without line breaks', () => { - const binary = pemToBytes( - '-----BEGIN PUBLIC KEY-----' + PLAIN_TEXT + '-----END PUBLIC KEY-----' - ); - const plain = atob(PLAIN_TEXT); - const len = plain.length; - expect(binary.byteLength).to.equal(len); - expect(binary[0]).to.equal(plain.charCodeAt(0)); - expect(binary[1]).to.equal(plain.charCodeAt(1)); - expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); - expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); - }); + it('should convert without line breaks', () => { + const binary = pemToBytes( + '-----BEGIN PUBLIC KEY-----' + PLAIN_TEXT + '-----END PUBLIC KEY-----' + ); + const plain = atob(PLAIN_TEXT); + const len = plain.length; + expect(binary.byteLength).to.equal(len); + expect(binary[0]).to.equal(plain.charCodeAt(0)); + expect(binary[1]).to.equal(plain.charCodeAt(1)); + expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); + expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); + }); - it('should convert without header', () => { - const binary = pemToBytes(PLAIN_TEXT + '-----END PUBLIC KEY-----'); - const plain = atob(PLAIN_TEXT); - const len = plain.length; - expect(binary.byteLength).to.equal(len); - expect(binary[0]).to.equal(plain.charCodeAt(0)); - expect(binary[1]).to.equal(plain.charCodeAt(1)); - expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); - expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); - }); + it('should convert without header', () => { + const binary = pemToBytes(PLAIN_TEXT + '-----END PUBLIC KEY-----'); + const plain = atob(PLAIN_TEXT); + const len = plain.length; + expect(binary.byteLength).to.equal(len); + expect(binary[0]).to.equal(plain.charCodeAt(0)); + expect(binary[1]).to.equal(plain.charCodeAt(1)); + expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); + expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); + }); - it('should convert without footer', () => { - const binary = pemToBytes('-----BEGIN PUBLIC KEY-----' + PLAIN_TEXT); - const plain = atob(PLAIN_TEXT); - const len = plain.length; - expect(binary.byteLength).to.equal(len); - expect(binary[0]).to.equal(plain.charCodeAt(0)); - expect(binary[1]).to.equal(plain.charCodeAt(1)); - expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); - expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); - }); + it('should convert without footer', () => { + const binary = pemToBytes('-----BEGIN PUBLIC KEY-----' + PLAIN_TEXT); + const plain = atob(PLAIN_TEXT); + const len = plain.length; + expect(binary.byteLength).to.equal(len); + expect(binary[0]).to.equal(plain.charCodeAt(0)); + expect(binary[1]).to.equal(plain.charCodeAt(1)); + expect(binary[len - 1]).to.equal(plain.charCodeAt(len - 1)); + expect(binary[len - 2]).to.equal(plain.charCodeAt(len - 2)); }); +}); diff --git a/extensions/amp-accordion/0.1/test/integration/test-amp-accordion.js b/extensions/amp-accordion/0.1/test/integration/test-amp-accordion.js index 960569e786a5..8d90b2c69e35 100644 --- a/extensions/amp-accordion/0.1/test/integration/test-amp-accordion.js +++ b/extensions/amp-accordion/0.1/test/integration/test-amp-accordion.js @@ -1,10 +1,7 @@ -describes.sandboxed - .configure() - .skipEdge() - .run('amp-accordion', {}, function () { - this.timeout(10000); - const extensions = ['amp-accordion']; - const body = ` +describes.sandboxed('amp-accordion', {}, function () { + this.timeout(10000); + const extensions = ['amp-accordion']; + const body = `

@@ -14,38 +11,36 @@ describes.sandboxed

`; - describes.integration( - 'amp-accordion', - { - body, - extensions, - }, - (env) => { - let win, iframe, doc; - beforeEach(() => { - win = env.win; - iframe = env.iframe; - doc = win.document; - iframe.width = 300; - }); + describes.integration( + 'amp-accordion', + { + body, + extensions, + }, + (env) => { + let win, iframe, doc; + beforeEach(() => { + win = env.win; + iframe = env.iframe; + doc = win.document; + iframe.width = 300; + }); - it('should respect the media attribute', () => { - const accordion = doc.getElementById('media-accordion'); - expect(iframe.clientWidth).to.equal(300); - expect(accordion.className).to.match( + it('should respect the media attribute', () => { + const accordion = doc.getElementById('media-accordion'); + expect(iframe.clientWidth).to.equal(300); + expect(accordion.className).to.match(/i-amphtml-hidden-by-media-query/); + iframe.width = 600; + expect(iframe.clientWidth).to.equal(600); + return timeout(200).then(() => { + expect(accordion.className).to.not.match( /i-amphtml-hidden-by-media-query/ ); - iframe.width = 600; - expect(iframe.clientWidth).to.equal(600); - return timeout(200).then(() => { - expect(accordion.className).to.not.match( - /i-amphtml-hidden-by-media-query/ - ); - }); }); - } - ); - }); + }); + } + ); +}); /** * @param {number} ms diff --git a/extensions/amp-ad/0.1/test/test-amp-ad-xorigin-iframe-handler.js b/extensions/amp-ad/0.1/test/test-amp-ad-xorigin-iframe-handler.js index d474e384648e..5e1964273466 100644 --- a/extensions/amp-ad/0.1/test/test-amp-ad-xorigin-iframe-handler.js +++ b/extensions/amp-ad/0.1/test/test-amp-ad-xorigin-iframe-handler.js @@ -147,22 +147,17 @@ describes.sandboxed('amp-ad-xorigin-iframe-handler', {}, (env) => { }); }); - // TODO(@lannka): unskip flaky test - it.skip( - 'should resolve on message "no-content" ' + - 'and remove non-master iframe', - () => { - expect(iframe.style.visibility).to.equal('hidden'); - iframe.postMessageToParent({ - sentinel: 'amp3ptest' + testIndex, - type: 'no-content', - }); - return initPromise.then(() => { - expect(noContentSpy).to.be.calledWith(false); - expect(iframeHandler.iframe).to.be.null; - }); - } - ); + it('should resolve on message "no-content" and remove non-master iframe', () => { + expect(iframe.style.visibility).to.equal('hidden'); + iframe.postMessageToParent({ + sentinel: 'amp3ptest' + testIndex, + type: 'no-content', + }); + return initPromise.then(() => { + expect(noContentSpy).to.be.calledWith(false); + expect(iframeHandler.iframe).to.be.null; + }); + }); it('should NOT remove master iframe on message "no-content"', () => { iframe.name = 'test_master'; @@ -177,8 +172,7 @@ describes.sandboxed('amp-ad-xorigin-iframe-handler', {}, (env) => { }); }); - // TODO(#18656, lannka): Fails due to bad error message. - it.skip('should NOT resolve on message "bootstrap-loaded"', () => { + it('should NOT resolve on message "bootstrap-loaded"', () => { expect(iframe.style.visibility).to.equal('hidden'); iframe.postMessageToParent({ sentinel: 'amp3ptest' + testIndex, diff --git a/extensions/amp-ad/0.1/test/test-concurrent-load.js b/extensions/amp-ad/0.1/test/test-concurrent-load.js index 4dde04d904d7..6cb4585242ad 100644 --- a/extensions/amp-ad/0.1/test/test-concurrent-load.js +++ b/extensions/amp-ad/0.1/test/test-concurrent-load.js @@ -109,8 +109,7 @@ describes.realWin('concurrent-load', {}, (env) => { installTimerService(env.win); }); - // TODO(jeffkaufman, #13422): this test was silently failing - it.skip('should block if incremented', () => { + it('should block if incremented', () => { incrementLoadingAds(env.win); const start = Date.now(); return waitFor3pThrottle(env.win).then(() => diff --git a/extensions/amp-analytics/0.1/test/test-visibility-manager.js b/extensions/amp-analytics/0.1/test/test-visibility-manager.js index 5dfc13e6a328..bd185ca188d0 100644 --- a/extensions/amp-analytics/0.1/test/test-visibility-manager.js +++ b/extensions/amp-analytics/0.1/test/test-visibility-manager.js @@ -759,9 +759,7 @@ describes.fakeWin('VisibilityManagerForDoc', {amp: true}, (env) => { }); }); - // TODO(micajuineho): Figure why out why `state.totalVisibleTime` - // is returning 17. - it.skip('should listen on a resource', () => { + it('should listen on a resource', () => { clock.tick(1); const target = win.document.createElement('div'); target.id = 'targetElementId'; diff --git a/extensions/amp-app-banner/0.1/test/test-amp-app-banner.js b/extensions/amp-app-banner/0.1/test/test-amp-app-banner.js index f061f794c1e5..5d7dd7ec2f1d 100644 --- a/extensions/amp-app-banner/0.1/test/test-amp-app-banner.js +++ b/extensions/amp-app-banner/0.1/test/test-amp-app-banner.js @@ -153,16 +153,11 @@ describes.realWin( ); }); - // TODO(alanorozco, #15844): Unskip. - it.skip( - 'should show banner and set up correctly', - testSetupAndShowBanner - ); + it('should show banner and set up correctly', testSetupAndShowBanner); it('should throw if open button is missing', testButtonMissing); - // TODO(#16916): Make this test work with synchronous throws. - it.skip( + it( 'should remove banner if already dismissed', testRemoveBannerIfDismissed ); @@ -268,8 +263,7 @@ describes.realWin( it('should throw if open button is missing', testButtonMissing); - // TODO(#16916): Make this test work with synchronous throws. - it.skip( + it( 'should remove banner if already dismissed', testRemoveBannerIfDismissed ); diff --git a/extensions/amp-base-carousel/0.1/test-e2e/test-vertical.js b/extensions/amp-base-carousel/0.1/test-e2e/test-vertical.js index 8ddfb119d503..a1fcc654a2bf 100644 --- a/extensions/amp-base-carousel/0.1/test-e2e/test-vertical.js +++ b/extensions/amp-base-carousel/0.1/test-e2e/test-vertical.js @@ -38,8 +38,7 @@ describes.endtoend( await waitForCarouselImg(controller, 0); }); - // TODO(sparhami): unskip - it.skip('should layout the two adjacent slides', async () => { + it('should layout the two adjacent slides', async () => { // TODO(sparhami) Verify this is on the right of the 0th slide await waitForCarouselImg(controller, 1); // TODO(sparhami) Verify this is on the left of the 0th slide diff --git a/extensions/amp-carousel/0.1/test/test-slidescroll.js b/extensions/amp-carousel/0.1/test/test-slidescroll.js index 715cd01268dc..23ffacc2760e 100644 --- a/extensions/amp-carousel/0.1/test/test-slidescroll.js +++ b/extensions/amp-carousel/0.1/test/test-slidescroll.js @@ -297,8 +297,7 @@ describes.realWin( expect(impl.slides_[0].getAttribute('aria-hidden')).to.equal(null); }); - // TODO(#17197): This test triggers sinonjs/sinon issues 1709 and 1321. - it.skip('should hide the unwanted slides', async () => { + it('should hide the unwanted slides', async () => { const ampSlideScroll = await getAmpSlideScroll(); const impl = await ampSlideScroll.getImpl(); @@ -946,8 +945,7 @@ describes.realWin( expect(impl.slidesContainer_.scrollLeft).to.equal(impl.slideWidth_); }); - // TODO(#17197): This test triggers sinonjs/sinon issues 1709 and 1321. - it.skip('should hide unwanted slides when looping', async () => { + it('should hide unwanted slides when looping', async () => { const ampSlideScroll = await getAmpSlideScroll(true); const impl = await ampSlideScroll.getImpl(); @@ -1211,8 +1209,7 @@ describes.realWin( expect(showSlideSpy).to.have.callCount(3); }); - // TODO(#17197): This test triggers sinonjs/sinon issues 1709 and 1321. - it.skip('should update slide when `slide` attribute is mutated', async () => { + it('should update slide when `slide` attribute is mutated', async () => { const ampSlideScroll = await getAmpSlideScroll(true); const impl = await ampSlideScroll.getImpl(); expectAsyncConsoleError(/Invalid \[slide\] value:/, 1); diff --git a/extensions/amp-carousel/0.2/test/test-e2e/test-arrows-in-lightbox.js b/extensions/amp-carousel/0.2/test/test-e2e/test-arrows-in-lightbox.js index 84f821699bab..1bb85883cbd4 100644 --- a/extensions/amp-carousel/0.2/test/test-e2e/test-arrows-in-lightbox.js +++ b/extensions/amp-carousel/0.2/test/test-e2e/test-arrows-in-lightbox.js @@ -25,8 +25,7 @@ describes.endtoend( controller = env.controller; }); - // TODO(#35241): flaky test disabled in #35176 - it.skip('should open with both arrows', async () => { + it('should open with both arrows', async () => { // Click on image 2 const secondImage = await controller.findElement('#second'); await controller.click(secondImage); @@ -47,8 +46,7 @@ describes.endtoend( ).to.equal('false'); }); - // TODO(#35241): flaky test disabled in #35176 - it.skip('should open with one arrow', async () => { + it('should open with one arrow', async () => { // Click on last image const lastImage = await controller.findElement('#fourth'); await controller.click(lastImage); diff --git a/extensions/amp-form/0.1/test/integration/test-integration-form.js b/extensions/amp-form/0.1/test/integration/test-integration-form.js index e413e72b29d8..9e21d69e2595 100644 --- a/extensions/amp-form/0.1/test/integration/test-integration-form.js +++ b/extensions/amp-form/0.1/test/integration/test-integration-form.js @@ -164,8 +164,7 @@ describes.realWin( }); }); - // TODO(cvializ, #19647): Broken on SL Chrome 71. - describeChrome.skip('Submit xhr-POST', function () { + describeChrome.run('Submit xhr-POST', function () { this.timeout(RENDER_TIMEOUT); it('should submit and render success', () => { @@ -241,8 +240,7 @@ describes.realWin( }); }); - // TODO(cvializ, #19647): Broken on SL Chrome 71. - describeChrome.skip('Submit xhr-GET', function () { + describeChrome.run('Submit xhr-GET', function () { this.timeout(RENDER_TIMEOUT); it('should submit and render success', () => { @@ -320,8 +318,7 @@ describes.realWin( }); }); - // TODO(cvializ, #19647): Broken on SL Chrome 71. - describeChrome.skip('Submit result message', () => { + describeChrome.run('Submit result message', () => { it('should render messages with or without a template', () => { // Stubbing timeout to catch async-thrown errors and expect // them. These catch errors thrown inside the catch-clause of the diff --git a/extensions/amp-fx-collection/0.1/test/integration/test-amp-fx-fly-in.js b/extensions/amp-fx-collection/0.1/test/integration/test-amp-fx-fly-in.js index ab72224b1a8d..016600e654fc 100644 --- a/extensions/amp-fx-collection/0.1/test/integration/test-amp-fx-fly-in.js +++ b/extensions/amp-fx-collection/0.1/test/integration/test-amp-fx-fly-in.js @@ -44,14 +44,12 @@ describes.sandboxed.skip('amp-fx-collection', {}, function () { win = env.win; toggleExperiment(win, 'amp-fx-fly-in', true, false); }); - //TODO(esth, #19392): Fails on Firefox 63.0.0 - it.skip('runs fly-in-left animation with default parameters', () => { + it('runs fly-in-left animation with default parameters', async () => { expect(isExperimentOn(win, 'amp-fx-fly-in')).to.be.true; const initialLeft = getComputedLeft(win); win.scrollTo(0, 0.5 * getViewportHeight(win)); - return timeout(2000).then(() => { - expect(getComputedLeft(win)).to.be.above(initialLeft); - }); + await timeout(2000); + expect(getComputedLeft(win)).to.be.above(initialLeft); }); } ); @@ -82,14 +80,12 @@ describes.sandboxed.skip('amp-fx-collection', {}, function () { win = env.win; toggleExperiment(win, 'amp-fx-fly-in', true, false); }); - //TODO(esth, #19392): Fails on Firefox 63.0.0 - it.skip('runs fly-in-right animation with default parameters', () => { + it.skip('runs fly-in-right animation with default parameters', async () => { expect(isExperimentOn(win, 'amp-fx-fly-in')).to.be.true; const initialLeft = getComputedLeft(win); win.scrollTo(0, 0.5 * getViewportHeight(win)); - return timeout(2000).then(() => { - expect(getComputedLeft(win)).to.be.below(initialLeft); - }); + await timeout(2000); + expect(getComputedLeft(win)).to.be.below(initialLeft); }); } ); diff --git a/extensions/amp-image-lightbox/0.1/test/integration/test-amp-image-lightbox.js b/extensions/amp-image-lightbox/0.1/test/integration/test-amp-image-lightbox.js index b93303fa9369..3ef7f804925b 100644 --- a/extensions/amp-image-lightbox/0.1/test/integration/test-amp-image-lightbox.js +++ b/extensions/amp-image-lightbox/0.1/test/integration/test-amp-image-lightbox.js @@ -1,12 +1,9 @@ import {poll} from '#testing/iframe'; -describes.sandboxed - .configure() - .skipFirefox() - .run('amp-image-lightbox', {}, function () { - this.timeout(5000); - const extensions = ['amp-image-lightbox']; - const imageLightboxBody = ` +describes.sandboxed('amp-image-lightbox', {}, function () { + this.timeout(5000); + const extensions = ['amp-image-lightbox']; + const imageLightboxBody = `
`; - describes.integration( - 'amp-image-lightbox opens', - { - body: imageLightboxBody, - extensions, - }, - (env) => { - let win; - beforeEach(() => { - win = env.win; - }); + describes.integration( + 'amp-image-lightbox opens', + { + body: imageLightboxBody, + extensions, + }, + (env) => { + let win; + beforeEach(() => { + win = env.win; + }); - // TODO(wg-components, #25675) Flaky during cross-browser tests. - it.skip('should activate on tap of source image', () => { - const lightbox = win.document.getElementById('image-lightbox-1'); - expect(lightbox).to.have.display('none'); - const ampImage = win.document.getElementById('img0'); - const imageLoadedPromise = waitForImageToLoad(ampImage); - return imageLoadedPromise - .then(() => { - const ampImage = win.document.getElementById('img0'); - // Simulate a click on the img inside the amp-img, because this is - // what people tend to actually click on. - const openerImage = ampImage.querySelector( - 'img[amp-img-id="img0"]' - ); - const openedPromise = waitForLightboxOpen(win.document); - openerImage.click(); - return openedPromise; - }) - .then(() => { - const imageSelection = win.document.getElementsByClassName( - 'i-amphtml-image-lightbox-viewer-image' - ); - expect(imageSelection.length).to.equal(1); - const image = imageSelection[0]; - expect(image.tagName).to.equal('IMG'); - }); - }); - } - ); - }); + // TODO(wg-components, #25675) Flaky during cross-browser tests. + it.skip('should activate on tap of source image', () => { + const lightbox = win.document.getElementById('image-lightbox-1'); + expect(lightbox).to.have.display('none'); + const ampImage = win.document.getElementById('img0'); + const imageLoadedPromise = waitForImageToLoad(ampImage); + return imageLoadedPromise + .then(() => { + const ampImage = win.document.getElementById('img0'); + // Simulate a click on the img inside the amp-img, because this is + // what people tend to actually click on. + const openerImage = ampImage.querySelector( + 'img[amp-img-id="img0"]' + ); + const openedPromise = waitForLightboxOpen(win.document); + openerImage.click(); + return openedPromise; + }) + .then(() => { + const imageSelection = win.document.getElementsByClassName( + 'i-amphtml-image-lightbox-viewer-image' + ); + expect(imageSelection.length).to.equal(1); + const image = imageSelection[0]; + expect(image.tagName).to.equal('IMG'); + }); + }); + } + ); +}); function waitForLightboxOpen(document) { return poll('wait for image-lightbox-1 to open', () => { diff --git a/extensions/amp-lightbox-gallery/0.1/test-e2e/test-open-close.js b/extensions/amp-lightbox-gallery/0.1/test-e2e/test-open-close.js index 9989a75d603f..6abb6d682b52 100644 --- a/extensions/amp-lightbox-gallery/0.1/test-e2e/test-open-close.js +++ b/extensions/amp-lightbox-gallery/0.1/test-e2e/test-open-close.js @@ -26,8 +26,7 @@ describes.endtoend( // TODO(sparhami) Cover swipe to dismiss if possible. // TODO(sparhami) Test basic transition to gallery and back. - // TODO(#28948) fix this flaky test. - it.skip('should open/close lightbox', async () => { + it('should open/close lightbox', async () => { // First open the gallery. const firstAmpImg = await controller.findElement('amp-img'); await controller.click(firstAmpImg); diff --git a/extensions/amp-lightbox/1.0/test-e2e/test-custom-close-button.js b/extensions/amp-lightbox/1.0/test-e2e/test-custom-close-button.js index 42b7ce757b29..f5d24bd323a9 100644 --- a/extensions/amp-lightbox/1.0/test-e2e/test-custom-close-button.js +++ b/extensions/amp-lightbox/1.0/test-e2e/test-custom-close-button.js @@ -29,8 +29,7 @@ describes.endtoend( ).to.equal(0); }); - // TODO(wg-components, #28948): Flaky during CI. - it.skip('should open the lightbox', async () => { + it('should open the lightbox', async () => { const open = await controller.findElement('#open'); await controller.click(open); diff --git a/extensions/amp-lightbox/test-e2e/test-amp-lightbox.js b/extensions/amp-lightbox/test-e2e/test-amp-lightbox.js index c276cc81bfc6..ad5fed3df5ac 100644 --- a/extensions/amp-lightbox/test-e2e/test-amp-lightbox.js +++ b/extensions/amp-lightbox/test-e2e/test-amp-lightbox.js @@ -33,8 +33,7 @@ describes.endtoend( ).to.equal(0); }); - // TODO(#33413): Fix flaky tests - it.skip('should open the lightbox', async () => { + it('should open the lightbox', async () => { const open = await controller.findElement('#open'); await controller.click(open); diff --git a/extensions/amp-list/0.1/test-e2e/test-load-more-auto.js b/extensions/amp-list/0.1/test-e2e/test-load-more-auto.js index 4891f40b15ac..8565364c7c69 100644 --- a/extensions/amp-list/0.1/test-e2e/test-load-more-auto.js +++ b/extensions/amp-list/0.1/test-e2e/test-load-more-auto.js @@ -46,7 +46,7 @@ describes.endtoend( ); }); - it.skip('should load more items on scroll', async () => { + it('should load more items on scroll', async () => { let listItems = await controller.findElements('.item'); await expect(listItems).to.have.length(2); diff --git a/extensions/amp-mowplayer/0.1/test/test-amp-mowplayer.js b/extensions/amp-mowplayer/0.1/test/test-amp-mowplayer.js index 29771f7ff9ce..73cab40873d6 100644 --- a/extensions/amp-mowplayer/0.1/test/test-amp-mowplayer.js +++ b/extensions/amp-mowplayer/0.1/test/test-amp-mowplayer.js @@ -54,8 +54,7 @@ describes.realWin( }); } - // TODO(#38720): fix flaky test. - describe.skip('with data-mediaid', function () { + describe('with data-mediaid', function () { runTestsForDatasource(EXAMPLE_VIDEOID); }); diff --git a/extensions/amp-powr-player/0.1/test/test-amp-powr-player.js b/extensions/amp-powr-player/0.1/test/test-amp-powr-player.js index 5398e2656843..11fac5b42a94 100644 --- a/extensions/amp-powr-player/0.1/test/test-amp-powr-player.js +++ b/extensions/amp-powr-player/0.1/test/test-amp-powr-player.js @@ -4,7 +4,6 @@ import {listenOncePromise} from '#utils/event-helper'; import {parseUrlDeprecated} from '../../../../src/url'; import {VideoEvents_Enum} from '../../../../src/video-interface'; -// TODO(#38975): fix all skipped tests in this file. describes.realWin( 'amp-powr-player', { @@ -50,7 +49,7 @@ describes.realWin( }); } - it.skip('renders', () => { + it('renders', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', @@ -66,7 +65,7 @@ describes.realWin( }); }); - it.skip('renders responsively', () => { + it('renders responsively', () => { return getPowrPlayer( { 'data-account': '945', @@ -81,14 +80,14 @@ describes.realWin( }); }); - it.skip('requires data-account', () => { + it('requires data-account', () => { expectAsyncConsoleError(/The data-account attribute is required for/, 1); return getPowrPlayer({}).should.eventually.be.rejectedWith( /The data-account attribute is required for/ ); }); - it.skip('requires data-player', () => { + it('requires data-player', () => { expectAsyncConsoleError(/The data-player attribute is required for/, 1); return getPowrPlayer({ 'data-account': '945', @@ -97,7 +96,7 @@ describes.realWin( ); }); - it.skip('removes iframe after unlayoutCallback', async () => { + it('removes iframe after unlayoutCallback', async () => { const bc = await getPowrPlayer( { 'data-account': '945', @@ -114,7 +113,7 @@ describes.realWin( expect(obj.iframe_).to.be.null; }); - it.skip('should pass data-param-* attributes to the iframe src', () => { + it('should pass data-param-* attributes to the iframe src', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', @@ -122,12 +121,12 @@ describes.realWin( 'data-param-foo': 'bar', }).then((bc) => { const iframe = bc.querySelector('iframe'); - const params = parseUrlDeprecated(iframe.src).search.split.skip('&'); + const params = parseUrlDeprecated(iframe.src).search.split('&'); expect(params).to.contain('foo=bar'); }); }); - it.skip('should propagate mutated attributes', () => { + it('should propagate mutated attributes', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', @@ -156,7 +155,7 @@ describes.realWin( }); }); - it.skip('should pass referrer', () => { + it('should pass referrer', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', @@ -169,7 +168,7 @@ describes.realWin( }); }); - it.skip('should force playsinline', () => { + it('should force playsinline', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', @@ -182,7 +181,7 @@ describes.realWin( }); }); - it.skip('should forward events', () => { + it('should forward events', () => { return getPowrPlayer({ 'data-account': '945', 'data-player': '1', diff --git a/extensions/amp-script/0.1/test/integration/test-amp-script.js b/extensions/amp-script/0.1/test/integration/test-amp-script.js index 93f27ce83bdf..658009ce3174 100644 --- a/extensions/amp-script/0.1/test/integration/test-amp-script.js +++ b/extensions/amp-script/0.1/test/integration/test-amp-script.js @@ -7,223 +7,220 @@ function poll(description, condition, opt_onError) { return classicPoll(description, condition, opt_onError, TIMEOUT); } -describes.sandboxed - .configure() - .skipFirefox() - .run('amp-script', {}, function () { - this.timeout(TIMEOUT); - - let browser, doc, element; - - describes.integration( - 'container ', - { - body: ` +describes.sandboxed('amp-script', {}, function () { + this.timeout(TIMEOUT); + + let browser, doc, element; + + describes.integration( + 'container ', + { + body: ` `, - extensions: ['amp-script'], - }, - (env) => { - beforeEach(() => { - browser = new BrowserController(env.win); - doc = env.win.document; - element = doc.querySelector('amp-script'); + extensions: ['amp-script'], + }, + (env) => { + beforeEach(() => { + browser = new BrowserController(env.win); + doc = env.win.document; + element = doc.querySelector('amp-script'); + }); + + it('should say "hello world"', function* () { + yield poll(' to be hydrated', () => + element.classList.contains('i-amphtml-hydrated') + ); + const impl = yield element.getImpl(); + + // Give event listeners in hydration a moment to attach. + yield browser.wait(100); + + env.sandbox + .stub(impl.getUserActivation(), 'isActive') + .callsFake(() => true); + browser.click('button#hello'); + + yield poll('mutations applied', () => { + const h1 = doc.querySelector('h1'); + return h1 && h1.textContent == 'Hello World!'; }); - - it('should say "hello world"', function* () { - yield poll(' to be hydrated', () => - element.classList.contains('i-amphtml-hydrated') - ); - const impl = yield element.getImpl(); - - // Give event listeners in hydration a moment to attach. - yield browser.wait(100); - - env.sandbox - .stub(impl.getUserActivation(), 'isActive') - .callsFake(() => true); - browser.click('button#hello'); - - yield poll('mutations applied', () => { - const h1 = doc.querySelector('h1'); - return h1 && h1.textContent == 'Hello World!'; - }); + }); + + it('should terminate without gesture', function* () { + yield poll(' to be hydrated', () => + element.classList.contains('i-amphtml-hydrated') + ); + const impl = yield element.getImpl(); + + // Give event listeners in hydration a moment to attach. + yield browser.wait(100); + + env.sandbox + .stub(impl.getUserActivation(), 'isActive') + .callsFake(() => false); + browser.click('button#hello'); + + // Give mutations time to apply. + yield browser.wait(100); + yield poll('terminated', () => { + return element.classList.contains('i-amphtml-broken'); }); - - it('should terminate without gesture', function* () { - yield poll(' to be hydrated', () => - element.classList.contains('i-amphtml-hydrated') - ); - const impl = yield element.getImpl(); - - // Give event listeners in hydration a moment to attach. - yield browser.wait(100); - - env.sandbox - .stub(impl.getUserActivation(), 'isActive') - .callsFake(() => false); - browser.click('button#hello'); - - // Give mutations time to apply. - yield browser.wait(100); - yield poll('terminated', () => { - return element.classList.contains('i-amphtml-broken'); - }); - }); - - it('should start long task', function* () { - yield poll(' to be hydrated', () => - element.classList.contains('i-amphtml-hydrated') - ); - const impl = yield element.getImpl(); - - // Give event listeners in hydration a moment to attach. - yield browser.wait(100); - - env.sandbox - .stub(impl.getUserActivation(), 'isActive') - .callsFake(() => true); - // TODO(dvoytenko): Find a way to test this with the race condition when - // the resource is fetched before the first polling iteration. - const stub = env.sandbox.stub( - impl.getUserActivation(), - 'expandLongTask' - ); - browser.click('button#long'); - yield poll('long task started', () => { - return stub.callCount > 0; - }); + }); + + it('should start long task', function* () { + yield poll(' to be hydrated', () => + element.classList.contains('i-amphtml-hydrated') + ); + const impl = yield element.getImpl(); + + // Give event listeners in hydration a moment to attach. + yield browser.wait(100); + + env.sandbox + .stub(impl.getUserActivation(), 'isActive') + .callsFake(() => true); + // TODO(dvoytenko): Find a way to test this with the race condition when + // the resource is fetched before the first polling iteration. + const stub = env.sandbox.stub( + impl.getUserActivation(), + 'expandLongTask' + ); + browser.click('button#long'); + yield poll('long task started', () => { + return stub.callCount > 0; }); - } - ); - - describes.integration( - 'sanitizer', - { - body: ` + }); + } + ); + + describes.integration( + 'sanitizer', + { + body: `

Number of mutations: 0

`, - extensions: ['amp-script'], - }, - (env) => { - beforeEach(() => { - browser = new BrowserController(env.win); - doc = env.win.document; - element = doc.querySelector('amp-script'); + extensions: ['amp-script'], + }, + (env) => { + beforeEach(() => { + browser = new BrowserController(env.win); + doc = env.win.document; + element = doc.querySelector('amp-script'); + }); + + it('should sanitize `, - extensions: ['amp-analytics'], - ampdoc: 'shadow', - }, - () => { - it('should send request', () => { - return RequestBank.withdraw().then((req) => { - expect(req.url).to.match(/\/?a=2&b=Shadow%20Viewer&cid=amp-.*/); - expect( - req.headers.referer, - 'should keep referrer if no referrerpolicy specified' - ).to.be.ok; - }); + extensions: ['amp-analytics'], + ampdoc: 'shadow', + }, + () => { + it('should send request', () => { + return RequestBank.withdraw().then((req) => { + expect(req.url).to.match(/\/?a=2&b=Shadow%20Viewer&cid=amp-.*/); + expect( + req.headers.referer, + 'should keep referrer if no referrerpolicy specified' + ).to.be.ok; }); - } - ); - }); + }); + } + ); + }); }); diff --git a/test/integration/test-amp-bind.js b/test/integration/test-amp-bind.js index 03646b954808..5c84bb154340 100644 --- a/test/integration/test-amp-bind.js +++ b/test/integration/test-amp-bind.js @@ -3,60 +3,55 @@ import {poll as classicPoll} from '#testing/iframe'; const TIMEOUT = 10000; -// Skip Edge, which throws "Permission denied" errors when inspecting -// element properties in the testing iframe (Edge 17, Windows 10). -describes.sandboxed - .configure() - .skipEdge() - .run('amp-bind', {}, function () { - this.timeout(TIMEOUT); - - // Helper that sets the poll timeout. - function poll(desc, condition, onError) { - return classicPoll(desc, condition, onError, TIMEOUT); - } - - describes.integration( - 'basic', - { - body: ` +describes.sandboxed('amp-bind', {}, function () { + this.timeout(TIMEOUT); + + // Helper that sets the poll timeout. + function poll(desc, condition, onError) { + return classicPoll(desc, condition, onError, TIMEOUT); + } + + describes.integration( + 'basic', + { + body: `

before_text

`, - extensions: ['amp-bind'], - }, - (env) => { - let browser; - let doc; - let text; - - beforeEach(() => { - doc = env.win.document; - text = doc.querySelector('p'); - browser = new BrowserController(env.win); - }); - - it('[text]', function* () { - expect(text.textContent).to.equal('before_text'); - yield browser.wait(200); - browser.click('#changeText'); - yield poll('[text]', () => text.textContent === 'after_text'); - }); - - it('[class]', function* () { - expect(text.className).to.equal('before_class'); - yield browser.wait(200); - browser.click('#changeClass'); - yield poll('[class]', () => text.className === 'after_class'); - }); - } - ); - - describes.integration( - '+ amp-img', - { - body: ` + extensions: ['amp-bind'], + }, + (env) => { + let browser; + let doc; + let text; + + beforeEach(() => { + doc = env.win.document; + text = doc.querySelector('p'); + browser = new BrowserController(env.win); + }); + + it('[text]', function* () { + expect(text.textContent).to.equal('before_text'); + yield browser.wait(200); + browser.click('#changeText'); + yield poll('[text]', () => text.textContent === 'after_text'); + }); + + it('[class]', function* () { + expect(text.className).to.equal('before_class'); + yield browser.wait(200); + browser.click('#changeClass'); + yield poll('[class]', () => text.className === 'after_class'); + }); + } + ); + + describes.integration( + '+ amp-img', + { + body: ` `, - extensions: ['amp-bind'], - }, - (env) => { - let doc, img; - - beforeEach(() => { - doc = env.win.document; - img = doc.querySelector('amp-img'); - }); - - it('[src] with valid URL', () => { - const button = doc.getElementById('changeSrc'); - expect(img.getAttribute('src')).to.equal( - 'http://example.com/before.jpg' - ); - button.click(); - return poll( - '[src]', - () => img.getAttribute('src') === 'http://example.com/after.jpg' - ); - }); - - it('[alt]', () => { - const button = doc.getElementById('changeAlt'); - expect(img.getAttribute('alt')).to.equal('before_alt'); - button.click(); - return poll('[src]', () => img.getAttribute('alt') === 'after_alt'); - }); - - it('[width] and [height]', () => { - const button = doc.getElementById('changeSize'); - expect(img.getAttribute('width')).to.equal('1'); - expect(img.getAttribute('height')).to.equal('1'); - button.click(); - return Promise.all([ - poll('[width]', () => img.getAttribute('width') === '2'), - poll('[height]', () => img.getAttribute('height') === '2'), - ]); - }); - } - ); - - describes.integration( - '+ forms', - { - body: ` + extensions: ['amp-bind'], + }, + (env) => { + let doc, img; + + beforeEach(() => { + doc = env.win.document; + img = doc.querySelector('amp-img'); + }); + + it('[src] with valid URL', () => { + const button = doc.getElementById('changeSrc'); + expect(img.getAttribute('src')).to.equal( + 'http://example.com/before.jpg' + ); + button.click(); + return poll( + '[src]', + () => img.getAttribute('src') === 'http://example.com/after.jpg' + ); + }); + + it('[alt]', () => { + const button = doc.getElementById('changeAlt'); + expect(img.getAttribute('alt')).to.equal('before_alt'); + button.click(); + return poll('[src]', () => img.getAttribute('alt') === 'after_alt'); + }); + + it('[width] and [height]', () => { + const button = doc.getElementById('changeSize'); + expect(img.getAttribute('width')).to.equal('1'); + expect(img.getAttribute('height')).to.equal('1'); + button.click(); + return Promise.all([ + poll('[width]', () => img.getAttribute('width') === '2'), + poll('[height]', () => img.getAttribute('height') === '2'), + ]); + }); + } + ); + + describes.integration( + '+ forms', + { + body: `

before_range

@@ -123,67 +118,67 @@ describes.sandboxed

before_radio

`, - extensions: ['amp-bind'], - }, - (env) => { - let doc; - - beforeEach(() => { - doc = env.win.document; - }); - - it('input[type=range] on:change', () => { - const rangeText = doc.getElementById('range'); - const range = doc.querySelector('input[type="range"]'); - expect(rangeText.textContent).to.equal('before_range'); - // Calling #click() on the range element will not generate a change event, - // so it must be generated manually. - range.value = 47; - range.dispatchEvent(new Event('change', {bubbles: true})); - poll('[text]', () => rangeText.textContent === '0 <= 47 <= 100'); - }); - - it('input[type=checkbox] on:change', () => { - const checkboxText = doc.getElementById('checkbox'); - const checkbox = doc.querySelector('input[type="checkbox"]'); - expect(checkboxText.textContent).to.equal('before_check'); - checkbox.click(); - poll('[text]', () => checkboxText.textContent === 'checked: true'); - }); - - it('[checked]', function* () { - const checkbox = doc.querySelector('input[type="checkbox"]'); - const button = doc.querySelector('button'); - - checkbox.click(); - // Note that attributes are initial values, properties are current values. - expect(checkbox.hasAttribute('checked')).to.be.false; - expect(checkbox.checked).to.be.true; - - button.click(); - yield poll('[checked]', () => !checkbox.checked); - expect(checkbox.hasAttribute('checked')).to.be.false; - - button.click(); - yield poll('[checked]', () => checkbox.checked); - // amp-bind sets both the attribute and property. - expect(checkbox.hasAttribute('checked')).to.be.true; - }); - - it('input[type=radio] on:change', () => { - const radioText = doc.getElementById('radio'); - const radio = doc.querySelector('input[type="radio"]'); - expect(radioText.textContent).to.equal('before_radio'); - radio.click(); - poll('[text]', () => radioText.textContent === 'checked: true'); - }); - } - ); - - describes.integration( - '+ amp-carousel', - { - body: ` + extensions: ['amp-bind'], + }, + (env) => { + let doc; + + beforeEach(() => { + doc = env.win.document; + }); + + it('input[type=range] on:change', () => { + const rangeText = doc.getElementById('range'); + const range = doc.querySelector('input[type="range"]'); + expect(rangeText.textContent).to.equal('before_range'); + // Calling #click() on the range element will not generate a change event, + // so it must be generated manually. + range.value = 47; + range.dispatchEvent(new Event('change', {bubbles: true})); + poll('[text]', () => rangeText.textContent === '0 <= 47 <= 100'); + }); + + it('input[type=checkbox] on:change', () => { + const checkboxText = doc.getElementById('checkbox'); + const checkbox = doc.querySelector('input[type="checkbox"]'); + expect(checkboxText.textContent).to.equal('before_check'); + checkbox.click(); + poll('[text]', () => checkboxText.textContent === 'checked: true'); + }); + + it('[checked]', function* () { + const checkbox = doc.querySelector('input[type="checkbox"]'); + const button = doc.querySelector('button'); + + checkbox.click(); + // Note that attributes are initial values, properties are current values. + expect(checkbox.hasAttribute('checked')).to.be.false; + expect(checkbox.checked).to.be.true; + + button.click(); + yield poll('[checked]', () => !checkbox.checked); + expect(checkbox.hasAttribute('checked')).to.be.false; + + button.click(); + yield poll('[checked]', () => checkbox.checked); + // amp-bind sets both the attribute and property. + expect(checkbox.hasAttribute('checked')).to.be.true; + }); + + it('input[type=radio] on:change', () => { + const radioText = doc.getElementById('radio'); + const radio = doc.querySelector('input[type="radio"]'); + expect(radioText.textContent).to.equal('before_radio'); + radio.click(); + poll('[text]', () => radioText.textContent === 'checked: true'); + }); + } + ); + + describes.integration( + '+ amp-carousel', + { + body: `

0

`, - extensions: ['amp-bind', 'amp-carousel'], - }, - (env) => { - let doc, carousel, slideText; - - beforeEach(() => { - doc = env.win.document; - carousel = doc.querySelector('amp-carousel'); - slideText = doc.querySelector('p'); - - const browserController = new BrowserController(env.win); - return browserController.waitForElementLayout('amp-carousel'); - }); - - it('on:slideChange', () => { - expect(slideText.textContent).to.equal('0'); - - const nextSlide = carousel.querySelector( - 'div.amp-carousel-button-next' - ); - nextSlide.click(); - return poll('[slide]', () => slideText.textContent === '1'); - }); - - it('[slide]', function* () { - const slides = carousel.querySelectorAll( - '.i-amphtml-slide-item > amp-img' - ); - const first = slides[0]; - const second = slides[1]; - - expect(first.getAttribute('aria-hidden')).to.equal('false'); - expect(second.getAttribute('aria-hidden')).to.be.equal('true'); - - const button = doc.getElementById('goToSlideOne'); - button.click(); - - yield poll( - '[slide]', - () => first.getAttribute('aria-hidden') === 'true' - ); - yield poll( - '[slide]', - () => second.getAttribute('aria-hidden') === 'false' - ); - }); - } - ); - - const list = ` + extensions: ['amp-bind', 'amp-carousel'], + }, + (env) => { + let doc, carousel, slideText; + + beforeEach(() => { + doc = env.win.document; + carousel = doc.querySelector('amp-carousel'); + slideText = doc.querySelector('p'); + + const browserController = new BrowserController(env.win); + return browserController.waitForElementLayout('amp-carousel'); + }); + + it('on:slideChange', () => { + expect(slideText.textContent).to.equal('0'); + + const nextSlide = carousel.querySelector( + 'div.amp-carousel-button-next' + ); + nextSlide.click(); + return poll('[slide]', () => slideText.textContent === '1'); + }); + + it('[slide]', function* () { + const slides = carousel.querySelectorAll( + '.i-amphtml-slide-item > amp-img' + ); + const first = slides[0]; + const second = slides[1]; + + expect(first.getAttribute('aria-hidden')).to.equal('false'); + expect(second.getAttribute('aria-hidden')).to.be.equal('true'); + + const button = doc.getElementById('goToSlideOne'); + button.click(); + + yield poll( + '[slide]', + () => first.getAttribute('aria-hidden') === 'true' + ); + yield poll( + '[slide]', + () => second.getAttribute('aria-hidden') === 'false' + ); + }); + } + ); + + const list = ` c')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - }); - - it('should NOT output security-sensitive markup when nested', () => { - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - expect(purify('ac')).to.be.equal('ac'); - }); + '' + ) + ).to.be.equal( + '

abc' + + '

' + ); + expect(rewriteAttributeValueSpy).to.be.calledWith( + 'amp-img', + 'src', + 'http://example.com/1.png' + ); + }); - it('should NOT output security-sensitive markup when broken', () => { - expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + }); - it('should output "on" attribute', () => { - expect(purify('ab')).to.be.equal( - 'ab' - ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); - }); + it('should NOT output security-sensitive markup when nested', () => { + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + expect(purify('ac')).to.be.equal('ac'); + }); - it('should output "data-, aria-, and role" attributes', () => { - // Can't use string equality since DOMPurify will reorder attributes. - const actual = serialize( - purify('b') - ); - const expected = serialize( - 'b' - ); - expectEqualNodeLists(actual, expected); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(3); - }); + it('should NOT output security-sensitive markup when broken', () => { + expect(purify('a">')).to.equal( - '' + expect(purify('ab')).to.be.equal( + 'ab' ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); - }); - - it('should allow div::template', () => { - expect(purify('
')).to.equal( - '
' - ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); - }); - - it('should allow form::action-xhr', () => { - expect(purify('
')).to.equal( - '
' + expect(purify('ab')).to.be.equal( + 'ab' ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); - }); - - it('should allow input::mask-output', () => { - expect(purify('')).to.equal( - '' + expect(purify('ab')).to.be.equal( + 'ab' ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); - }); - - // Need to test this since DOMPurify doesn't offer a API for tag-specific - // attribute allowlists. Instead, we hack around it with custom hooks. - it('should not allow unsupported attributes after a valid one', () => { - const html = - '
' + - '

'; - expect(purify(html)).to.equal( - '

' - ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); }); + }); - it('should allow -related attributes', () => { - expect(purify('
')).to.equal( - '
' - ); - expect(purify('
')).to.equal( - '
' + it('should NOT output denylisted values for class attributes', () => { + allowConsoleError(() => { + expect(purify('

hello

')).to.be.equal( + '

hello

' ); - expect(purify('
')).to.equal( - '
' + expect(purify('

hello

')).to.be.equal( + '

hello

' ); - expect(purify('
')).to.equal( - '
' + expect(purify('

hello

')).to.be.equal( + '

hello

' ); - expect( - purify('') - ).to.equal(''); - expect(purify('')).to.equal( - '' - ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(0); }); + }); - it('should avoid disallowing default-supported attributes', () => { - // We allowlist all attributes of AMP elements, but make sure we don't - // remove default-supported attributes from the allowlist afterwards. - expect( - purify( - '

' - ) - ).to.equal( - '

' - ); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); - }); + it('should allow amp-subscriptions attributes', () => { + expect(purify('
link
')).to.equal( + '
link
' + ); + expect( + purify('
link
') + ).to.equal('
link
'); + expect(purify('
link
')).to.equal( + '
link
' + ); + expect(purify('
link
')).to.equal( + '
link
' + ); + expect(purify('
link
')).to.equal( + '
link
' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + }); - it('should allow attributes', () => { - expect(purify('')).to.match( - /<\/amp-lightbox>/ - ); - }); + it('should allow source::src with valid protocol', () => { + expect(purify('')).to.equal( + '' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); + }); - it('should output diff marker attributes for some elements', () => { - // Elements with bindings should have [i-amphtml-key=]. - expect(purify('

')).to.match( - /

<\/p>/ - ); - // AMP elements should have [i-amphtml-key=]. - expect(purify('')).to.match( - /<\/amp-pixel>/ - ); - // AMP elements with bindings should have [i-amphtml-key=]. - expect(purify('')).to.match( - /<\/amp-pixel>/ - ); - // amp-img should have [i-amphtml-ignore]. - expect(purify('')).to.equal( - '' - ); - // Other elements should NOT have [i-amphtml-key]. - expect(purify('

')).to.equal('

'); - expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); - }); + // TODO(choumx): HTTPS-only URI attributes are not enforced consistently + // in the sanitizer yet. E.g. amp-video requires HTTPS, amp-img does not. + // Unskip when this is fixed. + it.skip('should not allow source::src with invalid protocol', () => { + expect(purify('')).to.equal( + '' + ); + expect(purify('')).to.equal( + '' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); + }); - it('should resolve URLs', () => { - expect(purify('')).to.match(/http/); - expect(purify('')).to.match(/http/); - expect(purify('')).to.match(/http/); - }); + it('should allow div::template', () => { + expect(purify('
')).to.equal( + '
' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); }); - describe('purifyTagsForTripleMustache()', () => { - it('should output basic text', () => { - expect(purifyTripleMustache('abc')).to.be.equal('abc'); - }); + it('should allow form::action-xhr', () => { + expect(purify('
')).to.equal( + '
' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); + }); - it('should output HTML entities', () => { - const entity = '<tag>'; - expect(purifyTripleMustache(entity)).to.be.equal(entity); - // DOMPurify short-circuits when there are no '<' characters. - expect(purifyTripleMustache(`

${entity}

`)).to.be.equal( - `

${entity}

` - ); - }); + it('should allow input::mask-output', () => { + expect(purify('')).to.equal( + '' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(1); + }); - it('should output valid markup', () => { - expect(purifyTripleMustache('abc')).to.be.equal('abc'); - expect(purifyTripleMustache('ab
c
')).to.be.equal( - 'ab
c
' - ); - expect(purifyTripleMustache('abc')).to.be.equal( - 'abc' - ); - const markupWithClassAttribute = '

heading

'; - expect(purifyTripleMustache(markupWithClassAttribute)).to.be.equal( - markupWithClassAttribute - ); - const markupWithClassesAttribute = - '
heading
'; - expect(purifyTripleMustache(markupWithClassesAttribute)).to.be.equal( - markupWithClassesAttribute - ); - const markupParagraph = '

paragraph

'; - expect(purifyTripleMustache(markupParagraph)).to.be.equal( - markupParagraph - ); - }); + // Need to test this since DOMPurify doesn't offer a API for tag-specific + // attribute allowlists. Instead, we hack around it with custom hooks. + it('should not allow unsupported attributes after a valid one', () => { + const html = + '
' + + '

'; + expect(purify(html)).to.equal( + '

' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + }); - it('should NOT output non-allowlisted markup', () => { - expect(purifyTripleMustache('ac')).to.be.equal('ac'); - expect(purifyTripleMustache('ac')).to.be.equal('ac'); - }); + it('should allow -related attributes', () => { + expect(purify('
')).to.equal( + '
' + ); + expect(purify('
')).to.equal( + '
' + ); + expect(purify('
')).to.equal( + '
' + ); + expect(purify('
')).to.equal( + '
' + ); + expect( + purify('') + ).to.equal(''); + expect(purify('')).to.equal( + '' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + }); - it('should compensate for broken markup', () => { - expect(purifyTripleMustache('ab')).to.be.equal( - 'ab' - ); - }); + it('should avoid disallowing default-supported attributes', () => { + // We allowlist all attributes of AMP elements, but make sure we don't + // remove default-supported attributes from the allowlist afterwards. + expect( + purify( + '

' + ) + ).to.equal( + '

' + ); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + }); - it('should support list tags', () => { - const html = '
    '; - expect(purifyTripleMustache(html)).to.be.equal(html); - }); + it('should allow attributes', () => { + expect(purify('')).to.match( + /<\/amp-lightbox>/ + ); + }); - ['amp', 'amp4email'].forEach((format) => { - describe(`with ${format} format`, () => { - beforeEach(() => { - html.setAttribute(format, ''); - }); - - it('should allowlist formatting related elements', () => { - const nonAllowlistedTag = ''; - const allowlistedFormattingTags = - 'abc
    def
    ' + - '
    ' + - '' + - '' + - '
    '; - const html = `${allowlistedFormattingTags}${nonAllowlistedTag}`; - // Expect the purifier to unescape the allowlisted tags and to - // sanitize and remove the img tag. - expect(purifyTripleMustache(html)).to.be.equal( - allowlistedFormattingTags - ); - }); - - it('should allowlist h1, h2 and h3 elements', () => { - const html = - '

    Heading 1

    ' + - '

    Heading 2

    ' + - '

    Heading 3

    '; - expect(purifyTripleMustache(html)).to.be.equal(html); - }); - - it('should allowlist table related elements and anchor tags', () => { - const html = - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
    caption
    header
    ' + - 'google' + - '
    footer
    '; - expect(purifyTripleMustache(html)).to.be.equal(html); - }); - - it('should allowlist container elements', () => { - const html = - '
    Article
    ' + - '' + - '
    A quote
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    Footer
    ' + - '
    ' + - '
    ' + - '' + - '
    ' +
    -              '
    ' + - ''; - expect(purifyTripleMustache(html)).to.be.equal(html); - }); - }); - }); + it('should output diff marker attributes for some elements', () => { + // Elements with bindings should have [i-amphtml-key=]. + expect(purify('

    ')).to.match( + /

    <\/p>/ + ); + // AMP elements should have [i-amphtml-key=]. + expect(purify('')).to.match( + /<\/amp-pixel>/ + ); + // AMP elements with bindings should have [i-amphtml-key=]. + expect(purify('')).to.match( + /<\/amp-pixel>/ + ); + // amp-img should have [i-amphtml-ignore]. + expect(purify('')).to.equal( + '' + ); + // Other elements should NOT have [i-amphtml-key]. + expect(purify('

    ')).to.equal('

    '); + expect(rewriteAttributeValueSpy.callCount).to.be.equal(2); + }); - it('should allowlist amp-img element', () => { - html.setAttribute('amp', ''); - const markup = ''; - expect(purifyTripleMustache(markup)).to.be.equal(markup); - }); + it('should resolve URLs', () => { + expect(purify('')).to.match(/http/); + expect(purify('')).to.match(/http/); + expect(purify('')).to.match(/http/); + }); + }); - it('should not allowlist amp-img element for AMP4Email', () => { - html.setAttribute('amp4email', ''); - const markup = ''; - expect(purifyTripleMustache(markup)).to.be.empty; - }); + describe('purifyTagsForTripleMustache()', () => { + it('should output basic text', () => { + expect(purifyTripleMustache('abc')).to.be.equal('abc'); + }); - it('should sanitize tags, removing unsafe attributes', () => { - const html = - 'test' + - ''; - expect(purifyTripleMustache(html)).to.be.equal('test'); - }); + it('should output HTML entities', () => { + const entity = '<tag>'; + expect(purifyTripleMustache(entity)).to.be.equal(entity); + // DOMPurify short-circuits when there are no '<' characters. + expect(purifyTripleMustache(`

    ${entity}

    `)).to.be.equal( + `

    ${entity}

    ` + ); + }); + + it('should output valid markup', () => { + expect(purifyTripleMustache('abc')).to.be.equal('abc'); + expect(purifyTripleMustache('ab
    c
    ')).to.be.equal( + 'ab
    c
    ' + ); + expect(purifyTripleMustache('abc')).to.be.equal( + 'abc' + ); + const markupWithClassAttribute = '

    heading

    '; + expect(purifyTripleMustache(markupWithClassAttribute)).to.be.equal( + markupWithClassAttribute + ); + const markupWithClassesAttribute = + '
    heading
    '; + expect(purifyTripleMustache(markupWithClassesAttribute)).to.be.equal( + markupWithClassesAttribute + ); + const markupParagraph = '

    paragraph

    '; + expect(purifyTripleMustache(markupParagraph)).to.be.equal( + markupParagraph + ); + }); + + it('should NOT output non-allowlisted markup', () => { + expect(purifyTripleMustache('ac')).to.be.equal('ac'); + expect(purifyTripleMustache('ac')).to.be.equal('ac'); + }); - describe('should sanitize `style` attribute', () => { - it('should allow valid styles', () => { - expect(purify('
    Test
    ')).to.equal( - '
    Test
    ' + it('should compensate for broken markup', () => { + expect(purifyTripleMustache('ab')).to.be.equal('ab'); + }); + + it('should support list tags', () => { + const html = '
      '; + expect(purifyTripleMustache(html)).to.be.equal(html); + }); + + ['amp', 'amp4email'].forEach((format) => { + describe(`with ${format} format`, () => { + beforeEach(() => { + html.setAttribute(format, ''); + }); + + it('should allowlist formatting related elements', () => { + const nonAllowlistedTag = ''; + const allowlistedFormattingTags = + 'abc
      def
      ' + + '
      ' + + '' + + '' + + '
      '; + const html = `${allowlistedFormattingTags}${nonAllowlistedTag}`; + // Expect the purifier to unescape the allowlisted tags and to + // sanitize and remove the img tag. + expect(purifyTripleMustache(html)).to.be.equal( + allowlistedFormattingTags ); }); - it('should ignore styles containing `!important`', () => { - allowConsoleError(() => { - expect( - purify('
      Test
      ') - ).to.equal('
      Test
      '); - }); + it('should allowlist h1, h2 and h3 elements', () => { + const html = '

      Heading 1

      Heading 2

      Heading 3

      '; + expect(purifyTripleMustache(html)).to.be.equal(html); }); - it('should ignore styles containing `position:fixed`', () => { - allowConsoleError(() => { - expect(purify('
      Test
      ')).to.equal( - '
      Test
      ' - ); - }); + it('should allowlist table related elements and anchor tags', () => { + const html = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
      caption
      header
      ' + + 'google' + + '
      footer
      '; + expect(purifyTripleMustache(html)).to.be.equal(html); }); - it('should ignore styles containing `position:sticky`', () => { - allowConsoleError(() => { - expect(purify('
      Test
      ')).to.equal( - '
      Test
      ' - ); - }); + it('should allowlist container elements', () => { + const html = + '
      Article
      ' + + '' + + '
      A quote
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      Footer
      ' + + '
      ' + + '
      ' + + '' + + '
      ' +
      +            '
      ' + + ''; + expect(purifyTripleMustache(html)).to.be.equal(html); }); }); }); - describe('')).to.equal(''); - }); + it('should allowlist amp-img element', () => { + html.setAttribute('amp', ''); + const markup = ''; + expect(purifyTripleMustache(markup)).to.be.equal(markup); + }); - it('should not allow script[type="text/javascript"]', () => { - expect( - purify('') - ).to.equal(''); - }); + it('should not allowlist amp-img element for AMP4Email', () => { + html.setAttribute('amp4email', ''); + const markup = ''; + expect(purifyTripleMustache(markup)).to.be.empty; + }); - it('should not allow script[type="application/javascript"]', () => { - const html = ''; - expect(purify(html)).to.equal(''); + it('should sanitize tags, removing unsafe attributes', () => { + const html = + 'test' + + ''; + expect(purifyTripleMustache(html)).to.be.equal('test'); + }); + + describe('should sanitize `style` attribute', () => { + it('should allow valid styles', () => { + expect(purify('
      Test
      ')).to.equal( + '
      Test
      ' + ); }); - it('should allow script[type="application/json"]', () => { - const html = ''; - expect(purify(html)).to.equal(html); + it('should ignore styles containing `!important`', () => { + allowConsoleError(() => { + expect( + purify('
      Test
      ') + ).to.equal('
      Test
      '); + }); }); - it('should allow script[type="application/ld+json"]', () => { - const html = ''; - expect(purify(html)).to.equal(html); + it('should ignore styles containing `position:fixed`', () => { + allowConsoleError(() => { + expect(purify('
      Test
      ')).to.equal( + '
      Test
      ' + ); + }); }); - it('should not allow insecure '; - // Should not allow an insecure tag following a secure one. - expect(purify(html + '')).to.equal(html); - // Should not allow an insecure tag preceding a secure one. - expect(purify('' + html)).to.equal(html); - // Should not allow an insecure tag containing a secure one. - expect( - purify( - '' - ) - ).to.equal(''); + it('should ignore styles containing `position:sticky`', () => { + allowConsoleError(() => { + expect(purify('
      Test
      ')).to.equal( + '
      Test
      ' + ); + }); }); }); + }); - describe('for ', () => { - it('should rewrite [text] and [class] attributes', () => { - expect(purify('

      ')).to.match( - /

      <\/p>/ - ); - expect(purify('

      ')).to.match( - /

      <\/p>/ - ); - }); + describe('')).to.equal(''); + }); - it('should add "i-amphtml-binding" for data-amp-bind-*', () => { - expect(purify('

      ')).to.match( - /

      <\/p>/ - ); - }); + it('should not allow script[type="text/javascript"]', () => { + expect( + purify('') + ).to.equal(''); + }); - it('should NOT rewrite values of binding attributes', () => { - // Should not change "foo.bar". - expect(purify('link')).to.match( - /link<\/a>/ - ); - }); + it('should not allow script[type="application/javascript"]', () => { + const html = ''; + expect(purify(html)).to.equal(''); }); - describe('structured data', () => { - it('[itemprop] global attribute', () => { - const h1 = '

      h1

      '; - expect(purify(h1)).to.equal(h1); + it('should allow script[type="application/json"]', () => { + const html = ''; + expect(purify(html)).to.equal(html); + }); - const span = 'span'; - expect(purify(span)).to.equal(span); + it('should allow script[type="application/ld+json"]', () => { + const html = ''; + expect(purify(html)).to.equal(html); + }); - const a = 'a'; - expect(purify(a)).to.equal(a); - }); + it('should not allow insecure '; + // Should not allow an insecure tag following a secure one. + expect(purify(html + '')).to.equal(html); + // Should not allow an insecure tag preceding a secure one. + expect(purify('' + html)).to.equal(html); + // Should not allow an insecure tag containing a secure one. + expect( + purify( + '' + ) + ).to.equal(''); }); + }); - // Select SVG XSS tests from https://html5sec.org/#svg. - describe('SVG', () => { - it('should prevent XSS via tag and onload attribute', () => { - const svg = - '' + - ''; - expect(purify(svg)).to.equal( - '' - ); - }); + describe('for ', () => { + it('should rewrite [text] and [class] attributes', () => { + expect(purify('

      ')).to.match( + /

      <\/p>/ + ); + expect(purify('

      ')).to.match( + /

      <\/p>/ + ); + }); - it('should prevent XSS via '; - expect(purify(svg)).to.equal( - '' - ); - }); + it('should add "i-amphtml-binding" for data-amp-bind-*', () => { + expect(purify('

      ')).to.match( + /

      <\/p>/ + ); + }); - it( - 'should prevent automatic execution of onload attribute without other ' + - 'SVG elements', - () => { - const svg = - ''; - expect(purify(svg)).to.equal( - '' - ); - } + it('should NOT rewrite values of binding attributes', () => { + // Should not change "foo.bar". + expect(purify('link')).to.match( + /link<\/a>/ ); + }); + }); - it('should prevent simple passive XSS via XLink', () => { - const svg = - '' + - '' + - ''; - expect(purify(svg)).to.equal( - '' + - '' + - '' - ); - }); + describe('structured data', () => { + it('[itemprop] global attribute', () => { + const h1 = '

      h1

      '; + expect(purify(h1)).to.equal(h1); + + const span = 'span'; + expect(purify(span)).to.equal(span); - it('should prevent XSS via "from" attribute in SVG and inline-SVG', () => { + const a = 'a'; + expect(purify(a)).to.equal(a); + }); + }); + + // Select SVG XSS tests from https://html5sec.org/#svg. + describe('SVG', () => { + it('should prevent XSS via tag and onload attribute', () => { + const svg = + '' + + ''; + expect(purify(svg)).to.equal( + '' + ); + }); + + it('should prevent XSS via '; + expect(purify(svg)).to.equal( + '' + ); + }); + + it( + 'should prevent automatic execution of onload attribute without other ' + + 'SVG elements', + () => { const svg = - '' + - '' + - '' + - '' + - ''; + ''; expect(purify(svg)).to.equal( - '' + - '' + '' ); - }); + } + ); + + it('should prevent simple passive XSS via XLink', () => { + const svg = + '' + + '' + + ''; + expect(purify(svg)).to.equal( + '' + + '' + + '' + ); + }); - it('should output only if href is relative', () => { - allowConsoleError(() => { - const href = - ''; - expect(purify(href)).to.equal(href); + it('should prevent XSS via "from" attribute in SVG and inline-SVG', () => { + const svg = + '' + + '' + + '' + + '' + + ''; + expect(purify(svg)).to.equal( + '' + + '' + ); + }); - const xlink = - ''; - expect(purify(xlink)).to.equal(xlink); + it('should output only if href is relative', () => { + allowConsoleError(() => { + const href = + ''; + expect(purify(href)).to.equal(href); - expect( - purify( - '' - ) - ).to.equal(''); - expect( - purify( - '' - ) - ).to.equal(''); - }); + const xlink = + ''; + expect(purify(xlink)).to.equal(xlink); + + expect( + purify( + '' + ) + ).to.equal(''); + expect( + purify( + '' + ) + ).to.equal(''); }); }); }); +}); -describes.sandboxed - .configure() - .skipFirefox() - .run('DOMPurify-based, custom html', {}, () => { - let html; - let purify; - - before(() => { - html = document.createElement('html'); - const doc = { - documentElement: html, - createElement: (tagName) => document.createElement(tagName), - }; - - const purifier = () => new Purifier(doc); - - /** - * Helper that serializes output of purifyHtml() to string. - * @param {string} html - * @return {string} - */ - purify = (html) => purifier().purifyHtml(html).innerHTML; - }); - - describe('AMP formats', () => { - it('should denylist input[type="image"] and input[type="button"] in AMP', () => { - // Given the AMP format type. - html.setAttribute('amp', ''); - allowConsoleError(() => { - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - }); - }); +describes.sandboxed('DOMPurify-based, custom html', {}, () => { + let html; + let purify; + + before(() => { + html = document.createElement('html'); + const doc = { + documentElement: html, + createElement: (tagName) => document.createElement(tagName), + }; + + const purifier = () => new Purifier(doc); + + /** + * Helper that serializes output of purifyHtml() to string. + * @param {string} html + * @return {string} + */ + purify = (html) => purifier().purifyHtml(html).innerHTML; + }); - it('should allow input[type="file"] and input[type="password"]', () => { - // Given that the AMP format does not denylist input types file and - // password. - html.setAttribute('amp', ''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal( - '' - ); + describe('AMP formats', () => { + it('should denylist input[type="image"] and input[type="button"] in AMP', () => { + // Given the AMP format type. + html.setAttribute('amp', ''); + allowConsoleError(() => { + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); }); + }); - it('should sanitize certain tag attributes for AMP4Email', () => { - html.setAttribute('amp4email', ''); - allowConsoleError(() => { - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('
      ')).to.equal( - '
      ' - ); - expect(purify('')).to.match( - /<\/amp-anim>/ - ); - }); - }); + it('should allow input[type="file"] and input[type="password"]', () => { + // Given that the AMP format does not denylist input types file and + // password. + html.setAttribute('amp', ''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal( + '' + ); + }); - it('should only allow allowlisted AMP elements in AMP4EMAIL', () => { - html.setAttribute('amp4email', ''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - expect(purify('')).to.equal(''); - - expect(purify('')).to.equal( - '' - ); - expect(purify('')).to.match( - /<\/amp-accordion>/ + it('should sanitize certain tag attributes for AMP4Email', () => { + html.setAttribute('amp4email', ''); + allowConsoleError(() => { + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('
      ')).to.equal( + '
      ' ); - expect(purify('')).to.match( + expect(purify('')).to.match( /<\/amp-anim>/ ); - expect(purify('')).to.match( - /<\/amp-bind-macro>/ - ); - expect(purify('')).to.match( - /<\/amp-carousel>/ - ); - expect(purify('')).to.match( - /<\/amp-fit-text>/ - ); - expect(purify('')).to.match( - /<\/amp-layout>/ - ); - expect(purify('')).to.match( - /<\/amp-selector>/ - ); - expect(purify('')).to.match( - /<\/amp-sidebar>/ - ); - expect(purify('')).to.match( - /<\/amp-timeago>/ - ); }); }); + + it('should only allow allowlisted AMP elements in AMP4EMAIL', () => { + html.setAttribute('amp4email', ''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + expect(purify('')).to.equal(''); + + expect(purify('')).to.equal( + '' + ); + expect(purify('')).to.match( + /<\/amp-accordion>/ + ); + expect(purify('')).to.match( + /<\/amp-anim>/ + ); + expect(purify('')).to.match( + /<\/amp-bind-macro>/ + ); + expect(purify('')).to.match( + /<\/amp-carousel>/ + ); + expect(purify('')).to.match( + /<\/amp-fit-text>/ + ); + expect(purify('')).to.match( + /<\/amp-layout>/ + ); + expect(purify('')).to.match( + /<\/amp-selector>/ + ); + expect(purify('')).to.match( + /<\/amp-sidebar>/ + ); + expect(purify('')).to.match( + /<\/amp-timeago>/ + ); + }); }); +}); describes.sandboxed('validateAttributeChange', {}, () => { let purifier; diff --git a/test/unit/test-resources.js b/test/unit/test-resources.js index b1cc47208adf..ef16711a00ba 100644 --- a/test/unit/test-resources.js +++ b/test/unit/test-resources.js @@ -1053,10 +1053,7 @@ describes.realWin('Resources discoverWork', {amp: true}, (env) => { expect(layoutCanceledSpy).to.be.calledOnce; }); - // TODO (#16156): this test results in too many calls to getRect on Safari - // TODO(jridgewell, #21546): fix this flaky test. Originally configured with: - // it.configure().skipSafari().run( - it.skip('should update inViewport before scheduling layouts', () => { + it('should update inViewport before scheduling layouts', () => { resources.visible_ = true; sandbox .stub(resources.ampdoc, 'getVisibilityState') @@ -1448,61 +1445,53 @@ describes.fakeWin('Resources.add/upgrade/remove', {amp: true}, (env) => { }); }); - // TODO(jridgewell, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should not schedule pass when immediate build fails', - () => { - const schedulePassStub = sandbox.stub(resources, 'schedulePass'); - child1.isBuilt = () => false; - const child1BuildSpy = sandbox.spy(); - child1.buildInternal = () => { - // Emulate an error happening during an element build. - child1BuildSpy(); - return Promise.reject(new Error('child1-build-error')); - }; - resources.documentReady_ = true; - resources.add(child1); - const resource1 = stubBuild(Resource.forElementOptional(child1)); - resources.upgraded(child1); - expect(resources.get()).to.contain(resource1); - return resource1.buildPromise.then( - () => { - throw new Error('must have failed'); - }, - () => { - expect(child1BuildSpy).to.be.calledOnce; - expect(schedulePassStub).to.not.be.called; - expect(resources.get()).to.not.contain(resource1); - } - ); - } - ); + it('should not schedule pass when immediate build fails', () => { + const schedulePassStub = sandbox.stub(resources, 'schedulePass'); + child1.isBuilt = () => false; + const child1BuildSpy = sandbox.spy(); + child1.buildInternal = () => { + // Emulate an error happening during an element build. + child1BuildSpy(); + return Promise.reject(new Error('child1-build-error')); + }; + resources.documentReady_ = true; + resources.add(child1); + const resource1 = stubBuild(Resource.forElementOptional(child1)); + resources.upgraded(child1); + expect(resources.get()).to.contain(resource1); + return resource1.buildPromise.then( + () => { + throw new Error('must have failed'); + }, + () => { + expect(child1BuildSpy).to.be.calledOnce; + expect(schedulePassStub).to.not.be.called; + expect(resources.get()).to.not.contain(resource1); + } + ); + }); - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should add element to pending build when document is not ready', - () => { - child1.isBuilt = () => false; - child2.isBuilt = () => false; - resources.buildReadyResources_ = sandbox.spy(); - resources.documentReady_ = false; - resources.add(child1); - resources.upgraded(child1); - expect(child1.buildInternal.called).to.be.false; - expect(resources.pendingBuildResources_.length).to.be.equal(1); - resources.add(child2); - resources.upgraded(child2); - expect(child2.buildInternal.called).to.be.false; - expect(resources.pendingBuildResources_.length).to.be.equal(2); - expect(resources.buildReadyResources_.calledTwice).to.be.true; - const resource1 = Resource.forElementOptional(child1); - const resource2 = Resource.forElementOptional(child2); - expect(resources.get()).to.contain(resource1); - expect(resources.get()).to.contain(resource2); - expect(resource1.isBuilding()).to.be.false; - expect(resource2.isBuilding()).to.be.false; - } - ); + it('should add element to pending build when document is not ready', () => { + child1.isBuilt = () => false; + child2.isBuilt = () => false; + resources.buildReadyResources_ = sandbox.spy(); + resources.documentReady_ = false; + resources.add(child1); + resources.upgraded(child1); + expect(child1.buildInternal.called).to.be.false; + expect(resources.pendingBuildResources_.length).to.be.equal(1); + resources.add(child2); + resources.upgraded(child2); + expect(child2.buildInternal.called).to.be.false; + expect(resources.pendingBuildResources_.length).to.be.equal(2); + expect(resources.buildReadyResources_.calledTwice).to.be.true; + const resource1 = Resource.forElementOptional(child1); + const resource2 = Resource.forElementOptional(child2); + expect(resources.get()).to.contain(resource1); + expect(resources.get()).to.contain(resource2); + expect(resource1.isBuilding()).to.be.false; + expect(resource2.isBuilding()).to.be.false; + }); describe('buildReadyResources_', () => { let schedulePassStub; @@ -1517,47 +1506,43 @@ describes.fakeWin('Resources.add/upgrade/remove', {amp: true}, (env) => { resources.resources_ = [resource1, resource2]; }); - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should build ready resources and remove them from pending', - () => { - resources.pendingBuildResources_ = [resource1, resource2]; - resources.buildReadyResources_(); - expect(child1.buildInternal.called).to.be.false; - expect(child2.buildInternal.called).to.be.false; - expect(resources.pendingBuildResources_.length).to.be.equal(2); - expect(resources.schedulePass.called).to.be.false; + it('should build ready resources and remove them from pending', () => { + resources.pendingBuildResources_ = [resource1, resource2]; + resources.buildReadyResources_(); + expect(child1.buildInternal.called).to.be.false; + expect(child2.buildInternal.called).to.be.false; + expect(resources.pendingBuildResources_.length).to.be.equal(2); + expect(resources.schedulePass.called).to.be.false; - child1.nextSibling = child2; - resources.buildReadyResources_(); - expect(child1.buildInternal.called).to.be.true; - expect(child2.buildInternal.called).to.be.false; - expect(resources.pendingBuildResources_.length).to.be.equal(1); - expect(resources.pendingBuildResources_[0]).to.be.equal(resource2); - expect(resource1.isBuilding()).to.be.true; - expect(resource2.isBuilding()).to.be.false; - return resource1.buildPromise - .then(() => { - expect(resources.schedulePass.calledOnce).to.be.true; - - child2.parentNode = parent; - parent.nextSibling = true; - resources.buildReadyResources_(); - expect(child1.buildInternal).to.be.calledOnce; - expect(child2.buildInternal.called).to.be.true; - expect(resources.pendingBuildResources_.length).to.be.equal(0); - expect(resource2.isBuilding()).to.be.true; - return resource2.buildPromise; - }) - .then(() => { - expect(resources.get()).to.contain(resource1); - expect(resources.get()).to.contain(resource2); - expect(resource1.isBuilding()).to.be.false; - expect(resource2.isBuilding()).to.be.false; - expect(resources.schedulePass.calledTwice).to.be.true; - }); - } - ); + child1.nextSibling = child2; + resources.buildReadyResources_(); + expect(child1.buildInternal.called).to.be.true; + expect(child2.buildInternal.called).to.be.false; + expect(resources.pendingBuildResources_.length).to.be.equal(1); + expect(resources.pendingBuildResources_[0]).to.be.equal(resource2); + expect(resource1.isBuilding()).to.be.true; + expect(resource2.isBuilding()).to.be.false; + return resource1.buildPromise + .then(() => { + expect(resources.schedulePass.calledOnce).to.be.true; + + child2.parentNode = parent; + parent.nextSibling = true; + resources.buildReadyResources_(); + expect(child1.buildInternal).to.be.calledOnce; + expect(child2.buildInternal.called).to.be.true; + expect(resources.pendingBuildResources_.length).to.be.equal(0); + expect(resource2.isBuilding()).to.be.true; + return resource2.buildPromise; + }) + .then(() => { + expect(resources.get()).to.contain(resource1); + expect(resources.get()).to.contain(resource2); + expect(resource1.isBuilding()).to.be.false; + expect(resource2.isBuilding()).to.be.false; + expect(resources.schedulePass.calledTwice).to.be.true; + }); + }); it('should NOT build past the root node when pending', () => { resources.pendingBuildResources_ = [resource1]; @@ -1612,82 +1597,69 @@ describes.fakeWin('Resources.add/upgrade/remove', {amp: true}, (env) => { expect(resources.pendingBuildResources_.length).to.be.equal(0); }); - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should build everything pending when document is ready', - () => { - resources.documentReady_ = true; - resources.pendingBuildResources_ = [ - parentResource, - resource1, - resource2, - ]; - const child1BuildSpy = sandbox.spy(); - child1.buildInternal = () => { - // Emulate an error happening during an element build. - child1BuildSpy(); - return Promise.reject(new Error('child1-build-error')); - }; - resources.buildReadyResources_(); - expect(child1BuildSpy.called).to.be.true; - expect(child2.buildInternal.called).to.be.true; - expect(parent.buildInternal.called).to.be.true; - expect(resources.pendingBuildResources_.length).to.be.equal(0); - return Promise.all([ - parentResource.buildPromise, - resource2.buildPromise, - resource1.buildPromise.then( - () => { - throw new Error('must have failed'); - }, - () => { - // Ignore error. - } - ), - ]).then(() => { - expect(schedulePassStub).to.be.calledTwice; - // Failed build. - expect(resources.get()).to.not.contain(resource1); - expect(resource1.isBuilding()).to.be.false; - // Successful build. - expect(resources.get()).to.contain(resource2); - expect(resource2.isBuilding()).to.be.false; - }); - } - ); - - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari( - 'should not schedule pass if all builds failed', - () => { - resources.documentReady_ = true; - resources.pendingBuildResources_ = [resource1]; - const child1BuildSpy = sandbox.spy(); - child1.buildInternal = () => { - // Emulate an error happening during an element build. - child1BuildSpy(); - return Promise.reject(new Error('child1-build-error')); - }; - resources.buildReadyResources_(); - expect(child1BuildSpy.called).to.be.true; - expect(resources.pendingBuildResources_.length).to.be.equal(0); - return resource1.buildPromise.then( + it('should build everything pending when document is ready', () => { + resources.documentReady_ = true; + resources.pendingBuildResources_ = [parentResource, resource1, resource2]; + const child1BuildSpy = sandbox.spy(); + child1.buildInternal = () => { + // Emulate an error happening during an element build. + child1BuildSpy(); + return Promise.reject(new Error('child1-build-error')); + }; + resources.buildReadyResources_(); + expect(child1BuildSpy.called).to.be.true; + expect(child2.buildInternal.called).to.be.true; + expect(parent.buildInternal.called).to.be.true; + expect(resources.pendingBuildResources_.length).to.be.equal(0); + return Promise.all([ + parentResource.buildPromise, + resource2.buildPromise, + resource1.buildPromise.then( () => { throw new Error('must have failed'); }, () => { - expect(schedulePassStub).to.not.be.called; - expect(resources.get()).to.not.contain(resource1); - expect(resource1.isBuilding()).to.be.false; + // Ignore error. } - ); - } - ); + ), + ]).then(() => { + expect(schedulePassStub).to.be.calledTwice; + // Failed build. + expect(resources.get()).to.not.contain(resource1); + expect(resource1.isBuilding()).to.be.false; + // Successful build. + expect(resources.get()).to.contain(resource2); + expect(resource2.isBuilding()).to.be.false; + }); + }); + + it('should not schedule pass if all builds failed', () => { + resources.documentReady_ = true; + resources.pendingBuildResources_ = [resource1]; + const child1BuildSpy = sandbox.spy(); + child1.buildInternal = () => { + // Emulate an error happening during an element build. + child1BuildSpy(); + return Promise.reject(new Error('child1-build-error')); + }; + resources.buildReadyResources_(); + expect(child1BuildSpy.called).to.be.true; + expect(resources.pendingBuildResources_.length).to.be.equal(0); + return resource1.buildPromise.then( + () => { + throw new Error('must have failed'); + }, + () => { + expect(schedulePassStub).to.not.be.called; + expect(resources.get()).to.not.contain(resource1); + expect(resource1.isBuilding()).to.be.false; + } + ); + }); }); describe('remove', () => { - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari('should remove resource and pause', () => { + it('should remove resource and pause', () => { child1.isBuilt = () => true; resources.add(child1); const resource = child1['__AMP__RESOURCE']; @@ -1736,8 +1708,7 @@ describes.fakeWin('Resources.add/upgrade/remove', {amp: true}, (env) => { resources.remove(child1); }); - // TODO(amphtml, #15748): Fails on Safari 11.1.0. - it.configure().skipSafari('should keep reference to the resource', () => { + it('should keep reference to the resource', () => { expect(resource).to.not.be.null; expect(Resource.forElementOptional(child1)).to.equal(resource); expect(resources.get()).to.not.contain(resource); diff --git a/test/unit/test-runtime.js b/test/unit/test-runtime.js index ee2633b385ed..4cab5719eed2 100644 --- a/test/unit/test-runtime.js +++ b/test/unit/test-runtime.js @@ -1868,64 +1868,52 @@ describes.realWin( }; } - it.configure() - .skipFirefox() - .run('should broadcast to all but sender', () => { - doc1.viewer.broadcast({test: 1}); - return doc1.viewer - .sendMessageAwaitResponse('ignore', {}) - .then(() => { - // Sender is not called. - expect(doc1.broadcastReceived).to.not.be.called; - - // All others are called. - expect(doc2.broadcastReceived).to.be.calledOnce; - expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); - expect(doc3.broadcastReceived).to.be.calledOnce; - expect(doc3.broadcastReceived.args[0][0]).deep.equal({test: 1}); - - // None of the onMessage are called. - expect(doc1.onMessage).to.not.be.called; - expect(doc2.onMessage).to.not.be.called; - expect(doc3.onMessage).to.not.be.called; - }); + it('should broadcast to all but sender', () => { + doc1.viewer.broadcast({test: 1}); + return doc1.viewer.sendMessageAwaitResponse('ignore', {}).then(() => { + // Sender is not called. + expect(doc1.broadcastReceived).to.not.be.called; + + // All others are called. + expect(doc2.broadcastReceived).to.be.calledOnce; + expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); + expect(doc3.broadcastReceived).to.be.calledOnce; + expect(doc3.broadcastReceived.args[0][0]).deep.equal({test: 1}); + + // None of the onMessage are called. + expect(doc1.onMessage).to.not.be.called; + expect(doc2.onMessage).to.not.be.called; + expect(doc3.onMessage).to.not.be.called; }); + }); - it.configure() - .skipFirefox() - .run('should stop broadcasting after close', () => { - doc3.amp.close(); - doc1.viewer.broadcast({test: 1}); - return doc1.viewer - .sendMessageAwaitResponse('ignore', {}) - .then(() => { - // Sender is not called, closed is not called. - expect(doc1.broadcastReceived).to.not.be.called; - expect(doc3.broadcastReceived).to.not.be.called; - - // All others are called. - expect(doc2.broadcastReceived).to.be.calledOnce; - expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); - }); + it('should stop broadcasting after close', () => { + doc3.amp.close(); + doc1.viewer.broadcast({test: 1}); + return doc1.viewer.sendMessageAwaitResponse('ignore', {}).then(() => { + // Sender is not called, closed is not called. + expect(doc1.broadcastReceived).to.not.be.called; + expect(doc3.broadcastReceived).to.not.be.called; + + // All others are called. + expect(doc2.broadcastReceived).to.be.calledOnce; + expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); }); + }); - it.configure() - .skipFirefox() - .run('should stop broadcasting after force-close', () => { - doc3.hostElement.parentNode.removeChild(doc3.hostElement); - doc1.viewer.broadcast({test: 1}); - return doc1.viewer - .sendMessageAwaitResponse('ignore', {}) - .then(() => { - // Sender is not called, closed is not called. - expect(doc1.broadcastReceived).to.not.be.called; - expect(doc3.broadcastReceived).to.not.be.called; - - // All others are called. - expect(doc2.broadcastReceived).to.be.calledOnce; - expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); - }); + it('should stop broadcasting after force-close', () => { + doc3.hostElement.parentNode.removeChild(doc3.hostElement); + doc1.viewer.broadcast({test: 1}); + return doc1.viewer.sendMessageAwaitResponse('ignore', {}).then(() => { + // Sender is not called, closed is not called. + expect(doc1.broadcastReceived).to.not.be.called; + expect(doc3.broadcastReceived).to.not.be.called; + + // All others are called. + expect(doc2.broadcastReceived).to.be.calledOnce; + expect(doc2.broadcastReceived.args[0][0]).deep.equal({test: 1}); }); + }); it('should send message', () => { doc1.onMessage.returns(Promise.resolve()); diff --git a/test/unit/test-storage.js b/test/unit/test-storage.js index 7a4888afeb05..41bfe1494d94 100644 --- a/test/unit/test-storage.js +++ b/test/unit/test-storage.js @@ -21,376 +21,372 @@ describes.sandboxed('Storage', {}, (env) => { let viewerBroadcastHandler; let clock; - // TODO(amphtml, #25621): Cannot find atob / btoa on Safari. - describe - .configure() - .skipSafari() - .run('Storage', () => { - beforeEach(() => { - viewerBroadcastHandler = undefined; - viewer = { - onBroadcast: (handler) => { - viewerBroadcastHandler = handler; - }, - broadcast: () => {}, - }; - viewerMock = env.sandbox.mock(viewer); - - windowApi = { - document: {}, - location: 'https://acme.com/document1', - performance: new FakePerformance(window), - }; - ampdoc = new AmpDocSingle(windowApi); - - binding = { - loadBlob: () => {}, - saveBlob: () => {}, - }; - bindingMock = env.sandbox.mock(binding); - - storage = new Storage(ampdoc, viewer, binding); - storage.start_(); - }); + describe('Storage', () => { + beforeEach(() => { + viewerBroadcastHandler = undefined; + viewer = { + onBroadcast: (handler) => { + viewerBroadcastHandler = handler; + }, + broadcast: () => {}, + }; + viewerMock = env.sandbox.mock(viewer); + + windowApi = { + document: {}, + location: 'https://acme.com/document1', + performance: new FakePerformance(window), + }; + ampdoc = new AmpDocSingle(windowApi); + + binding = { + loadBlob: () => {}, + saveBlob: () => {}, + }; + bindingMock = env.sandbox.mock(binding); + + storage = new Storage(ampdoc, viewer, binding); + storage.start_(); + }); - function expectStorage(keyValues) { - const list = []; - for (const k in keyValues) { - list.push( - storage.get(k).then((value) => { - const expectedValue = keyValues[k]; - expect(value).to.equal(expectedValue, `For "${k}"`); - }) - ); - } - return Promise.all(list); + function expectStorage(keyValues) { + const list = []; + for (const k in keyValues) { + list.push( + storage.get(k).then((value) => { + const expectedValue = keyValues[k]; + expect(value).to.equal(expectedValue, `For "${k}"`); + }) + ); } + return Promise.all(list); + } + + it('should configure store correctly', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + store1.set('key2', 'value2'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + return storage + .get('key1') + .then(() => { + return storage.storePromise_; + }) + .then((store) => { + expect(store.maxValues_).to.equal(8); + }); + }); - it('should configure store correctly', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - store1.set('key2', 'value2'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - return storage - .get('key1') - .then(() => { - return storage.storePromise_; - }) - .then((store) => { - expect(store.maxValues_).to.equal(8); - }); + it('should initialize empty store with prototype-less objects', () => { + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(null)) + .once(); + return storage + .get('key1') + .then(() => { + return storage.storePromise_; + }) + .then((store) => { + expect(store.obj.__proto__).to.be.undefined; + expect(store.values_.__proto__).to.be.undefined; + }); + }); + + it('should restore store with prototype-less objects', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + store1.set('key2', 'value2'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + return storage + .get('key1') + .then(() => { + return storage.storePromise_; + }) + .then((store) => { + expect(store.obj.__proto__).to.be.undefined; + expect(store.values_.__proto__).to.be.undefined; + }); + }); + + it('should get the value first time and reuse store', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + store1.set('key2', 'value2'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.get('key1'); + return promise.then((value) => { + expect(value).to.equal('value1'); + const store1Promise = storage.storePromise_; + expect(store1Promise).to.exist; + + // Repeat. + return storage.get('key2').then((value2) => { + expect(value2).to.equal('value2'); + expect(storage.storePromise_).to.equal(store1Promise); + }); }); + }); - it('should initialize empty store with prototype-less objects', () => { - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(null)) - .once(); - return storage - .get('key1') - .then(() => { - return storage.storePromise_; - }) - .then((store) => { - expect(store.obj.__proto__).to.be.undefined; - expect(store.values_.__proto__).to.be.undefined; - }); + it('should get the value from first ever request and reuse store', () => { + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(null)) + .once(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.get('key1'); + return promise.then((value) => { + expect(value).to.be.undefined; + const store1Promise = storage.storePromise_; + expect(store1Promise).to.exist; + + // Repeat. + return storage.get('key2').then((value2) => { + expect(value2).to.be.undefined; + expect(storage.storePromise_).to.equal(store1Promise); + }); }); + }); - it('should restore store with prototype-less objects', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - store1.set('key2', 'value2'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - return storage - .get('key1') - .then(() => { - return storage.storePromise_; - }) - .then((store) => { - expect(store.obj.__proto__).to.be.undefined; - expect(store.values_.__proto__).to.be.undefined; - }); + it('should recover from binding failure', () => { + expectAsyncConsoleError(/Failed to load store/); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.reject('intentional')) + .once(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.get('key1'); + return promise.then((value) => { + expect(value).to.be.undefined; + expect(storage.storePromise_).to.exist; + }); + }); + + it('should recover from binding error', () => { + expectAsyncConsoleError(/Failed to load store/); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve('UNKNOWN FORMAT')) + .once(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.get('key1'); + return promise.then((value) => { + expect(value).to.be.undefined; + expect(storage.storePromise_).to.exist; }); + }); - it('should get the value first time and reuse store', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - store1.set('key2', 'value2'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.get('key1'); - return promise.then((value) => { - expect(value).to.equal('value1'); + it('should save the value first time and reuse store', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + store1.set('key2', 'value2'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + bindingMock + .expects('saveBlob') + .withExactArgs( + 'https://acme.com', + env.sandbox.match((arg) => { + const store2 = new Store(JSON.parse(atob(arg))); + return ( + store2.get('key1') !== undefined && + store2.get('key2') !== undefined + ); + }) + ) + .returns(Promise.resolve()) + .twice(); + viewerMock + .expects('broadcast') + .withExactArgs( + env.sandbox.match((arg) => { + return ( + arg['type'] == 'amp-storage-reset' && + arg['origin'] == 'https://acme.com' + ); + }) + ) + .twice(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.set('key1', true); + return promise + .then(() => { const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. - return storage.get('key2').then((value2) => { - expect(value2).to.equal('value2'); + return storage.set('key2', true).then(() => { expect(storage.storePromise_).to.equal(store1Promise); }); + }) + .then(() => { + return expectStorage({ + 'key1': true, + 'key2': true, + }); }); - }); + }); - it('should get the value from first ever request and reuse store', () => { - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(null)) - .once(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.get('key1'); - return promise.then((value) => { - expect(value).to.be.undefined; + it('should remove the key first time and reuse store', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + store1.set('key2', 'value2'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + bindingMock + .expects('saveBlob') + .withExactArgs( + 'https://acme.com', + env.sandbox.match((arg) => { + const store2 = new Store(JSON.parse(atob(arg))); + return store2.get('key1') === undefined; + }) + ) + .returns(Promise.resolve()) + .twice(); + viewerMock + .expects('broadcast') + .withExactArgs( + env.sandbox.match((arg) => { + return ( + arg['type'] == 'amp-storage-reset' && + arg['origin'] == 'https://acme.com' + ); + }) + ) + .twice(); + expect(storage.storePromise_).to.not.exist; + const promise = storage.remove('key1'); + return promise + .then(() => { const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. - return storage.get('key2').then((value2) => { - expect(value2).to.be.undefined; + return storage.remove('key2').then(() => { expect(storage.storePromise_).to.equal(store1Promise); }); + }) + .then(() => { + return expectStorage({ + 'key1': undefined, + 'key2': undefined, + }); }); - }); - - it('should recover from binding failure', () => { - expectAsyncConsoleError(/Failed to load store/); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.reject('intentional')) - .once(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.get('key1'); - return promise.then((value) => { - expect(value).to.be.undefined; - expect(storage.storePromise_).to.exist; - }); - }); + }); - it('should recover from binding error', () => { - expectAsyncConsoleError(/Failed to load store/); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve('UNKNOWN FORMAT')) - .once(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.get('key1'); - return promise.then((value) => { - expect(value).to.be.undefined; - expect(storage.storePromise_).to.exist; - }); + it('should get unexpired value based on duration', async () => { + clock = env.sandbox.useFakeTimers(); + const store1 = new Store({}); + store1.set('key1', 'value1'); + expect(store1.values_).to.deep.equal({ + 'key1': {v: 'value1', t: 0}, }); - it('should save the value first time and reuse store', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - store1.set('key2', 'value2'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - bindingMock - .expects('saveBlob') - .withExactArgs( - 'https://acme.com', - env.sandbox.match((arg) => { - const store2 = new Store(JSON.parse(atob(arg))); - return ( - store2.get('key1') !== undefined && - store2.get('key2') !== undefined - ); - }) - ) - .returns(Promise.resolve()) - .twice(); - viewerMock - .expects('broadcast') - .withExactArgs( - env.sandbox.match((arg) => { - return ( - arg['type'] == 'amp-storage-reset' && - arg['origin'] == 'https://acme.com' - ); - }) - ) - .twice(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.set('key1', true); - return promise - .then(() => { - const store1Promise = storage.storePromise_; - expect(store1Promise).to.exist; - - // Repeat. - return storage.set('key2', true).then(() => { - expect(storage.storePromise_).to.equal(store1Promise); - }); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .once(); + bindingMock + .expects('saveBlob') + .withExactArgs( + 'https://acme.com', + env.sandbox.match((arg) => { + const store2 = new Store(JSON.parse(atob(arg))); + return store2.get('key1') === undefined; }) - .then(() => { - return expectStorage({ - 'key1': true, - 'key2': true, - }); - }); - }); - - it('should remove the key first time and reuse store', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - store1.set('key2', 'value2'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - bindingMock - .expects('saveBlob') - .withExactArgs( - 'https://acme.com', - env.sandbox.match((arg) => { - const store2 = new Store(JSON.parse(atob(arg))); - return store2.get('key1') === undefined; - }) - ) - .returns(Promise.resolve()) - .twice(); - viewerMock - .expects('broadcast') - .withExactArgs( - env.sandbox.match((arg) => { - return ( - arg['type'] == 'amp-storage-reset' && - arg['origin'] == 'https://acme.com' - ); - }) - ) - .twice(); - expect(storage.storePromise_).to.not.exist; - const promise = storage.remove('key1'); - return promise - .then(() => { - const store1Promise = storage.storePromise_; - expect(store1Promise).to.exist; - - // Repeat. - return storage.remove('key2').then(() => { - expect(storage.storePromise_).to.equal(store1Promise); - }); + ) + .returns(Promise.resolve()) + .twice(); + viewerMock + .expects('broadcast') + .withExactArgs( + env.sandbox.match((arg) => { + return ( + arg['type'] == 'amp-storage-reset' && + arg['origin'] == 'https://acme.com' + ); }) - .then(() => { - return expectStorage({ - 'key1': undefined, - 'key2': undefined, - }); - }); - }); - - it('should get unexpired value based on duration', async () => { - clock = env.sandbox.useFakeTimers(); - const store1 = new Store({}); - store1.set('key1', 'value1'); - expect(store1.values_).to.deep.equal({ - 'key1': {v: 'value1', t: 0}, - }); + ) + .once(); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .once(); - bindingMock - .expects('saveBlob') - .withExactArgs( - 'https://acme.com', - env.sandbox.match((arg) => { - const store2 = new Store(JSON.parse(atob(arg))); - return store2.get('key1') === undefined; - }) - ) - .returns(Promise.resolve()) - .twice(); - viewerMock - .expects('broadcast') - .withExactArgs( - env.sandbox.match((arg) => { - return ( - arg['type'] == 'amp-storage-reset' && - arg['origin'] == 'https://acme.com' - ); - }) - ) - .once(); - - expect(await storage.get('key1', 10)).to.equal('value1'); - clock.tick(100); - expect(await storage.get('key1', 5)).to.be.undefined; - }); + expect(await storage.get('key1', 10)).to.equal('value1'); + clock.tick(100); + expect(await storage.get('key1', 5)).to.be.undefined; + }); - it('should react to reset messages', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .twice(); + it('should react to reset messages', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .twice(); + return storage.get('key1').then((value) => { + expect(value).to.equal('value1'); + const store1Promise = storage.storePromise_; + expect(store1Promise).to.exist; + + // Issue broadcast event. + viewerBroadcastHandler({ + 'type': 'amp-storage-reset', + 'origin': 'https://acme.com', + }); + expect(storage.storePromise_).to.not.exist; return storage.get('key1').then((value) => { expect(value).to.equal('value1'); - const store1Promise = storage.storePromise_; - expect(store1Promise).to.exist; - - // Issue broadcast event. - viewerBroadcastHandler({ - 'type': 'amp-storage-reset', - 'origin': 'https://acme.com', - }); - expect(storage.storePromise_).to.not.exist; - return storage.get('key1').then((value) => { - expect(value).to.equal('value1'); - expect(storage.storePromise_).to.exist; - }); + expect(storage.storePromise_).to.exist; }); }); + }); - it('should ignore unrelated reset messages', () => { - const store1 = new Store({}); - store1.set('key1', 'value1'); - bindingMock - .expects('loadBlob') - .withExactArgs('https://acme.com') - .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) - .twice(); - return storage.get('key1').then((value) => { - expect(value).to.equal('value1'); - const store1Promise = storage.storePromise_; - expect(store1Promise).to.exist; - - // Issue broadcast event. - viewerBroadcastHandler({ - 'type': 'amp-storage-reset', - 'origin': 'OTHER', - }); - expect(storage.storePromise_).to.exist; + it('should ignore unrelated reset messages', () => { + const store1 = new Store({}); + store1.set('key1', 'value1'); + bindingMock + .expects('loadBlob') + .withExactArgs('https://acme.com') + .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) + .twice(); + return storage.get('key1').then((value) => { + expect(value).to.equal('value1'); + const store1Promise = storage.storePromise_; + expect(store1Promise).to.exist; + + // Issue broadcast event. + viewerBroadcastHandler({ + 'type': 'amp-storage-reset', + 'origin': 'OTHER', }); + expect(storage.storePromise_).to.exist; }); }); + }); }); describes.sandboxed('Store', {}, (env) => { diff --git a/test/unit/test-url-replacements.js b/test/unit/test-url-replacements.js index 8b4306458651..848cde3e9ed3 100644 --- a/test/unit/test-url-replacements.js +++ b/test/unit/test-url-replacements.js @@ -33,232 +33,239 @@ describes.sandboxed('UrlReplacements', {}, (env) => { let userErrorStub; let ampdoc; - // TODO(amphtml, #25621): Cannot find atob / btoa on Safari. - describe - .configure() - .skipSafari() - .run('UrlReplacements', () => { - beforeEach(() => { - canonical = 'https://canonical.com/doc1'; - userErrorStub = env.sandbox.stub(user(), 'error'); - }); + describe('UrlReplacements', () => { + beforeEach(() => { + canonical = 'https://canonical.com/doc1'; + userErrorStub = env.sandbox.stub(user(), 'error'); + }); - function getReplacements(opt_options) { - return createIframePromise().then((iframe) => { - ampdoc = iframe.ampdoc; - iframe.doc.title = 'Pixel Test'; - const link = iframe.doc.createElement('link'); - link.setAttribute('href', 'https://pinterest.com:8080/pin1'); - link.setAttribute('rel', 'canonical'); - iframe.doc.head.appendChild(link); - iframe.win.__AMP_SERVICES.documentInfo = null; - installDocumentInfoServiceForDoc(iframe.ampdoc); - resetScheduledElementForTesting(iframe.win, 'amp-analytics'); - resetScheduledElementForTesting(iframe.win, 'amp-experiment'); - if (opt_options) { - if (opt_options.withCid) { - markElementScheduledForTesting(iframe.win, 'amp-analytics'); - cidServiceForDocForTesting(iframe.ampdoc); - installCryptoService(iframe.win); - } - if (opt_options.withActivity) { - markElementScheduledForTesting(iframe.win, 'amp-analytics'); - installActivityServiceForTesting(iframe.ampdoc); - } - if (opt_options.withVariant) { - markElementScheduledForTesting(iframe.win, 'amp-experiment'); - registerServiceBuilder(iframe.win, 'variant', function () { - return { - getVariants: () => - Promise.resolve({ - 'x1': 'v1', - 'x2': null, - }), - }; - }); - } - if (opt_options.withViewerIntegrationVariableService) { - markElementScheduledForTesting( - iframe.win, - 'amp-viewer-integration' - ); - registerServiceBuilder( - iframe.win, - 'viewer-integration-variable', - function () { - return Promise.resolve( - opt_options.withViewerIntegrationVariableService - ); - } - ); - } - if (opt_options.withOriginalTitle) { - iframe.doc.originalTitle = 'Original Pixel Test'; - } + function getReplacements(opt_options) { + return createIframePromise().then((iframe) => { + ampdoc = iframe.ampdoc; + iframe.doc.title = 'Pixel Test'; + const link = iframe.doc.createElement('link'); + link.setAttribute('href', 'https://pinterest.com:8080/pin1'); + link.setAttribute('rel', 'canonical'); + iframe.doc.head.appendChild(link); + iframe.win.__AMP_SERVICES.documentInfo = null; + installDocumentInfoServiceForDoc(iframe.ampdoc); + resetScheduledElementForTesting(iframe.win, 'amp-analytics'); + resetScheduledElementForTesting(iframe.win, 'amp-experiment'); + if (opt_options) { + if (opt_options.withCid) { + markElementScheduledForTesting(iframe.win, 'amp-analytics'); + cidServiceForDocForTesting(iframe.ampdoc); + installCryptoService(iframe.win); } - viewerService = Services.viewerForDoc(iframe.ampdoc); - replacements = Services.urlReplacementsForDoc( - iframe.doc.documentElement - ); - return replacements; - }); - } - - function expandUrlAsync(url, opt_bindings, opt_options) { - return getReplacements(opt_options).then((replacements) => - replacements.expandUrlAsync(url, opt_bindings) + if (opt_options.withActivity) { + markElementScheduledForTesting(iframe.win, 'amp-analytics'); + installActivityServiceForTesting(iframe.ampdoc); + } + if (opt_options.withVariant) { + markElementScheduledForTesting(iframe.win, 'amp-experiment'); + registerServiceBuilder(iframe.win, 'variant', function () { + return { + getVariants: () => + Promise.resolve({ + 'x1': 'v1', + 'x2': null, + }), + }; + }); + } + if (opt_options.withViewerIntegrationVariableService) { + markElementScheduledForTesting( + iframe.win, + 'amp-viewer-integration' + ); + registerServiceBuilder( + iframe.win, + 'viewer-integration-variable', + function () { + return Promise.resolve( + opt_options.withViewerIntegrationVariableService + ); + } + ); + } + if (opt_options.withOriginalTitle) { + iframe.doc.originalTitle = 'Original Pixel Test'; + } + } + viewerService = Services.viewerForDoc(iframe.ampdoc); + replacements = Services.urlReplacementsForDoc( + iframe.doc.documentElement ); - } + return replacements; + }); + } - function getFakeWindow() { - loadObservable = new Observable(); - const win = { - addEventListener(type, callback) { - loadObservable.add(callback); - }, - Object, - performance: { - timing: { - navigationStart: 100, - loadEventStart: 0, - }, - }, - removeEventListener(type, callback) { - loadObservable.remove(callback); - }, - document: { - nodeType: /* document */ 9, - querySelector: () => { - return {href: canonical}; - }, - getElementById: () => {}, - cookie: '', - documentElement: { - nodeType: /* element */ 1, - getRootNode() { - return win.document; - }, - hasAttribute: () => {}, - }, + function expandUrlAsync(url, opt_bindings, opt_options) { + return getReplacements(opt_options).then((replacements) => + replacements.expandUrlAsync(url, opt_bindings) + ); + } + + function getFakeWindow() { + loadObservable = new Observable(); + const win = { + addEventListener(type, callback) { + loadObservable.add(callback); + }, + Object, + performance: { + timing: { + navigationStart: 100, + loadEventStart: 0, }, - Math: { - random: () => 0.1234, + }, + removeEventListener(type, callback) { + loadObservable.remove(callback); + }, + document: { + nodeType: /* document */ 9, + querySelector: () => { + return {href: canonical}; }, - crypto: { - getRandomValues: (array) => { - array[0] = 1; - array[1] = 2; - array[2] = 3; - array[15] = 15; + getElementById: () => {}, + cookie: '', + documentElement: { + nodeType: /* element */ 1, + getRootNode() { + return win.document; }, + hasAttribute: () => {}, }, - __AMP_SERVICES: { - 'viewport': {obj: {}, ctor: Object}, - 'cid': { - promise: Promise.resolve({ - get: (config) => - Promise.resolve('test-cid(' + config.scope + ')'), - }), - }, + }, + Math: { + random: () => 0.1234, + }, + crypto: { + getRandomValues: (array) => { + array[0] = 1; + array[1] = 2; + array[2] = 3; + array[15] = 15; }, - }; - win.document.defaultView = win; - win.document.documentElement.ownerDocument = win.document; - win.document.head = { - nodeType: /* element */ 1, - // Fake query selectors needed to bypass tag checks. - querySelector: () => null, - querySelectorAll: () => [], - getRootNode() { - return win.document; + }, + __AMP_SERVICES: { + 'viewport': {obj: {}, ctor: Object}, + 'cid': { + promise: Promise.resolve({ + get: (config) => + Promise.resolve('test-cid(' + config.scope + ')'), + }), }, - }; - installDocService(win, /* isSingleDoc */ true); - const ampdoc = Services.ampdocServiceFor(win).getSingleDoc(); - win.__AMP_SERVICES.documentInfo = null; - installDocumentInfoServiceForDoc(ampdoc); - win.ampdoc = ampdoc; - env.sandbox.stub(win.ampdoc, 'getMeta').returns({ - 'amp-link-variable-allowed-origin': - 'https://allowlisted.com http://example.com', - }); - installUrlReplacementsServiceForDoc(ampdoc); - return win; - } + }, + }; + win.document.defaultView = win; + win.document.documentElement.ownerDocument = win.document; + win.document.head = { + nodeType: /* element */ 1, + // Fake query selectors needed to bypass tag checks. + querySelector: () => null, + querySelectorAll: () => [], + getRootNode() { + return win.document; + }, + }; + installDocService(win, /* isSingleDoc */ true); + const ampdoc = Services.ampdocServiceFor(win).getSingleDoc(); + win.__AMP_SERVICES.documentInfo = null; + installDocumentInfoServiceForDoc(ampdoc); + win.ampdoc = ampdoc; + env.sandbox.stub(win.ampdoc, 'getMeta').returns({ + 'amp-link-variable-allowed-origin': + 'https://allowlisted.com http://example.com', + }); + installUrlReplacementsServiceForDoc(ampdoc); + return win; + } + + it('limit replacement params size', () => { + return getReplacements().then((replacements) => { + replacements.getVariableSource().initialize(); + const variables = Object.keys( + replacements.getVariableSource().replacements_ + ); + // Restrict the number of replacement params to globalVariableSource + // Please consider adding the logic to amp-analytics instead. + // Please contact @lannka or @zhouyx if the test fail. + expect(variables.length).to.equal(60); + }); + }); - it('limit replacement params size', () => { - return getReplacements().then((replacements) => { - replacements.getVariableSource().initialize(); - const variables = Object.keys( - replacements.getVariableSource().replacements_ - ); - // Restrict the number of replacement params to globalVariableSource - // Please consider adding the logic to amp-analytics instead. - // Please contact @lannka or @zhouyx if the test fail. - expect(variables.length).to.equal(60); - }); + it('should replace RANDOM', () => { + return expandUrlAsync('ord=RANDOM?').then((res) => { + expect(res).to.match(/ord=(\d+(\.\d+)?)\?$/); }); + }); - it('should replace RANDOM', () => { - return expandUrlAsync('ord=RANDOM?').then((res) => { - expect(res).to.match(/ord=(\d+(\.\d+)?)\?$/); - }); + it('should replace COUNTER', () => { + return expandUrlAsync( + 'COUNTER(foo),COUNTER(bar),COUNTER(foo),COUNTER(bar),COUNTER(bar)' + ).then((res) => { + expect(res).to.equal('1,1,2,2,3'); }); + }); - it('should replace COUNTER', () => { - return expandUrlAsync( - 'COUNTER(foo),COUNTER(bar),COUNTER(foo),COUNTER(bar),COUNTER(bar)' - ).then((res) => { - expect(res).to.equal('1,1,2,2,3'); - }); + it('should replace CANONICAL_URL', () => { + return expandUrlAsync('?href=CANONICAL_URL').then((res) => { + expect(res).to.equal('?href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1'); }); + }); - it.configure() - .skipFirefox() - .run('should replace CANONICAL_URL', () => { - return expandUrlAsync('?href=CANONICAL_URL').then((res) => { - expect(res).to.equal( - '?href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1' - ); - }); - }); + it('should replace CANONICAL_HOST', () => { + return expandUrlAsync('?host=CANONICAL_HOST').then((res) => { + expect(res).to.equal('?host=pinterest.com%3A8080'); + }); + }); - it.configure() - .skipFirefox() - .run('should replace CANONICAL_HOST', () => { - return expandUrlAsync('?host=CANONICAL_HOST').then((res) => { - expect(res).to.equal('?host=pinterest.com%3A8080'); - }); - }); + it('should replace CANONICAL_HOSTNAME', () => { + return expandUrlAsync('?host=CANONICAL_HOSTNAME').then((res) => { + expect(res).to.equal('?host=pinterest.com'); + }); + }); - it.configure() - .skipFirefox() - .run('should replace CANONICAL_HOSTNAME', () => { - return expandUrlAsync('?host=CANONICAL_HOSTNAME').then((res) => { - expect(res).to.equal('?host=pinterest.com'); - }); - }); + it('should replace CANONICAL_PATH', () => { + return expandUrlAsync('?path=CANONICAL_PATH').then((res) => { + expect(res).to.equal('?path=%2Fpin1'); + }); + }); - it.configure() - .skipFirefox() - .run('should replace CANONICAL_PATH', () => { - return expandUrlAsync('?path=CANONICAL_PATH').then((res) => { - expect(res).to.equal('?path=%2Fpin1'); - }); - }); + it('should replace DOCUMENT_REFERRER', async () => { + const replacements = await getReplacements(); + env.sandbox + .stub(viewerService, 'getReferrerUrl') + .returns('http://fake.example/?foo=bar'); + const res = await replacements.expandUrlAsync('?ref=DOCUMENT_REFERRER'); + expect(res).to.equal('?ref=http%3A%2F%2Ffake.example%2F%3Ffoo%3Dbar'); + }); - it('should replace DOCUMENT_REFERRER', async () => { - const replacements = await getReplacements(); - env.sandbox - .stub(viewerService, 'getReferrerUrl') - .returns('http://fake.example/?foo=bar'); - const res = await replacements.expandUrlAsync('?ref=DOCUMENT_REFERRER'); - expect(res).to.equal('?ref=http%3A%2F%2Ffake.example%2F%3Ffoo%3Dbar'); - }); + it('should replace EXTERNAL_REFERRER', () => { + const windowInterface = mockWindowInterface(env.sandbox); + windowInterface.getHostname.returns('different.org'); + return getReplacements() + .then((replacements) => { + stubServiceForDoc( + env.sandbox, + ampdoc, + 'viewer', + 'getReferrerUrl' + ).returns(Promise.resolve('http://example.org/page.html')); + return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); + }) + .then((res) => { + expect(res).to.equal('?ref=http%3A%2F%2Fexample.org%2Fpage.html'); + }); + }); - it('should replace EXTERNAL_REFERRER', () => { + it( + 'should replace EXTERNAL_REFERRER to empty string ' + + 'if referrer is of same domain', + () => { const windowInterface = mockWindowInterface(env.sandbox); - windowInterface.getHostname.returns('different.org'); + windowInterface.getHostname.returns('example.org'); return getReplacements() .then((replacements) => { stubServiceForDoc( @@ -270,1922 +277,1857 @@ describes.sandboxed('UrlReplacements', {}, (env) => { return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); }) .then((res) => { - expect(res).to.equal('?ref=http%3A%2F%2Fexample.org%2Fpage.html'); + expect(res).to.equal('?ref='); }); - }); - - it( - 'should replace EXTERNAL_REFERRER to empty string ' + - 'if referrer is of same domain', - () => { - const windowInterface = mockWindowInterface(env.sandbox); - windowInterface.getHostname.returns('example.org'); - return getReplacements() - .then((replacements) => { - stubServiceForDoc( - env.sandbox, - ampdoc, - 'viewer', - 'getReferrerUrl' - ).returns(Promise.resolve('http://example.org/page.html')); - return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); - }) - .then((res) => { - expect(res).to.equal('?ref='); - }); - } - ); + } + ); - it( - 'should replace EXTERNAL_REFERRER to empty string ' + - 'if referrer is CDN proxy of same domain', - () => { - const windowInterface = mockWindowInterface(env.sandbox); - windowInterface.getHostname.returns('example.org'); - return getReplacements() - .then((replacements) => { - stubServiceForDoc( - env.sandbox, - ampdoc, - 'viewer', - 'getReferrerUrl' - ).returns( - Promise.resolve( - 'https://example-org.cdn.ampproject.org/v/example.org/page.html' - ) - ); - return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); - }) - .then((res) => { - expect(res).to.equal('?ref='); - }); - } - ); + it( + 'should replace EXTERNAL_REFERRER to empty string ' + + 'if referrer is CDN proxy of same domain', + () => { + const windowInterface = mockWindowInterface(env.sandbox); + windowInterface.getHostname.returns('example.org'); + return getReplacements() + .then((replacements) => { + stubServiceForDoc( + env.sandbox, + ampdoc, + 'viewer', + 'getReferrerUrl' + ).returns( + Promise.resolve( + 'https://example-org.cdn.ampproject.org/v/example.org/page.html' + ) + ); + return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); + }) + .then((res) => { + expect(res).to.equal('?ref='); + }); + } + ); - it( - 'should replace EXTERNAL_REFERRER to empty string ' + - 'if referrer is CDN proxy of same domain (before CURLS)', - () => { - const windowInterface = mockWindowInterface(env.sandbox); - windowInterface.getHostname.returns('example.org'); - return getReplacements() - .then((replacements) => { - stubServiceForDoc( - env.sandbox, - ampdoc, - 'viewer', - 'getReferrerUrl' - ).returns( - Promise.resolve( - 'https://cdn.ampproject.org/v/example.org/page.html' - ) - ); - return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); - }) - .then((res) => { - expect(res).to.equal('?ref='); - }); - } - ); + it( + 'should replace EXTERNAL_REFERRER to empty string ' + + 'if referrer is CDN proxy of same domain (before CURLS)', + () => { + const windowInterface = mockWindowInterface(env.sandbox); + windowInterface.getHostname.returns('example.org'); + return getReplacements() + .then((replacements) => { + stubServiceForDoc( + env.sandbox, + ampdoc, + 'viewer', + 'getReferrerUrl' + ).returns( + Promise.resolve( + 'https://cdn.ampproject.org/v/example.org/page.html' + ) + ); + return replacements.expandUrlAsync('?ref=EXTERNAL_REFERRER'); + }) + .then((res) => { + expect(res).to.equal('?ref='); + }); + } + ); - it('should replace TITLE', () => { - return expandUrlAsync('?title=TITLE').then((res) => { - expect(res).to.equal('?title=Pixel%20Test'); - }); + it('should replace TITLE', () => { + return expandUrlAsync('?title=TITLE').then((res) => { + expect(res).to.equal('?title=Pixel%20Test'); }); + }); - it('should prefer original title for TITLE', () => { - return expandUrlAsync('?title=TITLE', /*opt_bindings*/ undefined, { - withOriginalTitle: true, - }).then((res) => { - expect(res).to.equal('?title=Original%20Pixel%20Test'); - }); + it('should prefer original title for TITLE', () => { + return expandUrlAsync('?title=TITLE', /*opt_bindings*/ undefined, { + withOriginalTitle: true, + }).then((res) => { + expect(res).to.equal('?title=Original%20Pixel%20Test'); }); + }); - describe('AMPDOC_URL', () => { - it('should replace AMPDOC_URL', () => { - return expandUrlAsync('?ref=AMPDOC_URL').then((res) => { - expect(res).to.not.match(/AMPDOC_URL/); - }); - }); - - it('should add extra params to AMPDOC_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=hello%3Dworld' - ); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=AMPDOC_URL') - .then((res) => { - expect(res).to.contain( - encodeURIComponent( - 'https://cdn.ampproject.org/a/o.com/foo/?hello=world' - ) - ); - }); - }); - - it('should merge extra params in AMPDOC_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case&_r=hello%3Dworld' - ); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=AMPDOC_URL') - .then((res) => { - expect(res).to.contain( - encodeURIComponent( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case&hello=world' - ) - ); - }); - }); - - it('should allow an embedded amp_r parameter', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=amp_r%3Dweird' - ); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=AMPDOC_URL') - .then((res) => { - expect(res).to.contain( - encodeURIComponent( - 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=weird' - ) - ); - }); - }); - - it('should prefer original params in AMPDOC_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case&_r=test%3Devil' - ); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=AMPDOC_URL') - .then((res) => { - expect(res).to.contain( - encodeURIComponent( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case' - ) - ); - }); - }); - - it('should merge multiple extra params safely in AMPDOC_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case&a&hello=you&_r=hello%3Dworld%26goodnight%3Dmoon' - ); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=AMPDOC_URL') - .then((res) => { - expect(res).to.contain( - encodeURIComponent( - 'https://cdn.ampproject.org/a/o.com/foo/?test=case&a&hello=you&goodnight=moon' - ) - ); - }); + describe('AMPDOC_URL', () => { + it('should replace AMPDOC_URL', () => { + return expandUrlAsync('?ref=AMPDOC_URL').then((res) => { + expect(res).to.not.match(/AMPDOC_URL/); }); }); - it('should replace AMPDOC_HOST', () => { - return expandUrlAsync('?ref=AMPDOC_HOST').then((res) => { - expect(res).to.not.match(/AMPDOC_HOST/); - }); + it('should add extra params to AMPDOC_URL', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=hello%3Dworld' + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=AMPDOC_URL') + .then((res) => { + expect(res).to.contain( + encodeURIComponent( + 'https://cdn.ampproject.org/a/o.com/foo/?hello=world' + ) + ); + }); }); - it('should replace AMPDOC_HOSTNAME', () => { - return expandUrlAsync('?ref=AMPDOC_HOSTNAME').then((res) => { - expect(res).to.not.match(/AMPDOC_HOSTNAME/); - }); + it('should merge extra params in AMPDOC_URL', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case&_r=hello%3Dworld' + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=AMPDOC_URL') + .then((res) => { + expect(res).to.contain( + encodeURIComponent( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case&hello=world' + ) + ); + }); }); - describe('SOURCE_URL', () => { - it('should replace SOURCE_URL and SOURCE_HOST', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://wrong.com'); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated('https://example.com/test'); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL&host=SOURCE_HOST') - .then((res) => { - expect(res).to.equal( - '?url=https%3A%2F%2Fexample.com%2Ftest&host=example.com' - ); - }); - }); - - it('should replace SOURCE_URL and SOURCE_HOSTNAME', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://wrong.com'); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated('https://example.com/test'); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL&hostname=SOURCE_HOSTNAME') - .then((res) => { - expect(res).to.equal( - '?url=https%3A%2F%2Fexample.com%2Ftest&hostname=example.com' - ); - }); - }); - - it('should update SOURCE_URL after track impression', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://wrong.com'); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated( - 'https://example.com?gclid=123456' - ); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL') - .then((res) => { - expect(res).to.contain('example.com'); - }); - }); - - it('should add extra params to SOURCE_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?a&_r=hello%3Dworld' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return Promise.resolve(); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL') - .then((res) => { - expect(res).to.equal( - '?url=' + encodeURIComponent('http://o.com/foo/?a&hello=world') - ); - }); - }); - - it('should ignore extra params that already exists in SOURCE_URL', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?a=1&safe=1&_r=hello%3Dworld%26safe=evil' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return Promise.resolve(); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL') - .then((res) => { - expect(res).to.equal( - '?url=' + - encodeURIComponent('http://o.com/foo/?a=1&safe=1&hello=world') - ); - }); - }); - - it('should not change SOURCE_URL if is not ad landing page', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/v/o.com/foo/?a&_r=hello%3Dworld' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return Promise.resolve(); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?url=SOURCE_URL') - .then((res) => { - expect(res).to.equal( - '?url=' + encodeURIComponent('http://o.com/foo/?a') - ); - }); - }); + it('should allow an embedded amp_r parameter', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=amp_r%3Dweird' + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=AMPDOC_URL') + .then((res) => { + expect(res).to.contain( + encodeURIComponent( + 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=weird' + ) + ); + }); }); - it('should replace SOURCE_PATH', () => { - return expandUrlAsync('?path=SOURCE_PATH').then((res) => { - expect(res).to.not.match(/SOURCE_PATH/); - }); + it('should prefer original params in AMPDOC_URL', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case&_r=test%3Devil' + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=AMPDOC_URL') + .then((res) => { + expect(res).to.contain( + encodeURIComponent( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case' + ) + ); + }); }); - it('should replace PAGE_VIEW_ID', () => { - return expandUrlAsync('?pid=PAGE_VIEW_ID').then((res) => { - expect(res).to.match(/pid=\d+/); - }); + it('should merge multiple extra params safely in AMPDOC_URL', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case&a&hello=you&_r=hello%3Dworld%26goodnight%3Dmoon' + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=AMPDOC_URL') + .then((res) => { + expect(res).to.contain( + encodeURIComponent( + 'https://cdn.ampproject.org/a/o.com/foo/?test=case&a&hello=you&goodnight=moon' + ) + ); + }); }); + }); - it('should replace PAGE_VIEW_ID_64', () => { - return expandUrlAsync('?pid=PAGE_VIEW_ID_64').then((res) => { - expect(res).to.match(/pid=([a-zA-Z0-9_-]{10,})/); - }); + it('should replace AMPDOC_HOST', () => { + return expandUrlAsync('?ref=AMPDOC_HOST').then((res) => { + expect(res).to.not.match(/AMPDOC_HOST/); }); + }); - it('should replace CLIENT_ID', () => { - setCookie(window, 'url-abc', 'cid-for-abc'); - // Make sure cookie does not exist - setCookie(window, 'url-xyz', ''); - return expandUrlAsync( - '?a=CLIENT_ID(url-abc)&b=CLIENT_ID(url-xyz)', - /*opt_bindings*/ undefined, - {withCid: true} - ).then((res) => { - expect(res).to.match(/^\?a=cid-for-abc\&b=amp-([a-zA-Z0-9_-]{10,})/); - }); + it('should replace AMPDOC_HOSTNAME', () => { + return expandUrlAsync('?ref=AMPDOC_HOSTNAME').then((res) => { + expect(res).to.not.match(/AMPDOC_HOSTNAME/); }); + }); - it('should allow empty CLIENT_ID', () => { - return getReplacements() - .then((replacements) => { - stubServiceForDoc(env.sandbox, ampdoc, 'cid', 'get').returns( - Promise.resolve() - ); - return replacements.expandUrlAsync('?a=CLIENT_ID(_ga)'); - }) + describe('SOURCE_URL', () => { + it('should replace SOURCE_URL and SOURCE_HOST', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated('https://wrong.com'); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated('https://example.com/test'); + resolve(); + }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=SOURCE_URL&host=SOURCE_HOST') .then((res) => { - expect(res).to.equal('?a='); + expect(res).to.equal( + '?url=https%3A%2F%2Fexample.com%2Ftest&host=example.com' + ); }); }); - it('should replace CLIENT_ID with opt_cookieName', () => { - setCookie(window, 'url-abc', 'cid-for-abc'); - // Make sure cookie does not exist - setCookie(window, 'url-xyz', ''); - return expandUrlAsync( - '?a=CLIENT_ID(abc,,url-abc)&b=CLIENT_ID(xyz,,url-xyz)', - /*opt_bindings*/ undefined, - {withCid: true} - ).then((res) => { - expect(res).to.match(/^\?a=cid-for-abc\&b=amp-([a-zA-Z0-9_-]{10,})/); - }); + it('should replace SOURCE_URL and SOURCE_HOSTNAME', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated('https://wrong.com'); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated('https://example.com/test'); + resolve(); + }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=SOURCE_URL&hostname=SOURCE_HOSTNAME') + .then((res) => { + expect(res).to.equal( + '?url=https%3A%2F%2Fexample.com%2Ftest&hostname=example.com' + ); + }); }); - it('should parse _ga cookie correctly', () => { - setCookie(window, '_ga', 'GA1.2.12345.54321'); - return expandUrlAsync( - '?a=CLIENT_ID(AMP_ECID_GOOGLE,,_ga)&b=CLIENT_ID(_ga)', - /*opt_bindings*/ undefined, - {withCid: true} - ).then((res) => { - expect(res).to.match(/^\?a=12345.54321&b=12345.54321/); - }); + it('should update SOURCE_URL after track impression', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated('https://wrong.com'); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated( + 'https://example.com?gclid=123456' + ); + resolve(); + }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=SOURCE_URL') + .then((res) => { + expect(res).to.contain('example.com'); + }); }); - // TODO(alanorozco, #11827): Make this test work on Safari. - it.configure() - .skipSafari() - .run('should replace CLIENT_ID synchronously when available', () => { - return getReplacements({withCid: true}).then((urlReplacements) => { - setCookie(window, 'url-abc', 'cid-for-abc'); - setCookie(window, 'url-xyz', 'cid-for-xyz'); - // Only requests cid-for-xyz in async path - return urlReplacements - .expandUrlAsync('b=CLIENT_ID(url-xyz)') - .then((res) => { - expect(res).to.equal('b=cid-for-xyz'); - }) - .then(() => { - const result = urlReplacements.expandUrlSync( - '?a=CLIENT_ID(url-abc)&b=CLIENT_ID(url-xyz)' + - '&c=CLIENT_ID(other)' - ); - expect(result).to.equal('?a=&b=cid-for-xyz&c='); - }); + it('should add extra params to SOURCE_URL', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?a&_r=hello%3Dworld' + ); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return Promise.resolve(); }); - }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=SOURCE_URL') + .then((res) => { + expect(res).to.equal( + '?url=' + encodeURIComponent('http://o.com/foo/?a&hello=world') + ); + }); + }); - it('should replace AMP_STATE(key)', () => { + it('should ignore extra params that already exists in SOURCE_URL', () => { const win = getFakeWindow(); - env.sandbox.stub(Services, 'bindForDocOrNull').returns( - Promise.resolve({ - getStateValue(key) { - expect(key).to.equal('foo.bar'); - return Promise.resolve('baz'); - }, - }) + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?a=1&safe=1&_r=hello%3Dworld%26safe=evil' ); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return Promise.resolve(); + }); return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?state=AMP_STATE(foo.bar)') + .expandUrlAsync('?url=SOURCE_URL') .then((res) => { - expect(res).to.equal('?state=baz'); + expect(res).to.equal( + '?url=' + + encodeURIComponent('http://o.com/foo/?a=1&safe=1&hello=world') + ); }); }); - // TODO(#16916): Make this test work with synchronous throws. - it.skip('should replace VARIANT', () => { - return expect( - expandUrlAsync( - '?x1=VARIANT(x1)&x2=VARIANT(x2)&x3=VARIANT(x3)', - /*opt_bindings*/ undefined, - {withVariant: true} - ) - ).to.eventually.equal('?x1=v1&x2=none&x3='); + it('should not change SOURCE_URL if is not ad landing page', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/v/o.com/foo/?a&_r=hello%3Dworld' + ); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return Promise.resolve(); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?url=SOURCE_URL') + .then((res) => { + expect(res).to.equal( + '?url=' + encodeURIComponent('http://o.com/foo/?a') + ); + }); }); + }); - // TODO(#16916): Make this test work with synchronous throws. - it.skip( - 'should replace VARIANT with empty string if ' + - 'amp-experiment is not configured ', - () => { - return expect( - expandUrlAsync('?x1=VARIANT(x1)&x2=VARIANT(x2)&x3=VARIANT(x3)') - ).to.eventually.equal('?x1=&x2=&x3='); - } - ); - - it('should replace VARIANTS', () => { - return expect( - expandUrlAsync('?VARIANTS', /*opt_bindings*/ undefined, { - withVariant: true, - }) - ).to.eventually.equal('?x1.v1!x2.none'); + it('should replace SOURCE_PATH', () => { + return expandUrlAsync('?path=SOURCE_PATH').then((res) => { + expect(res).to.not.match(/SOURCE_PATH/); }); + }); - // TODO(#16916): Make this test work with synchronous throws. - it.skip( - 'should replace VARIANTS with empty string if ' + - 'amp-experiment is not configured ', - () => { - return expect(expandUrlAsync('?VARIANTS')).to.eventually.equal('?'); - } - ); - - it('should replace TIMESTAMP', () => { - return expandUrlAsync('?ts=TIMESTAMP').then((res) => { - expect(res).to.match(/ts=\d+/); - }); + it('should replace PAGE_VIEW_ID', () => { + return expandUrlAsync('?pid=PAGE_VIEW_ID').then((res) => { + expect(res).to.match(/pid=\d+/); }); + }); - it('should replace TIMESTAMP_ISO', () => { - return expandUrlAsync('?tsf=TIMESTAMP_ISO').then((res) => { - expect(res).to.match(/tsf=\d+/); - }); + it('should replace PAGE_VIEW_ID_64', () => { + return expandUrlAsync('?pid=PAGE_VIEW_ID_64').then((res) => { + expect(res).to.match(/pid=([a-zA-Z0-9_-]{10,})/); }); + }); - it('should return correct ISO timestamp', () => { - const fakeTime = 1499979336612; - env.sandbox.useFakeTimers(fakeTime); - return expect(expandUrlAsync('?tsf=TIMESTAMP_ISO')).to.eventually.equal( - '?tsf=2017-07-13T20%3A55%3A36.612Z' - ); + it('should replace CLIENT_ID', () => { + setCookie(window, 'url-abc', 'cid-for-abc'); + // Make sure cookie does not exist + setCookie(window, 'url-xyz', ''); + return expandUrlAsync( + '?a=CLIENT_ID(url-abc)&b=CLIENT_ID(url-xyz)', + /*opt_bindings*/ undefined, + {withCid: true} + ).then((res) => { + expect(res).to.match(/^\?a=cid-for-abc\&b=amp-([a-zA-Z0-9_-]{10,})/); }); + }); - it('should replace TIMEZONE', () => { - return expandUrlAsync('?tz=TIMEZONE').then((res) => { - expect(res).to.match(/tz=-?\d+/); + it('should allow empty CLIENT_ID', () => { + return getReplacements() + .then((replacements) => { + stubServiceForDoc(env.sandbox, ampdoc, 'cid', 'get').returns( + Promise.resolve() + ); + return replacements.expandUrlAsync('?a=CLIENT_ID(_ga)'); + }) + .then((res) => { + expect(res).to.equal('?a='); }); - }); + }); - it('should replace SCROLL_HEIGHT', () => { - return expandUrlAsync('?scrollHeight=SCROLL_HEIGHT').then((res) => { - expect(res).to.match(/scrollHeight=\d+/); - }); + it('should replace CLIENT_ID with opt_cookieName', () => { + setCookie(window, 'url-abc', 'cid-for-abc'); + // Make sure cookie does not exist + setCookie(window, 'url-xyz', ''); + return expandUrlAsync( + '?a=CLIENT_ID(abc,,url-abc)&b=CLIENT_ID(xyz,,url-xyz)', + /*opt_bindings*/ undefined, + {withCid: true} + ).then((res) => { + expect(res).to.match(/^\?a=cid-for-abc\&b=amp-([a-zA-Z0-9_-]{10,})/); }); + }); - it('should replace SCREEN_WIDTH', () => { - return expandUrlAsync('?sw=SCREEN_WIDTH').then((res) => { - expect(res).to.match(/sw=\d+/); - }); + it('should parse _ga cookie correctly', () => { + setCookie(window, '_ga', 'GA1.2.12345.54321'); + return expandUrlAsync( + '?a=CLIENT_ID(AMP_ECID_GOOGLE,,_ga)&b=CLIENT_ID(_ga)', + /*opt_bindings*/ undefined, + {withCid: true} + ).then((res) => { + expect(res).to.match(/^\?a=12345.54321&b=12345.54321/); }); + }); - it('should replace SCREEN_HEIGHT', () => { - return expandUrlAsync('?sh=SCREEN_HEIGHT').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace CLIENT_ID synchronously when available', () => { + return getReplacements({withCid: true}).then((urlReplacements) => { + setCookie(window, 'url-abc', 'cid-for-abc'); + setCookie(window, 'url-xyz', 'cid-for-xyz'); + // Only requests cid-for-xyz in async path + return urlReplacements + .expandUrlAsync('b=CLIENT_ID(url-xyz)') + .then((res) => { + expect(res).to.equal('b=cid-for-xyz'); + }) + .then(() => { + const result = urlReplacements.expandUrlSync( + '?a=CLIENT_ID(url-abc)&b=CLIENT_ID(url-xyz)' + + '&c=CLIENT_ID(other)' + ); + expect(result).to.equal('?a=&b=cid-for-xyz&c='); + }); }); + }); - it('should replace VIEWPORT_WIDTH', () => { - return expandUrlAsync('?vw=VIEWPORT_WIDTH').then((res) => { - expect(res).to.match(/vw=\d+/); + it('should replace AMP_STATE(key)', () => { + const win = getFakeWindow(); + env.sandbox.stub(Services, 'bindForDocOrNull').returns( + Promise.resolve({ + getStateValue(key) { + expect(key).to.equal('foo.bar'); + return Promise.resolve('baz'); + }, + }) + ); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?state=AMP_STATE(foo.bar)') + .then((res) => { + expect(res).to.equal('?state=baz'); }); - }); + }); - it('should replace VIEWPORT_HEIGHT', () => { - return expandUrlAsync('?vh=VIEWPORT_HEIGHT').then((res) => { - expect(res).to.match(/vh=\d+/); - }); - }); + it.skip('should replace VARIANT', () => { + return expect( + expandUrlAsync( + '?x1=VARIANT(x1)&x2=VARIANT(x2)&x3=VARIANT(x3)', + /*opt_bindings*/ undefined, + {withVariant: true} + ) + ).to.eventually.equal('?x1=v1&x2=none&x3='); + }); - it('should replace PAGE_LOAD_TIME', () => { - return expandUrlAsync('?sh=PAGE_LOAD_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); - }); - }); + it.skip( + 'should replace VARIANT with empty string if ' + + 'amp-experiment is not configured ', + () => { + return expect( + expandUrlAsync('?x1=VARIANT(x1)&x2=VARIANT(x2)&x3=VARIANT(x3)') + ).to.eventually.equal('?x1=&x2=&x3='); + } + ); + + it('should replace VARIANTS', () => { + return expect( + expandUrlAsync('?VARIANTS', /*opt_bindings*/ undefined, { + withVariant: true, + }) + ).to.eventually.equal('?x1.v1!x2.none'); + }); - it('should reject protocol changes', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = Services.urlReplacementsForDoc(documentElement); - return urlReplacements - .expandUrlAsync('PROTOCOL://example.com/?r=RANDOM', { - 'PROTOCOL': Promise.resolve('abc'), - }) - .then((expanded) => { - expect(expanded).to.equal('PROTOCOL://example.com/?r=RANDOM'); - }); - }); + it.skip( + 'should replace VARIANTS with empty string if ' + + 'amp-experiment is not configured ', + () => { + return expect(expandUrlAsync('?VARIANTS')).to.eventually.equal('?'); + } + ); - it('Should replace BACKGROUND_STATE with 0', () => { - const win = getFakeWindow(); - const {ampdoc} = win; - env.sandbox.stub(ampdoc, 'isVisible').returns(true); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=BACKGROUND_STATE') - .then((res) => { - expect(res).to.equal('?sh=0'); - }); + it('should replace TIMESTAMP', () => { + return expandUrlAsync('?ts=TIMESTAMP').then((res) => { + expect(res).to.match(/ts=\d+/); }); + }); - it('Should replace BACKGROUND_STATE with 1', () => { - const win = getFakeWindow(); - const {ampdoc} = win; - env.sandbox.stub(ampdoc, 'isVisible').returns(false); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=BACKGROUND_STATE') - .then((res) => { - expect(res).to.equal('?sh=1'); - }); + it('should replace TIMESTAMP_ISO', () => { + return expandUrlAsync('?tsf=TIMESTAMP_ISO').then((res) => { + expect(res).to.match(/tsf=\d+/); }); + }); - it('Should replace VIDEO_STATE(video,parameter) with video data', () => { - const win = getFakeWindow(); - env.sandbox.stub(Services, 'videoManagerForDoc').returns({ - getVideoStateProperty() { - return Promise.resolve('1.5'); - }, - }); - env.sandbox - .stub(win.document, 'getElementById') - .withArgs('video') - .returns(document.createElement('video')); + it('should return correct ISO timestamp', () => { + const fakeTime = 1499979336612; + env.sandbox.useFakeTimers(fakeTime); + return expect(expandUrlAsync('?tsf=TIMESTAMP_ISO')).to.eventually.equal( + '?tsf=2017-07-13T20%3A55%3A36.612Z' + ); + }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=VIDEO_STATE(video,currentTime)') - .then((res) => { - expect(res).to.equal('?sh=1.5'); - }); + it('should replace TIMEZONE', () => { + return expandUrlAsync('?tz=TIMEZONE').then((res) => { + expect(res).to.match(/tz=-?\d+/); }); + }); - describe('PAGE_LOAD_TIME', () => { - let win; - let eventListeners; - beforeEach(() => { - win = getFakeWindow(); - eventListeners = {}; - win.document.readyState = 'loading'; - win.document.addEventListener = function (eventType, handler) { - eventListeners[eventType] = handler; - }; - win.document.removeEventListener = function (eventType, handler) { - if (eventListeners[eventType] == handler) { - delete eventListeners[eventType]; - } - }; - }); - - it('is replaced if timing info is not available', () => { - win.document.readyState = 'complete'; - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=PAGE_LOAD_TIME&s') - .then((res) => { - expect(res).to.match(/sh=&s/); - }); - }); + it('should replace SCROLL_HEIGHT', () => { + return expandUrlAsync('?scrollHeight=SCROLL_HEIGHT').then((res) => { + expect(res).to.match(/scrollHeight=\d+/); + }); + }); - it('is replaced if PAGE_LOAD_TIME is available within a delay', () => { - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const validMetric = urlReplacements.expandUrlAsync( - '?sh=PAGE_LOAD_TIME&s' - ); - urlReplacements.ampdoc.win.performance.timing.loadEventStart = 109; - win.document.readyState = 'complete'; - loadObservable.fire({type: 'load'}); - return validMetric.then((res) => { - expect(res).to.match(/sh=9&s/); - }); - }); + it('should replace SCREEN_WIDTH', () => { + return expandUrlAsync('?sw=SCREEN_WIDTH').then((res) => { + expect(res).to.match(/sw=\d+/); }); + }); - it('should replace NAV_REDIRECT_COUNT', () => { - return expandUrlAsync('?sh=NAV_REDIRECT_COUNT').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace SCREEN_HEIGHT', () => { + return expandUrlAsync('?sh=SCREEN_HEIGHT').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - // TODO(cvializ, #12336): unskip - it.skip('should replace NAV_TIMING', () => { - return expandUrlAsync( - '?a=NAV_TIMING(navigationStart)' + - '&b=NAV_TIMING(navigationStart,responseStart)' - ).then((res) => { - expect(res).to.match(/a=\d+&b=\d+/); - }); + it('should replace VIEWPORT_WIDTH', () => { + return expandUrlAsync('?vw=VIEWPORT_WIDTH').then((res) => { + expect(res).to.match(/vw=\d+/); }); + }); - it('should replace NAV_TIMING when attribute names are invalid', () => { - return expandUrlAsync( - '?a=NAV_TIMING(invalid)' + - '&b=NAV_TIMING(invalid,invalid)' + - '&c=NAV_TIMING(navigationStart,invalid)' + - '&d=NAV_TIMING(invalid,responseStart)' - ).then((res) => { - expect(res).to.match(/a=&b=&c=&d=/); - }); + it('should replace VIEWPORT_HEIGHT', () => { + return expandUrlAsync('?vh=VIEWPORT_HEIGHT').then((res) => { + expect(res).to.match(/vh=\d+/); }); + }); - it('should replace NAV_TYPE', () => { - return expandUrlAsync('?sh=NAV_TYPE').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace PAGE_LOAD_TIME', () => { + return expandUrlAsync('?sh=PAGE_LOAD_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace DOMAIN_LOOKUP_TIME', () => { - return expandUrlAsync('?sh=DOMAIN_LOOKUP_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); + it('should reject protocol changes', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + return urlReplacements + .expandUrlAsync('PROTOCOL://example.com/?r=RANDOM', { + 'PROTOCOL': Promise.resolve('abc'), + }) + .then((expanded) => { + expect(expanded).to.equal('PROTOCOL://example.com/?r=RANDOM'); }); - }); + }); - it('should replace TCP_CONNECT_TIME', () => { - return expandUrlAsync('?sh=TCP_CONNECT_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); + it('Should replace BACKGROUND_STATE with 0', () => { + const win = getFakeWindow(); + const {ampdoc} = win; + env.sandbox.stub(ampdoc, 'isVisible').returns(true); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=BACKGROUND_STATE') + .then((res) => { + expect(res).to.equal('?sh=0'); }); - }); + }); - it('should replace SERVER_RESPONSE_TIME', () => { - return expandUrlAsync('?sh=SERVER_RESPONSE_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); + it('Should replace BACKGROUND_STATE with 1', () => { + const win = getFakeWindow(); + const {ampdoc} = win; + env.sandbox.stub(ampdoc, 'isVisible').returns(false); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=BACKGROUND_STATE') + .then((res) => { + expect(res).to.equal('?sh=1'); }); + }); + + it('Should replace VIDEO_STATE(video,parameter) with video data', () => { + const win = getFakeWindow(); + env.sandbox.stub(Services, 'videoManagerForDoc').returns({ + getVideoStateProperty() { + return Promise.resolve('1.5'); + }, }); + env.sandbox + .stub(win.document, 'getElementById') + .withArgs('video') + .returns(document.createElement('video')); - it('should replace PAGE_DOWNLOAD_TIME', () => { - return expandUrlAsync('?sh=PAGE_DOWNLOAD_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=VIDEO_STATE(video,currentTime)') + .then((res) => { + expect(res).to.equal('?sh=1.5'); }); + }); + + describe('PAGE_LOAD_TIME', () => { + let win; + let eventListeners; + beforeEach(() => { + win = getFakeWindow(); + eventListeners = {}; + win.document.readyState = 'loading'; + win.document.addEventListener = function (eventType, handler) { + eventListeners[eventType] = handler; + }; + win.document.removeEventListener = function (eventType, handler) { + if (eventListeners[eventType] == handler) { + delete eventListeners[eventType]; + } + }; }); - // TODO(cvializ, #12336): unskip - it.skip('should replace REDIRECT_TIME', () => { - return expandUrlAsync('?sh=REDIRECT_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('is replaced if timing info is not available', () => { + win.document.readyState = 'complete'; + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=PAGE_LOAD_TIME&s') + .then((res) => { + expect(res).to.match(/sh=&s/); + }); }); - it('should replace DOM_INTERACTIVE_TIME', () => { - return expandUrlAsync('?sh=DOM_INTERACTIVE_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); + it('is replaced if PAGE_LOAD_TIME is available within a delay', () => { + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const validMetric = urlReplacements.expandUrlAsync( + '?sh=PAGE_LOAD_TIME&s' + ); + urlReplacements.ampdoc.win.performance.timing.loadEventStart = 109; + win.document.readyState = 'complete'; + loadObservable.fire({type: 'load'}); + return validMetric.then((res) => { + expect(res).to.match(/sh=9&s/); }); }); + }); - it('should replace CONTENT_LOAD_TIME', () => { - return expandUrlAsync('?sh=CONTENT_LOAD_TIME').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace NAV_REDIRECT_COUNT', () => { + return expandUrlAsync('?sh=NAV_REDIRECT_COUNT').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace AVAILABLE_SCREEN_HEIGHT', () => { - return expandUrlAsync('?sh=AVAILABLE_SCREEN_HEIGHT').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it.skip('should replace NAV_TIMING', () => { + return expandUrlAsync( + '?a=NAV_TIMING(navigationStart)' + + '&b=NAV_TIMING(navigationStart,responseStart)' + ).then((res) => { + expect(res).to.match(/a=\d+&b=\d+/); }); + }); - it('should replace AVAILABLE_SCREEN_WIDTH', () => { - return expandUrlAsync('?sh=AVAILABLE_SCREEN_WIDTH').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace NAV_TIMING when attribute names are invalid', () => { + return expandUrlAsync( + '?a=NAV_TIMING(invalid)' + + '&b=NAV_TIMING(invalid,invalid)' + + '&c=NAV_TIMING(navigationStart,invalid)' + + '&d=NAV_TIMING(invalid,responseStart)' + ).then((res) => { + expect(res).to.match(/a=&b=&c=&d=/); }); + }); - it('should replace SCREEN_COLOR_DEPTH', () => { - return expandUrlAsync('?sh=SCREEN_COLOR_DEPTH').then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace NAV_TYPE', () => { + return expandUrlAsync('?sh=NAV_TYPE').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace BROWSER_LANGUAGE', () => { - return expandUrlAsync('?sh=BROWSER_LANGUAGE').then((res) => { - expect(res).to.match(/sh=\w+/); - }); + it('should replace DOMAIN_LOOKUP_TIME', () => { + return expandUrlAsync('?sh=DOMAIN_LOOKUP_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace UACH platform', () => { - return expandUrlAsync('?sh=UACH(platform)').then((res) => { - expect(res).to.match(/sh=\w?/); - }); + it('should replace TCP_CONNECT_TIME', () => { + return expandUrlAsync('?sh=TCP_CONNECT_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace UACH brands', () => { - return expandUrlAsync('?sh=UACH(brands)').then((res) => { - expect(res).to.match(/sh=\w?/); - }); + it('should replace SERVER_RESPONSE_TIME', () => { + return expandUrlAsync('?sh=SERVER_RESPONSE_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace USER_AGENT', () => { - return expandUrlAsync('?sh=USER_AGENT').then((res) => { - expect(res).to.match(/sh=\w+/); - }); + it('should replace PAGE_DOWNLOAD_TIME', () => { + return expandUrlAsync('?sh=PAGE_DOWNLOAD_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace VIEWER with origin', () => { - return getReplacements().then((replacements) => { - env.sandbox - .stub(viewerService, 'getViewerOrigin') - .returns(Promise.resolve('https://www.google.com')); - return replacements.expandUrlAsync('?sh=VIEWER').then((res) => { - expect(res).to.equal('?sh=https%3A%2F%2Fwww.google.com'); - }); - }); + it.skip('should replace REDIRECT_TIME', () => { + return expandUrlAsync('?sh=REDIRECT_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace VIEWER with empty string', () => { - return getReplacements().then((replacements) => { - env.sandbox - .stub(viewerService, 'getViewerOrigin') - .returns(Promise.resolve('')); - return replacements.expandUrlAsync('?sh=VIEWER').then((res) => { - expect(res).to.equal('?sh='); - }); - }); + it('should replace DOM_INTERACTIVE_TIME', () => { + return expandUrlAsync('?sh=DOM_INTERACTIVE_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace TOTAL_ENGAGED_TIME', () => { - return expandUrlAsync( - '?sh=TOTAL_ENGAGED_TIME', - /*opt_bindings*/ undefined, - {withActivity: true} - ).then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace CONTENT_LOAD_TIME', () => { + return expandUrlAsync('?sh=CONTENT_LOAD_TIME').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace INCREMENTAL_ENGAGED_TIME', () => { - return expandUrlAsync( - '?sh=INCREMENTAL_ENGAGED_TIME', - /*opt_bindings*/ undefined, - {withActivity: true} - ).then((res) => { - expect(res).to.match(/sh=\d+/); - }); + it('should replace AVAILABLE_SCREEN_HEIGHT', () => { + return expandUrlAsync('?sh=AVAILABLE_SCREEN_HEIGHT').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace AMP_VERSION', () => { - return expandUrlAsync('?sh=AMP_VERSION').then((res) => { - expect(res).to.equal('?sh=%24internalRuntimeVersion%24'); - }); + it('should replace AVAILABLE_SCREEN_WIDTH', () => { + return expandUrlAsync('?sh=AVAILABLE_SCREEN_WIDTH').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should replace FRAGMENT_PARAM with 2', () => { - const win = getFakeWindow(); - win.location = {originalHash: '#margarine=1&ice=2&cream=3'}; - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=FRAGMENT_PARAM(ice)&s') - .then((res) => { - expect(res).to.equal('?sh=2&s'); - }); + it('should replace SCREEN_COLOR_DEPTH', () => { + return expandUrlAsync('?sh=SCREEN_COLOR_DEPTH').then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - it('should async replace AMP_GEO(ISOCountry) and AMP_GEO', () => { - env.sandbox.stub(Services, 'geoForDocOrNull').returns( - Promise.resolve({ - 'ISOCountry': 'unknown', - 'ISOCountryGroups': ['nafta', 'waldo'], - 'nafta': true, - 'waldo': true, - 'matchedISOCountryGroups': ['nafta', 'waldo'], - }) - ); - return expandUrlAsync('?geo=AMP_GEO,country=AMP_GEO(ISOCountry)').then( - (res) => { - expect(res).to.equal('?geo=nafta%2Cwaldo,country=unknown'); - } - ); + it('should replace BROWSER_LANGUAGE', () => { + return expandUrlAsync('?sh=BROWSER_LANGUAGE').then((res) => { + expect(res).to.match(/sh=\w+/); }); + }); - it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO', () => { - env.sandbox.stub(Services, 'geoForDocOrNull').returns( - Promise.resolve({ - 'ISOCountry': 'unknown', - 'ISOCountryGroups': ['nafta', 'waldo'], - 'nafta': true, - 'waldo': true, - 'matchedISOCountryGroups': ['nafta', 'waldo'], - }) - ); - getReplacements().then((replacements) => - expect( - replacements.expandUrlSync( - '?geo=AMP_GEO,country=AMP_GEO(ISOCountry)' - ) - ).to.equal('?geo=nafta%2Cwaldo,country=unknown') - ); + it('should replace UACH platform', () => { + return expandUrlAsync('?sh=UACH(platform)').then((res) => { + expect(res).to.match(/sh=\w?/); }); + }); - it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO with unknown when geo is not available', () => { - env.sandbox.stub(Services, 'geoForDocOrNull').returns(null); - getReplacements().then((replacements) => - expect( - replacements.expandUrlSync( - '?geo=AMP_GEO,country=AMP_GEO(ISOCountry)' - ) - ).to.equal('?geo=unknown,country=unknown') - ); + it('should replace UACH brands', () => { + return expandUrlAsync('?sh=UACH(brands)').then((res) => { + expect(res).to.match(/sh=\w?/); }); + }); - it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO with unknown when geo is unknown', () => { - getReplacements().then((replacements) => - expect( - replacements.expandUrlSync( - '?geo=AMP_GEO,country=AMP_GEO(ISOCountry)' - ) - ).to.equal('?geo=unknown,country=unknown') - ); + it('should replace USER_AGENT', () => { + return expandUrlAsync('?sh=USER_AGENT').then((res) => { + expect(res).to.match(/sh=\w+/); }); + }); - it.configure() - .skipFirefox() - .run('should accept $expressions', () => { - return expandUrlAsync('?href=$CANONICAL_URL').then((res) => { - expect(res).to.equal( - '?href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1' - ); - }); + it('should replace VIEWER with origin', () => { + return getReplacements().then((replacements) => { + env.sandbox + .stub(viewerService, 'getViewerOrigin') + .returns(Promise.resolve('https://www.google.com')); + return replacements.expandUrlAsync('?sh=VIEWER').then((res) => { + expect(res).to.equal('?sh=https%3A%2F%2Fwww.google.com'); }); + }); + }); - it('should ignore unknown substitutions', () => { - return expandUrlAsync('?a=UNKNOWN').then((res) => { - expect(res).to.equal('?a=UNKNOWN'); + it('should replace VIEWER with empty string', () => { + return getReplacements().then((replacements) => { + env.sandbox + .stub(viewerService, 'getViewerOrigin') + .returns(Promise.resolve('')); + return replacements.expandUrlAsync('?sh=VIEWER').then((res) => { + expect(res).to.equal('?sh='); }); }); + }); - it.configure() - .skipFirefox() - .run('should replace several substitutions', () => { - return expandUrlAsync( - '?a=UNKNOWN&href=CANONICAL_URL&title=TITLE' - ).then((res) => { - expect(res).to.equal( - '?a=UNKNOWN' + - '&href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1' + - '&title=Pixel%20Test' - ); - }); - }); + it('should replace TOTAL_ENGAGED_TIME', () => { + return expandUrlAsync( + '?sh=TOTAL_ENGAGED_TIME', + /*opt_bindings*/ undefined, + {withActivity: true} + ).then((res) => { + expect(res).to.match(/sh=\d+/); + }); + }); - it('should replace new substitutions', () => { - return getReplacements().then((replacements) => { - replacements.getVariableSource().set('ONE', () => 'a'); - expect(replacements.expandUrlAsync('?a=ONE')).to.eventually.equal( - '?a=a' - ); - replacements.getVariableSource().set('ONE', () => 'b'); - replacements.getVariableSource().set('TWO', () => 'b'); - return expect( - replacements.expandUrlAsync('?a=ONE&b=TWO') - ).to.eventually.equal('?a=b&b=b'); - }); + it('should replace INCREMENTAL_ENGAGED_TIME', () => { + return expandUrlAsync( + '?sh=INCREMENTAL_ENGAGED_TIME', + /*opt_bindings*/ undefined, + {withActivity: true} + ).then((res) => { + expect(res).to.match(/sh=\d+/); }); + }); - // TODO(#16916): Make this test work with synchronous throws. - it.skip('should report errors & replace them with empty string (sync)', () => { - const clock = env.sandbox.useFakeTimers(); - const {documentElement} = window.document; - const replacements = Services.urlReplacementsForDoc(documentElement); - replacements.getVariableSource().set('ONE', () => { - throw new Error('boom'); - }); - const p = expect( - replacements.expandUrlAsync('?a=ONE') - ).to.eventually.equal('?a='); - allowConsoleError(() => { - expect(() => { - clock.tick(1); - }).to.throw(/boom/); - }); - return p; + it('should replace AMP_VERSION', () => { + return expandUrlAsync('?sh=AMP_VERSION').then((res) => { + expect(res).to.equal('?sh=%24internalRuntimeVersion%24'); }); + }); - // TODO(#16916): Make this test work with synchronous throws. - it.skip('should report errors & replace them with empty string (promise)', () => { - const clock = env.sandbox.useFakeTimers(); - const {documentElement} = window.document; - const replacements = Services.urlReplacementsForDoc(documentElement); - replacements.getVariableSource().set('ONE', () => { - return Promise.reject(new Error('boom')); + it('should replace FRAGMENT_PARAM with 2', () => { + const win = getFakeWindow(); + win.location = {originalHash: '#margarine=1&ice=2&cream=3'}; + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=FRAGMENT_PARAM(ice)&s') + .then((res) => { + expect(res).to.equal('?sh=2&s'); }); - return expect(replacements.expandUrlAsync('?a=ONE')) - .to.eventually.equal('?a=') - .then(() => { - allowConsoleError(() => { - expect(() => { - clock.tick(1); - }).to.throw(/boom/); - }); - }); + }); + + it('should async replace AMP_GEO(ISOCountry) and AMP_GEO', () => { + env.sandbox.stub(Services, 'geoForDocOrNull').returns( + Promise.resolve({ + 'ISOCountry': 'unknown', + 'ISOCountryGroups': ['nafta', 'waldo'], + 'nafta': true, + 'waldo': true, + 'matchedISOCountryGroups': ['nafta', 'waldo'], + }) + ); + return expandUrlAsync('?geo=AMP_GEO,country=AMP_GEO(ISOCountry)').then( + (res) => { + expect(res).to.equal('?geo=nafta%2Cwaldo,country=unknown'); + } + ); + }); + + it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO', () => { + env.sandbox.stub(Services, 'geoForDocOrNull').returns( + Promise.resolve({ + 'ISOCountry': 'unknown', + 'ISOCountryGroups': ['nafta', 'waldo'], + 'nafta': true, + 'waldo': true, + 'matchedISOCountryGroups': ['nafta', 'waldo'], + }) + ); + getReplacements().then((replacements) => + expect( + replacements.expandUrlSync('?geo=AMP_GEO,country=AMP_GEO(ISOCountry)') + ).to.equal('?geo=nafta%2Cwaldo,country=unknown') + ); + }); + + it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO with unknown when geo is not available', () => { + env.sandbox.stub(Services, 'geoForDocOrNull').returns(null); + getReplacements().then((replacements) => + expect( + replacements.expandUrlSync('?geo=AMP_GEO,country=AMP_GEO(ISOCountry)') + ).to.equal('?geo=unknown,country=unknown') + ); + }); + + it('should sync replace AMP_GEO(ISOCountry) and AMP_GEO with unknown when geo is unknown', () => { + getReplacements().then((replacements) => + expect( + replacements.expandUrlSync('?geo=AMP_GEO,country=AMP_GEO(ISOCountry)') + ).to.equal('?geo=unknown,country=unknown') + ); + }); + + it('should accept $expressions', () => { + return expandUrlAsync('?href=$CANONICAL_URL').then((res) => { + expect(res).to.equal('?href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1'); }); + }); - it('should support positional arguments', () => { - return getReplacements().then((replacements) => { - replacements.getVariableSource().set('FN', (one) => one); - return expect( - replacements.expandUrlAsync('?a=FN(xyz1)') - ).to.eventually.equal('?a=xyz1'); - }); + it('should ignore unknown substitutions', () => { + return expandUrlAsync('?a=UNKNOWN').then((res) => { + expect(res).to.equal('?a=UNKNOWN'); }); + }); - it('should support multiple positional arguments', () => { - return getReplacements().then((replacements) => { - replacements.getVariableSource().set('FN', (one, two) => { - return one + '-' + two; - }); - return expect( - replacements.expandUrlAsync('?a=FN(xyz,abc)') - ).to.eventually.equal('?a=xyz-abc'); - }); + it('should replace several substitutions', () => { + return expandUrlAsync('?a=UNKNOWN&href=CANONICAL_URL&title=TITLE').then( + (res) => { + expect(res).to.equal( + '?a=UNKNOWN' + + '&href=https%3A%2F%2Fpinterest.com%3A8080%2Fpin1' + + '&title=Pixel%20Test' + ); + } + ); + }); + + it('should replace new substitutions', () => { + return getReplacements().then((replacements) => { + replacements.getVariableSource().set('ONE', () => 'a'); + expect(replacements.expandUrlAsync('?a=ONE')).to.eventually.equal( + '?a=a' + ); + replacements.getVariableSource().set('ONE', () => 'b'); + replacements.getVariableSource().set('TWO', () => 'b'); + return expect( + replacements.expandUrlAsync('?a=ONE&b=TWO') + ).to.eventually.equal('?a=b&b=b'); }); + }); - it('should support multiple positional arguments with dots', () => { - return getReplacements().then((replacements) => { - replacements.getVariableSource().set('FN', (one, two) => { - return one + '-' + two; + it.skip('should report errors & replace them with empty string (sync)', () => { + const clock = env.sandbox.useFakeTimers(); + const {documentElement} = window.document; + const replacements = Services.urlReplacementsForDoc(documentElement); + replacements.getVariableSource().set('ONE', () => { + throw new Error('boom'); + }); + const p = expect( + replacements.expandUrlAsync('?a=ONE') + ).to.eventually.equal('?a='); + allowConsoleError(() => { + expect(() => { + clock.tick(1); + }).to.throw(/boom/); + }); + return p; + }); + + it.skip('should report errors & replace them with empty string (promise)', () => { + const clock = env.sandbox.useFakeTimers(); + const {documentElement} = window.document; + const replacements = Services.urlReplacementsForDoc(documentElement); + replacements.getVariableSource().set('ONE', () => { + return Promise.reject(new Error('boom')); + }); + return expect(replacements.expandUrlAsync('?a=ONE')) + .to.eventually.equal('?a=') + .then(() => { + allowConsoleError(() => { + expect(() => { + clock.tick(1); + }).to.throw(/boom/); }); - return expect( - replacements.expandUrlAsync('?a=FN(xy.z,ab.c)') - ).to.eventually.equal('?a=xy.z-ab.c'); }); + }); + + it('should support positional arguments', () => { + return getReplacements().then((replacements) => { + replacements.getVariableSource().set('FN', (one) => one); + return expect( + replacements.expandUrlAsync('?a=FN(xyz1)') + ).to.eventually.equal('?a=xyz1'); }); + }); - it('should support promises as replacements', () => { - return getReplacements().then((replacements) => { - replacements - .getVariableSource() - .set('P1', () => Promise.resolve('abc ')); - replacements - .getVariableSource() - .set('P2', () => Promise.resolve('xyz')); - replacements - .getVariableSource() - .set('P3', () => Promise.resolve('123')); - replacements.getVariableSource().set('OTHER', () => 'foo'); - return expect( - replacements.expandUrlAsync('?a=P1&b=P2&c=P3&d=OTHER') - ).to.eventually.equal('?a=abc%20&b=xyz&c=123&d=foo'); + it('should support multiple positional arguments', () => { + return getReplacements().then((replacements) => { + replacements.getVariableSource().set('FN', (one, two) => { + return one + '-' + two; }); + return expect( + replacements.expandUrlAsync('?a=FN(xyz,abc)') + ).to.eventually.equal('?a=xyz-abc'); }); + }); - it('should override an existing binding', () => { - return expandUrlAsync('ord=RANDOM?', {'RANDOM': 'abc'}).then((res) => { - expect(res).to.match(/ord=abc\?$/); + it('should support multiple positional arguments with dots', () => { + return getReplacements().then((replacements) => { + replacements.getVariableSource().set('FN', (one, two) => { + return one + '-' + two; }); + return expect( + replacements.expandUrlAsync('?a=FN(xy.z,ab.c)') + ).to.eventually.equal('?a=xy.z-ab.c'); }); + }); - it('should add an additional binding', () => { - return expandUrlAsync('rid=NONSTANDARD?', {'NONSTANDARD': 'abc'}).then( - (res) => { - expect(res).to.match(/rid=abc\?$/); - } - ); + it('should support promises as replacements', () => { + return getReplacements().then((replacements) => { + replacements + .getVariableSource() + .set('P1', () => Promise.resolve('abc ')); + replacements + .getVariableSource() + .set('P2', () => Promise.resolve('xyz')); + replacements + .getVariableSource() + .set('P3', () => Promise.resolve('123')); + replacements.getVariableSource().set('OTHER', () => 'foo'); + return expect( + replacements.expandUrlAsync('?a=P1&b=P2&c=P3&d=OTHER') + ).to.eventually.equal('?a=abc%20&b=xyz&c=123&d=foo'); }); + }); - it('should NOT overwrite the cached expression with new bindings', () => { - return expandUrlAsync('rid=NONSTANDARD?', {'NONSTANDARD': 'abc'}).then( - (res) => { - expect(res).to.match(/rid=abc\?$/); - return expandUrlAsync('rid=NONSTANDARD?').then((res) => { - expect(res).to.match(/rid=NONSTANDARD\?$/); - }); - } - ); + it('should override an existing binding', () => { + return expandUrlAsync('ord=RANDOM?', {'RANDOM': 'abc'}).then((res) => { + expect(res).to.match(/ord=abc\?$/); }); + }); - it('should expand bindings as functions', () => { - return expandUrlAsync('rid=FUNC(abc)?', { - 'FUNC': (value) => 'func_' + value, - }).then((res) => { - expect(res).to.match(/rid=func_abc\?$/); - }); - }); + it('should add an additional binding', () => { + return expandUrlAsync('rid=NONSTANDARD?', {'NONSTANDARD': 'abc'}).then( + (res) => { + expect(res).to.match(/rid=abc\?$/); + } + ); + }); - it('should expand bindings as functions with promise', () => { - return expandUrlAsync('rid=FUNC(abc)?', { - 'FUNC': (value) => Promise.resolve('func_' + value), - }).then((res) => { - expect(res).to.match(/rid=func_abc\?$/); - }); - }); + it('should NOT overwrite the cached expression with new bindings', () => { + return expandUrlAsync('rid=NONSTANDARD?', {'NONSTANDARD': 'abc'}).then( + (res) => { + expect(res).to.match(/rid=abc\?$/); + return expandUrlAsync('rid=NONSTANDARD?').then((res) => { + expect(res).to.match(/rid=NONSTANDARD\?$/); + }); + } + ); + }); - it('should expand null as empty string', () => { - return expandUrlAsync('v=VALUE', {'VALUE': null}).then((res) => { - expect(res).to.equal('v='); - }); + it('should expand bindings as functions', () => { + return expandUrlAsync('rid=FUNC(abc)?', { + 'FUNC': (value) => 'func_' + value, + }).then((res) => { + expect(res).to.match(/rid=func_abc\?$/); }); + }); - it('should expand undefined as empty string', () => { - return expandUrlAsync('v=VALUE', {'VALUE': undefined}).then((res) => { - expect(res).to.equal('v='); - }); + it('should expand bindings as functions with promise', () => { + return expandUrlAsync('rid=FUNC(abc)?', { + 'FUNC': (value) => Promise.resolve('func_' + value), + }).then((res) => { + expect(res).to.match(/rid=func_abc\?$/); }); + }); - it('should expand empty string as empty string', () => { - return expandUrlAsync('v=VALUE', {'VALUE': ''}).then((res) => { - expect(res).to.equal('v='); - }); + it('should expand null as empty string', () => { + return expandUrlAsync('v=VALUE', {'VALUE': null}).then((res) => { + expect(res).to.equal('v='); }); + }); - it('should expand zero as zero', () => { - return expandUrlAsync('v=VALUE', {'VALUE': 0}).then((res) => { - expect(res).to.equal('v=0'); - }); + it('should expand undefined as empty string', () => { + return expandUrlAsync('v=VALUE', {'VALUE': undefined}).then((res) => { + expect(res).to.equal('v='); }); + }); - it('should expand false as false', () => { - return expandUrlAsync('v=VALUE', {'VALUE': false}).then((res) => { - expect(res).to.equal('v=false'); - }); + it('should expand empty string as empty string', () => { + return expandUrlAsync('v=VALUE', {'VALUE': ''}).then((res) => { + expect(res).to.equal('v='); }); + }); - it('should resolve sub-included bindings', () => { - // RANDOM is a standard property and we add RANDOM_OTHER. - return expandUrlAsync('r=RANDOM&ro=RANDOM_OTHER?', { - 'RANDOM_OTHER': 'ABC', - }).then((res) => { - expect(res).to.match(/r=(\d+(\.\d+)?)&ro=ABC\?$/); - }); + it('should expand zero as zero', () => { + return expandUrlAsync('v=VALUE', {'VALUE': 0}).then((res) => { + expect(res).to.equal('v=0'); }); + }); - it('should expand multiple vars', () => { - return expandUrlAsync('a=VALUEA&b=VALUEB?', { - 'VALUEA': 'aaa', - 'VALUEB': 'bbb', - }).then((res) => { - expect(res).to.match(/a=aaa&b=bbb\?$/); - }); + it('should expand false as false', () => { + return expandUrlAsync('v=VALUE', {'VALUE': false}).then((res) => { + expect(res).to.equal('v=false'); }); + }); - describe('QUERY_PARAM', () => { - it('should replace QUERY_PARAM with foo', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://example.com?query_string_param1=wrong' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated( - 'https://example.com?query_string_param1=foo' - ); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=QUERY_PARAM(query_string_param1)&s') - .then((res) => { - expect(res).to.match(/sh=foo&s/); - }); - }); - - it('should replace QUERY_PARAM with ""', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://example.com'); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return Promise.resolve(); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=QUERY_PARAM(query_string_param1)&s') - .then((res) => { - expect(res).to.match(/sh=&s/); - }); - }); + it('should resolve sub-included bindings', () => { + // RANDOM is a standard property and we add RANDOM_OTHER. + return expandUrlAsync('r=RANDOM&ro=RANDOM_OTHER?', { + 'RANDOM_OTHER': 'ABC', + }).then((res) => { + expect(res).to.match(/r=(\d+(\.\d+)?)&ro=ABC\?$/); + }); + }); - it('should replace QUERY_PARAM with default_value', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://example.com'); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return Promise.resolve(); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync( - '?sh=QUERY_PARAM(query_string_param1,default_value)&s' - ) - .then((res) => { - expect(res).to.match(/sh=default_value&s/); - }); - }); + it('should expand multiple vars', () => { + return expandUrlAsync('a=VALUEA&b=VALUEB?', { + 'VALUEA': 'aaa', + 'VALUEB': 'bbb', + }).then((res) => { + expect(res).to.match(/a=aaa&b=bbb\?$/); + }); + }); - it('should replace QUERY_PARAM with extra param', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?x=wrong' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=x%3Dfoo' - ); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=QUERY_PARAM(x)&s') - .then((res) => { - expect(res).to.match(/sh=foo&s/); + describe('QUERY_PARAM', () => { + it('should replace QUERY_PARAM with foo', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://example.com?query_string_param1=wrong' + ); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated( + 'https://example.com?query_string_param1=foo' + ); + resolve(); }); - }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=QUERY_PARAM(query_string_param1)&s') + .then((res) => { + expect(res).to.match(/sh=foo&s/); + }); + }); - it('should replace QUERY_PARAM, preferring original over extra', () => { - const win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?x=wrong' - ); - env.sandbox - .stub(trackPromise, 'getTrackImpressionPromise') - .callsFake(() => { - return new Promise((resolve) => { - win.location = parseUrlDeprecated( - 'https://cdn.ampproject.org/a/o.com/foo/?x=foo&_r=x%3Devil' - ); - resolve(); - }); - }); - return Services.urlReplacementsForDoc(win.document.documentElement) - .expandUrlAsync('?sh=QUERY_PARAM(x)&s') - .then((res) => { - expect(res).to.match(/sh=foo&s/); - }); - }); + it('should replace QUERY_PARAM with ""', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated('https://example.com'); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return Promise.resolve(); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=QUERY_PARAM(query_string_param1)&s') + .then((res) => { + expect(res).to.match(/sh=&s/); + }); }); - it('should collect vars', () => { + it('should replace QUERY_PARAM with default_value', () => { const win = getFakeWindow(); - win.location = parseUrlDeprecated('https://example.com?p1=foo'); + win.location = parseUrlDeprecated('https://example.com'); env.sandbox .stub(trackPromise, 'getTrackImpressionPromise') .callsFake(() => { return Promise.resolve(); }); return Services.urlReplacementsForDoc(win.document.documentElement) - .collectVars('?SOURCE_HOST&QUERY_PARAM(p1)&SIMPLE&FUNC&PROMISE', { - 'SIMPLE': 21, - 'FUNC': () => 22, - 'PROMISE': () => Promise.resolve(23), - }) + .expandUrlAsync( + '?sh=QUERY_PARAM(query_string_param1,default_value)&s' + ) .then((res) => { - expect(res).to.deep.equal({ - 'SOURCE_HOST': 'example.com', - 'QUERY_PARAM(p1)': 'foo', - 'SIMPLE': 21, - 'FUNC': 22, - 'PROMISE': 23, - }); + expect(res).to.match(/sh=default_value&s/); }); }); - it('should collect unallowlisted vars', () => { + it('should replace QUERY_PARAM with extra param', () => { const win = getFakeWindow(); win.location = parseUrlDeprecated( - 'https://example.com/base?foo=bar&bar=abc&gclid=123' + 'https://cdn.ampproject.org/a/o.com/foo/?x=wrong' ); - const element = document.createElement('amp-foo'); - element.setAttribute('src', '?SOURCE_HOST&QUERY_PARAM(p1)&COUNTER'); - element.setAttribute('data-amp-replace', 'QUERY_PARAM'); - const {documentElement} = win.document; - const urlReplacements = Services.urlReplacementsForDoc(documentElement); - const unallowlisted = - urlReplacements.collectDisallowedVarsSync(element); - expect(unallowlisted).to.deep.equal(['SOURCE_HOST', 'COUNTER']); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?amp_r=x%3Dfoo' + ); + resolve(); + }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=QUERY_PARAM(x)&s') + .then((res) => { + expect(res).to.match(/sh=foo&s/); + }); }); - it('should reject javascript protocol', () => { - const protocolErrorRegex = /invalid protocol/; - expectAsyncConsoleError(protocolErrorRegex); + it('should replace QUERY_PARAM, preferring original over extra', () => { const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = Services.urlReplacementsForDoc(documentElement); - /*eslint no-script-url: 0*/ - return urlReplacements - .expandUrlAsync('javascript://example.com/?r=RANDOM') - .then( - () => { - throw new Error('never here'); - }, - (err) => { - expect(err.message).to.match(protocolErrorRegex); - } - ); + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?x=wrong' + ); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return new Promise((resolve) => { + win.location = parseUrlDeprecated( + 'https://cdn.ampproject.org/a/o.com/foo/?x=foo&_r=x%3Devil' + ); + resolve(); + }); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .expandUrlAsync('?sh=QUERY_PARAM(x)&s') + .then((res) => { + expect(res).to.match(/sh=foo&s/); + }); }); + }); - describe('sync expansion', () => { - it('should expand w/ collect vars (skip async macro)', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - urlReplacements.ampdoc.win.performance.timing.loadEventStart = 109; - const expanded = urlReplacements.expandUrlSync( - 'r=RANDOM&c=CONST&f=FUNCT(hello,world)&a=b&d=PROM&e=PAGE_LOAD_TIME', - { - 'CONST': 'ABC', - 'FUNCT': function (a, b) { - return a + b; - }, - // Will ignore promise based result and instead insert empty string. - 'PROM': function () { - return Promise.resolve('boo'); - }, - } - ); - expect(expanded).to.match( - /^r=\d(\.\d+)?&c=ABC&f=helloworld&a=b&d=&e=9$/ - ); + it('should collect vars', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated('https://example.com?p1=foo'); + env.sandbox + .stub(trackPromise, 'getTrackImpressionPromise') + .callsFake(() => { + return Promise.resolve(); + }); + return Services.urlReplacementsForDoc(win.document.documentElement) + .collectVars('?SOURCE_HOST&QUERY_PARAM(p1)&SIMPLE&FUNC&PROMISE', { + 'SIMPLE': 21, + 'FUNC': () => 22, + 'PROMISE': () => Promise.resolve(23), + }) + .then((res) => { + expect(res).to.deep.equal({ + 'SOURCE_HOST': 'example.com', + 'QUERY_PARAM(p1)': 'foo', + 'SIMPLE': 21, + 'FUNC': 22, + 'PROMISE': 23, + }); }); + }); - it('should reject protocol changes', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - let expanded = urlReplacements.expandUrlSync( - 'PROTOCOL://example.com/?r=RANDOM', - { - 'PROTOCOL': 'abc', - } - ); - expect(expanded).to.equal('PROTOCOL://example.com/?r=RANDOM'); - expanded = urlReplacements.expandUrlSync( - 'FUNCT://example.com/?r=RANDOM', - { - 'FUNCT': function () { - return 'abc'; - }, - } - ); - expect(expanded).to.equal('FUNCT://example.com/?r=RANDOM'); - }); + it('should collect unallowlisted vars', () => { + const win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://example.com/base?foo=bar&bar=abc&gclid=123' + ); + const element = document.createElement('amp-foo'); + element.setAttribute('src', '?SOURCE_HOST&QUERY_PARAM(p1)&COUNTER'); + element.setAttribute('data-amp-replace', 'QUERY_PARAM'); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const unallowlisted = urlReplacements.collectDisallowedVarsSync(element); + expect(unallowlisted).to.deep.equal(['SOURCE_HOST', 'COUNTER']); + }); - it('should reject javascript protocol', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - allowConsoleError(() => { - expect(() => { - /*eslint no-script-url: 0*/ - urlReplacements.expandUrlSync( - 'javascript://example.com/?r=RANDOM' - ); - }).to.throw('invalid protocol'); - }); - }); - }); + it('should reject javascript protocol', () => { + const protocolErrorRegex = /invalid protocol/; + expectAsyncConsoleError(protocolErrorRegex); + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + /*eslint no-script-url: 0*/ + return urlReplacements + .expandUrlAsync('javascript://example.com/?r=RANDOM') + .then( + () => { + throw new Error('never here'); + }, + (err) => { + expect(err.message).to.match(protocolErrorRegex); + } + ); + }); - it('should expand sync and respect allowlisted', () => { + describe('sync expansion', () => { + it('should expand w/ collect vars (skip async macro)', () => { const win = getFakeWindow(); const {documentElement} = win.document; const urlReplacements = Services.urlReplacementsForDoc(documentElement); + urlReplacements.ampdoc.win.performance.timing.loadEventStart = 109; const expanded = urlReplacements.expandUrlSync( 'r=RANDOM&c=CONST&f=FUNCT(hello,world)&a=b&d=PROM&e=PAGE_LOAD_TIME', { 'CONST': 'ABC', - 'FUNCT': () => { - throw Error('Should not be called'); + 'FUNCT': function (a, b) { + return a + b; }, - }, + // Will ignore promise based result and instead insert empty string. + 'PROM': function () { + return Promise.resolve('boo'); + }, + } + ); + expect(expanded).to.match( + /^r=\d(\.\d+)?&c=ABC&f=helloworld&a=b&d=&e=9$/ + ); + }); + + it('should reject protocol changes', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + let expanded = urlReplacements.expandUrlSync( + 'PROTOCOL://example.com/?r=RANDOM', { - 'CONST': true, + 'PROTOCOL': 'abc', } ); - expect(expanded).to.equal( - 'r=RANDOM&c=ABC&f=FUNCT(hello,world)&a=b&d=PROM&e=PAGE_LOAD_TIME' + expect(expanded).to.equal('PROTOCOL://example.com/?r=RANDOM'); + expanded = urlReplacements.expandUrlSync( + 'FUNCT://example.com/?r=RANDOM', + { + 'FUNCT': function () { + return 'abc'; + }, + } ); + expect(expanded).to.equal('FUNCT://example.com/?r=RANDOM'); }); - describe('access values via amp-access', () => { - let accessService; - let accessServiceMock; - - beforeEach(() => { - accessService = { - getAccessReaderId: () => {}, - getAuthdataField: () => {}, - }; - accessServiceMock = env.sandbox.mock(accessService); - env.sandbox - .stub(Services, 'accessServiceForDocOrNull') - .callsFake(() => { - return Promise.resolve(accessService); - }); + it('should reject javascript protocol', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + allowConsoleError(() => { + expect(() => { + /*eslint no-script-url: 0*/ + urlReplacements.expandUrlSync('javascript://example.com/?r=RANDOM'); + }).to.throw('invalid protocol'); }); + }); + }); - afterEach(() => { - accessServiceMock.verify(); + it('should expand sync and respect allowlisted', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const expanded = urlReplacements.expandUrlSync( + 'r=RANDOM&c=CONST&f=FUNCT(hello,world)&a=b&d=PROM&e=PAGE_LOAD_TIME', + { + 'CONST': 'ABC', + 'FUNCT': () => { + throw Error('Should not be called'); + }, + }, + { + 'CONST': true, + } + ); + expect(expanded).to.equal( + 'r=RANDOM&c=ABC&f=FUNCT(hello,world)&a=b&d=PROM&e=PAGE_LOAD_TIME' + ); + }); + + describe('access values via amp-access', () => { + let accessService; + let accessServiceMock; + + beforeEach(() => { + accessService = { + getAccessReaderId: () => {}, + getAuthdataField: () => {}, + }; + accessServiceMock = env.sandbox.mock(accessService); + env.sandbox + .stub(Services, 'accessServiceForDocOrNull') + .callsFake(() => { + return Promise.resolve(accessService); + }); + }); + + afterEach(() => { + accessServiceMock.verify(); + }); + + function expandUrlAsync(url, opt_disabled) { + if (opt_disabled) { + accessService = null; + } + return createIframePromise().then((iframe) => { + iframe.doc.title = 'Pixel Test'; + const link = iframe.doc.createElement('link'); + link.setAttribute('href', 'https://pinterest.com/pin1'); + link.setAttribute('rel', 'canonical'); + iframe.doc.head.appendChild(link); + const {documentElement} = iframe.doc; + Services.ampdoc(documentElement).setExtensionsKnown(); + const replacements = Services.urlReplacementsForDoc(documentElement); + return replacements.expandUrlAsync(url); }); + } - function expandUrlAsync(url, opt_disabled) { - if (opt_disabled) { - accessService = null; - } - return createIframePromise().then((iframe) => { - iframe.doc.title = 'Pixel Test'; - const link = iframe.doc.createElement('link'); - link.setAttribute('href', 'https://pinterest.com/pin1'); - link.setAttribute('rel', 'canonical'); - iframe.doc.head.appendChild(link); - const {documentElement} = iframe.doc; - Services.ampdoc(documentElement).setExtensionsKnown(); - const replacements = - Services.urlReplacementsForDoc(documentElement); - return replacements.expandUrlAsync(url); - }); - } - - it('should replace ACCESS_READER_ID', () => { - accessServiceMock - .expects('getAccessReaderId') - .returns(Promise.resolve('reader1')) - .once(); - return expandUrlAsync('?a=ACCESS_READER_ID').then((res) => { - expect(res).to.match(/a=reader1/); - expect(userErrorStub).to.have.not.been.called; - }); + it('should replace ACCESS_READER_ID', () => { + accessServiceMock + .expects('getAccessReaderId') + .returns(Promise.resolve('reader1')) + .once(); + return expandUrlAsync('?a=ACCESS_READER_ID').then((res) => { + expect(res).to.match(/a=reader1/); + expect(userErrorStub).to.have.not.been.called; }); + }); - it('should replace AUTHDATA', () => { - accessServiceMock - .expects('getAuthdataField') - .withExactArgs('field1') - .returns(Promise.resolve('value1')) - .once(); - return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { - expect(res).to.match(/a=value1/); - expect(userErrorStub).to.have.not.been.called; - }); + it('should replace AUTHDATA', () => { + accessServiceMock + .expects('getAuthdataField') + .withExactArgs('field1') + .returns(Promise.resolve('value1')) + .once(); + return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { + expect(res).to.match(/a=value1/); + expect(userErrorStub).to.have.not.been.called; }); + }); - it('should report error if not available', () => { - accessServiceMock.expects('getAccessReaderId').never(); - return expandUrlAsync( - '?a=ACCESS_READER_ID;', - /* disabled */ true - ).then((res) => { + it('should report error if not available', () => { + accessServiceMock.expects('getAccessReaderId').never(); + return expandUrlAsync('?a=ACCESS_READER_ID;', /* disabled */ true).then( + (res) => { expect(res).to.match(/a=;/); expect(userErrorStub).to.be.calledOnce; - }); - }); + } + ); }); + }); - describe('access values via amp-subscriptions', () => { - let subscriptionsService; - let subscriptionsServiceMock; - - beforeEach(() => { - subscriptionsService = { - getAccessReaderId: () => {}, - getAuthdataField: () => {}, - }; - subscriptionsServiceMock = env.sandbox.mock(subscriptionsService); - env.sandbox - .stub(Services, 'subscriptionsServiceForDocOrNull') - .callsFake(() => { - return Promise.resolve(subscriptionsService); - }); - }); - - afterEach(() => { - subscriptionsServiceMock.verify(); - }); + describe('access values via amp-subscriptions', () => { + let subscriptionsService; + let subscriptionsServiceMock; - function expandUrlAsync(url, opt_disabled) { - if (opt_disabled) { - subscriptionsService = null; - } - return createIframePromise().then((iframe) => { - iframe.doc.title = 'Pixel Test'; - const link = iframe.doc.createElement('link'); - link.setAttribute('href', 'https://pinterest.com/pin1'); - link.setAttribute('rel', 'canonical'); - iframe.doc.head.appendChild(link); - const {documentElement} = iframe.doc; - Services.ampdoc(documentElement).setExtensionsKnown(); - const replacements = - Services.urlReplacementsForDoc(documentElement); - return replacements.expandUrlAsync(url); + beforeEach(() => { + subscriptionsService = { + getAccessReaderId: () => {}, + getAuthdataField: () => {}, + }; + subscriptionsServiceMock = env.sandbox.mock(subscriptionsService); + env.sandbox + .stub(Services, 'subscriptionsServiceForDocOrNull') + .callsFake(() => { + return Promise.resolve(subscriptionsService); }); + }); + + afterEach(() => { + subscriptionsServiceMock.verify(); + }); + + function expandUrlAsync(url, opt_disabled) { + if (opt_disabled) { + subscriptionsService = null; } + return createIframePromise().then((iframe) => { + iframe.doc.title = 'Pixel Test'; + const link = iframe.doc.createElement('link'); + link.setAttribute('href', 'https://pinterest.com/pin1'); + link.setAttribute('rel', 'canonical'); + iframe.doc.head.appendChild(link); + const {documentElement} = iframe.doc; + Services.ampdoc(documentElement).setExtensionsKnown(); + const replacements = Services.urlReplacementsForDoc(documentElement); + return replacements.expandUrlAsync(url); + }); + } - it('should replace ACCESS_READER_ID', () => { - subscriptionsServiceMock - .expects('getAccessReaderId') - .returns(Promise.resolve('reader1')) - .once(); - return expandUrlAsync('?a=ACCESS_READER_ID').then((res) => { - expect(res).to.match(/a=reader1/); - expect(userErrorStub).to.have.not.been.called; - }); + it('should replace ACCESS_READER_ID', () => { + subscriptionsServiceMock + .expects('getAccessReaderId') + .returns(Promise.resolve('reader1')) + .once(); + return expandUrlAsync('?a=ACCESS_READER_ID').then((res) => { + expect(res).to.match(/a=reader1/); + expect(userErrorStub).to.have.not.been.called; }); + }); - it('should replace AUTHDATA', () => { - subscriptionsServiceMock - .expects('getAuthdataField') - .withExactArgs('field1') - .returns(Promise.resolve('value1')) - .once(); - return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { - expect(res).to.match(/a=value1/); - expect(userErrorStub).to.have.not.been.called; - }); + it('should replace AUTHDATA', () => { + subscriptionsServiceMock + .expects('getAuthdataField') + .withExactArgs('field1') + .returns(Promise.resolve('value1')) + .once(); + return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { + expect(res).to.match(/a=value1/); + expect(userErrorStub).to.have.not.been.called; }); + }); - it('should report error if not available', () => { - subscriptionsServiceMock.expects('getAccessReaderId').never(); - return expandUrlAsync( - '?a=ACCESS_READER_ID;', - /* disabled */ true - ).then((res) => { + it('should report error if not available', () => { + subscriptionsServiceMock.expects('getAccessReaderId').never(); + return expandUrlAsync('?a=ACCESS_READER_ID;', /* disabled */ true).then( + (res) => { expect(res).to.match(/a=;/); expect(userErrorStub).to.be.calledOnce; - }); - }); + } + ); + }); - it('should prefer amp-subscriptions if amp-access also available', () => { - const accessService = { - getAccessReaderId: () => {}, - getAuthdataField: () => {}, - }; - const accessServiceMock = env.sandbox.mock(accessService); - env.sandbox - .stub(Services, 'accessServiceForDocOrNull') - .callsFake(() => { - return Promise.resolve(accessService); - }); - accessServiceMock.expects('getAuthdataField').never(); - - subscriptionsServiceMock - .expects('getAuthdataField') - .withExactArgs('field1') - .returns(Promise.resolve('value1')) - .once(); - return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { - expect(res).to.match(/a=value1/); - expect(userErrorStub).to.have.not.been.called; - accessServiceMock.verify(); + it('should prefer amp-subscriptions if amp-access also available', () => { + const accessService = { + getAccessReaderId: () => {}, + getAuthdataField: () => {}, + }; + const accessServiceMock = env.sandbox.mock(accessService); + env.sandbox + .stub(Services, 'accessServiceForDocOrNull') + .callsFake(() => { + return Promise.resolve(accessService); }); + accessServiceMock.expects('getAuthdataField').never(); + + subscriptionsServiceMock + .expects('getAuthdataField') + .withExactArgs('field1') + .returns(Promise.resolve('value1')) + .once(); + return expandUrlAsync('?a=AUTHDATA(field1)').then((res) => { + expect(res).to.match(/a=value1/); + expect(userErrorStub).to.have.not.been.called; + accessServiceMock.verify(); }); }); + }); - describe('link expansion', () => { - let urlReplacements; - let a; - let win; + describe('link expansion', () => { + let urlReplacements; + let a; + let win; - beforeEach(() => { - a = document.createElement('a'); - win = getFakeWindow(); - win.location = parseUrlDeprecated( - 'https://example.com/base?foo=bar&bar=abc&gclid=123' - ); - const {documentElement} = win.document; - urlReplacements = Services.urlReplacementsForDoc(documentElement); - }); + beforeEach(() => { + a = document.createElement('a'); + win = getFakeWindow(); + win.location = parseUrlDeprecated( + 'https://example.com/base?foo=bar&bar=abc&gclid=123' + ); + const {documentElement} = win.document; + urlReplacements = Services.urlReplacementsForDoc(documentElement); + }); - it('should replace href', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link?out=bar'); - }); + it('should replace href', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link?out=bar'); + }); - it('should append default outgoing decoration', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); - expect(a.href).to.equal('https://example.com/link?out=bar&gclid=123'); - }); + it('should append default outgoing decoration', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); + expect(a.href).to.equal('https://example.com/link?out=bar&gclid=123'); + }); - it('should replace href 2x', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link?out=bar'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link?out=bar'); - }); + it('should replace href 2x', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link?out=bar'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link?out=bar'); + }); - it('should replace href 2', () => { - a.href = - 'https://example.com/link?out=QUERY_PARAM(foo)&' + - 'out2=QUERY_PARAM(bar)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link?out=bar&out2=abc'); - }); + it('should replace href 2', () => { + a.href = + 'https://example.com/link?out=QUERY_PARAM(foo)&' + + 'out2=QUERY_PARAM(bar)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link?out=bar&out2=abc'); + }); - it('has nothing to replace', () => { - a.href = 'https://example.com/link'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link'); - }); + it('has nothing to replace', () => { + a.href = 'https://example.com/link'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link'); + }); - it('should not replace without user allowance', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'https://example.com/link?out=QUERY_PARAM(foo)' - ); - }); + it('should not replace without user allowance', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'https://example.com/link?out=QUERY_PARAM(foo)' + ); + }); - it('should not replace without user allowance 2', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'ABC'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'https://example.com/link?out=QUERY_PARAM(foo)' - ); - }); + it('should not replace without user allowance 2', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'ABC'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'https://example.com/link?out=QUERY_PARAM(foo)' + ); + }); - it('should replace default append params regardless of allowlist', () => { - a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; - urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); - expect(a.href).to.equal( - 'https://example.com/link?out=QUERY_PARAM(foo)&gclid=123' - ); - }); + it('should replace default append params regardless of allowlist', () => { + a.href = 'https://example.com/link?out=QUERY_PARAM(foo)'; + urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); + expect(a.href).to.equal( + 'https://example.com/link?out=QUERY_PARAM(foo)&gclid=123' + ); + }); - it('should not replace unallowlisted fields', () => { - a.href = 'https://example.com/link?out=RANDOM'; - a.setAttribute('data-amp-replace', 'RANDOM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example.com/link?out=RANDOM'); - }); + it('should not replace unallowlisted fields', () => { + a.href = 'https://example.com/link?out=RANDOM'; + a.setAttribute('data-amp-replace', 'RANDOM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example.com/link?out=RANDOM'); + }); - it('should replace for http (non-secure) allowlisted origin', () => { - canonical = 'http://example.com/link'; - a.href = 'http://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('http://example.com/link?out=bar'); - }); + it('should replace for http (non-secure) allowlisted origin', () => { + canonical = 'http://example.com/link'; + a.href = 'http://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('http://example.com/link?out=bar'); + }); - it('should replace with canonical origin', () => { - a.href = 'https://canonical.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://canonical.com/link?out=bar'); - }); + it('should replace with canonical origin', () => { + a.href = 'https://canonical.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://canonical.com/link?out=bar'); + }); - it('should replace with allowlisted origin', () => { - a.href = 'https://allowlisted.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://allowlisted.com/link?out=bar'); - }); + it('should replace with allowlisted origin', () => { + a.href = 'https://allowlisted.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://allowlisted.com/link?out=bar'); + }); - it('should not replace to different origin', () => { - a.href = 'https://example2.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + it('should not replace to different origin', () => { + a.href = 'https://example2.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'https://example2.com/link?out=QUERY_PARAM(foo)' + ); + }); + + it('should not append default param to different origin', () => { + a.href = 'https://example2.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); + expect(a.href).to.equal( + 'https://example2.com/link?out=QUERY_PARAM(foo)' + ); + }); + + it('should replace allowlisted fields', () => { + a.href = + 'https://canonical.com/link?' + + 'out=QUERY_PARAM(foo)' + + '&c=PAGE_VIEW_IDCLIENT_ID(abc)NAV_TIMING(navigationStart)'; + a.setAttribute( + 'data-amp-replace', + 'QUERY_PARAM CLIENT_ID PAGE_VIEW_ID NAV_TIMING' + ); + // No replacement without previous async replacement + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://canonical.com/link?out=bar&c=1234100'); + // Get a cid, then proceed. + return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { urlReplacements.maybeExpandLink(a, null); expect(a.href).to.equal( - 'https://example2.com/link?out=QUERY_PARAM(foo)' + 'https://canonical.com/link?out=bar&c=1234test-cid(abc)100' ); }); + }); - it('should not append default param to different origin', () => { - a.href = 'https://example2.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - urlReplacements.maybeExpandLink(a, 'gclid=QUERY_PARAM(gclid)'); - expect(a.href).to.equal( - 'https://example2.com/link?out=QUERY_PARAM(foo)' - ); - }); + it('should add URL parameters for different origin', () => { + a.href = 'https://example2.com/link'; + a.setAttribute('data-amp-addparams', 'guid=123'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal('https://example2.com/link?guid=123'); + }); - it('should replace allowlisted fields', () => { - a.href = - 'https://canonical.com/link?' + - 'out=QUERY_PARAM(foo)' + - '&c=PAGE_VIEW_IDCLIENT_ID(abc)NAV_TIMING(navigationStart)'; - a.setAttribute( - 'data-amp-replace', - 'QUERY_PARAM CLIENT_ID PAGE_VIEW_ID NAV_TIMING' - ); - // No replacement without previous async replacement - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'https://canonical.com/link?out=bar&c=1234100' - ); + it("should add URL parameters for http URL's(non-secure)", () => { + a.href = 'http://allowlisted.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-addparams', 'guid=123'); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'http://allowlisted.com/link?out=QUERY_PARAM(foo)&guid=123' + ); + }); + + it('should concatenate and expand additional params w/ allowlist', () => { + a.href = 'http://example.com/link?first=QUERY_PARAM(src,YYYY)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM'); + a.setAttribute( + 'data-amp-addparams', + 'second=QUERY_PARAM(baz,XXXX)&third=CLIENT_ID(AMP_ECID_GOOGLE,,_ga)&' + + 'fourth=link123' + ); + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'http://example.com/link?first=YYYY&second=XXXX&' + + 'third=CLIENT_ID(AMP_ECID_GOOGLE%2C%2C_ga)&fourth=link123' + ); + }); + + it( + 'should add URL parameters and repalce allowlisted' + + " values for http allowlisted URL's(non-secure)", + () => { + a.href = 'http://example.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'CLIENT_ID'); + a.setAttribute('data-amp-addparams', 'guid=123&c=CLIENT_ID(abc)'); // Get a cid, then proceed. return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { urlReplacements.maybeExpandLink(a, null); expect(a.href).to.equal( - 'https://canonical.com/link?out=bar&c=1234test-cid(abc)100' + 'http://example.com/link?out=QUERY_PARAM(foo)&guid=123&c=test-cid(abc)' ); }); - }); - - it('should add URL parameters for different origin', () => { - a.href = 'https://example2.com/link'; - a.setAttribute('data-amp-addparams', 'guid=123'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal('https://example2.com/link?guid=123'); - }); - - it("should add URL parameters for http URL's(non-secure)", () => { - a.href = 'http://allowlisted.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-addparams', 'guid=123'); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'http://allowlisted.com/link?out=QUERY_PARAM(foo)&guid=123' - ); - }); - - it('should concatenate and expand additional params w/ allowlist', () => { - a.href = 'http://example.com/link?first=QUERY_PARAM(src,YYYY)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM'); - a.setAttribute( - 'data-amp-addparams', - 'second=QUERY_PARAM(baz,XXXX)&third=CLIENT_ID(AMP_ECID_GOOGLE,,_ga)&' + - 'fourth=link123' - ); - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'http://example.com/link?first=YYYY&second=XXXX&' + - 'third=CLIENT_ID(AMP_ECID_GOOGLE%2C%2C_ga)&fourth=link123' - ); - }); - - it( - 'should add URL parameters and repalce allowlisted' + - " values for http allowlisted URL's(non-secure)", - () => { - a.href = 'http://example.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'CLIENT_ID'); - a.setAttribute('data-amp-addparams', 'guid=123&c=CLIENT_ID(abc)'); - // Get a cid, then proceed. - return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'http://example.com/link?out=QUERY_PARAM(foo)&guid=123&c=test-cid(abc)' - ); - }); - } - ); - - it( - 'should add URL parameters and not repalce allowlisted' + - " values for non allowlisted http URL's(non-secure)", - () => { - a.href = 'http://example2.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'CLIENT_ID'); - a.setAttribute('data-amp-addparams', 'guid=123&c=CLIENT_ID(abc)'); - // Get a cid, then proceed. - return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { - urlReplacements.maybeExpandLink(a, null); - expect(a.href).to.equal( - 'http://example2.com/link?out=QUERY_PARAM(foo)&guid=123&c=CLIENT_ID(abc)' - ); - }); - } - ); + } + ); - it('should append query parameters and repalce allowlisted values', () => { - a.href = 'https://allowlisted.com/link?out=QUERY_PARAM(foo)'; - a.setAttribute('data-amp-replace', 'QUERY_PARAM CLIENT_ID'); + it( + 'should add URL parameters and not repalce allowlisted' + + " values for non allowlisted http URL's(non-secure)", + () => { + a.href = 'http://example2.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'CLIENT_ID'); a.setAttribute('data-amp-addparams', 'guid=123&c=CLIENT_ID(abc)'); // Get a cid, then proceed. return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { urlReplacements.maybeExpandLink(a, null); expect(a.href).to.equal( - 'https://allowlisted.com/link?out=bar&guid=123&c=test-cid(abc)' + 'http://example2.com/link?out=QUERY_PARAM(foo)&guid=123&c=CLIENT_ID(abc)' ); }); - }); - }); + } + ); - describe('Expanding String', () => { - it('should not reject protocol changes with expandStringSync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - let expanded = urlReplacements.expandStringSync( - 'PROTOCOL://example.com/?r=RANDOM', - { - 'PROTOCOL': 'abc', - } - ); - expect(expanded).to.match(/abc:\/\/example\.com\/\?r=(\d+(\.\d+)?)$/); - expanded = urlReplacements.expandStringSync( - 'FUNCT://example.com/?r=RANDOM', - { - 'FUNCT': function () { - return 'abc'; - }, - } + it('should append query parameters and repalce allowlisted values', () => { + a.href = 'https://allowlisted.com/link?out=QUERY_PARAM(foo)'; + a.setAttribute('data-amp-replace', 'QUERY_PARAM CLIENT_ID'); + a.setAttribute('data-amp-addparams', 'guid=123&c=CLIENT_ID(abc)'); + // Get a cid, then proceed. + return urlReplacements.expandUrlAsync('CLIENT_ID(abc)').then(() => { + urlReplacements.maybeExpandLink(a, null); + expect(a.href).to.equal( + 'https://allowlisted.com/link?out=bar&guid=123&c=test-cid(abc)' ); - expect(expanded).to.match(/abc:\/\/example\.com\/\?r=(\d+(\.\d+)?)$/); - }); - - it('should not encode values returned by expandStringSync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const expanded = urlReplacements.expandStringSync('title=TITLE', { - 'TITLE': 'test with spaces', - }); - expect(expanded).to.equal('title=test with spaces'); }); + }); + }); - it('should not check protocol changes with expandStringAsync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - return urlReplacements - .expandStringAsync('RANDOM:X:Y', { - 'RANDOM': Promise.resolve('abc'), - }) - .then((expanded) => { - expect(expanded).to.equal('abc:X:Y'); - }); - }); + describe('Expanding String', () => { + it('should not reject protocol changes with expandStringSync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + let expanded = urlReplacements.expandStringSync( + 'PROTOCOL://example.com/?r=RANDOM', + { + 'PROTOCOL': 'abc', + } + ); + expect(expanded).to.match(/abc:\/\/example\.com\/\?r=(\d+(\.\d+)?)$/); + expanded = urlReplacements.expandStringSync( + 'FUNCT://example.com/?r=RANDOM', + { + 'FUNCT': function () { + return 'abc'; + }, + } + ); + expect(expanded).to.match(/abc:\/\/example\.com\/\?r=(\d+(\.\d+)?)$/); + }); - it('should not encode values returned by expandStringAsync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - return urlReplacements - .expandStringAsync('title=TITLE', { - 'TITLE': Promise.resolve('test with spaces'), - }) - .then((expanded) => { - expect(expanded).to.equal('title=test with spaces'); - }); + it('should not encode values returned by expandStringSync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const expanded = urlReplacements.expandStringSync('title=TITLE', { + 'TITLE': 'test with spaces', }); + expect(expanded).to.equal('title=test with spaces'); }); - describe('Expanding Input Value', () => { - it('should fail for non-inputs', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const input = document.createElement('textarea'); - input.value = 'RANDOM'; - input.setAttribute('data-amp-replace', 'RANDOM'); - allowConsoleError(() => { - expect(() => urlReplacements.expandInputValueSync(input)).to.throw( - /Input value expansion only works on hidden input fields/ - ); + it('should not check protocol changes with expandStringAsync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + return urlReplacements + .expandStringAsync('RANDOM:X:Y', { + 'RANDOM': Promise.resolve('abc'), + }) + .then((expanded) => { + expect(expanded).to.equal('abc:X:Y'); }); - expect(input.value).to.equal('RANDOM'); - }); + }); - it('should fail for non-hidden inputs', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const input = document.createElement('input'); - input.value = 'RANDOM'; - input.setAttribute('data-amp-replace', 'RANDOM'); - allowConsoleError(() => { - expect(() => urlReplacements.expandInputValueSync(input)).to.throw( - /Input value expansion only works on hidden input fields/ - ); + it('should not encode values returned by expandStringAsync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + return urlReplacements + .expandStringAsync('title=TITLE', { + 'TITLE': Promise.resolve('test with spaces'), + }) + .then((expanded) => { + expect(expanded).to.equal('title=test with spaces'); }); - expect(input.value).to.equal('RANDOM'); - }); + }); + }); - it('should not replace not allowlisted vars', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const input = document.createElement('input'); - input.value = 'RANDOM'; - input.type = 'hidden'; - input.setAttribute('data-amp-replace', 'CANONICAL_URL'); - let expandedValue = urlReplacements.expandInputValueSync(input); - expect(expandedValue).to.equal('RANDOM'); - input.setAttribute('data-amp-replace', 'CANONICAL_URL RANDOM'); - expandedValue = urlReplacements.expandInputValueSync(input); - expect(expandedValue).to.match(/(\d+(\.\d+)?)/); - expect(input.value).to.match(/(\d+(\.\d+)?)/); - expect(input['amp-original-value']).to.equal('RANDOM'); + describe('Expanding Input Value', () => { + it('should fail for non-inputs', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const input = document.createElement('textarea'); + input.value = 'RANDOM'; + input.setAttribute('data-amp-replace', 'RANDOM'); + allowConsoleError(() => { + expect(() => urlReplacements.expandInputValueSync(input)).to.throw( + /Input value expansion only works on hidden input fields/ + ); }); + expect(input.value).to.equal('RANDOM'); + }); - it('should replace input value with var subs - sync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const input = document.createElement('input'); - input.value = 'RANDOM'; - input.type = 'hidden'; - input.setAttribute('data-amp-replace', 'RANDOM'); - let expandedValue = urlReplacements.expandInputValueSync(input); - expect(expandedValue).to.match(/(\d+(\.\d+)?)/); - - input['amp-original-value'] = 'RANDOM://example.com/RANDOM'; - expandedValue = urlReplacements.expandInputValueSync(input); - expect(expandedValue).to.match( - /(\d+(\.\d+)?):\/\/example\.com\/(\d+(\.\d+)?)$/ - ); - expect(input.value).to.match( - /(\d+(\.\d+)?):\/\/example\.com\/(\d+(\.\d+)?)$/ - ); - expect(input['amp-original-value']).to.equal( - 'RANDOM://example.com/RANDOM' + it('should fail for non-hidden inputs', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const input = document.createElement('input'); + input.value = 'RANDOM'; + input.setAttribute('data-amp-replace', 'RANDOM'); + allowConsoleError(() => { + expect(() => urlReplacements.expandInputValueSync(input)).to.throw( + /Input value expansion only works on hidden input fields/ ); }); + expect(input.value).to.equal('RANDOM'); + }); - it('should replace input value with var subs - sync', () => { - const win = getFakeWindow(); - const {documentElement} = win.document; - const urlReplacements = - Services.urlReplacementsForDoc(documentElement); - const input = document.createElement('input'); - input.value = 'RANDOM'; - input.type = 'hidden'; - input.setAttribute('data-amp-replace', 'RANDOM'); - return urlReplacements - .expandInputValueAsync(input) - .then((expandedValue) => { - expect(input['amp-original-value']).to.equal('RANDOM'); - expect(input.value).to.match(/(\d+(\.\d+)?)/); - expect(expandedValue).to.match(/(\d+(\.\d+)?)/); - }); - }); + it('should not replace not allowlisted vars', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const input = document.createElement('input'); + input.value = 'RANDOM'; + input.type = 'hidden'; + input.setAttribute('data-amp-replace', 'CANONICAL_URL'); + let expandedValue = urlReplacements.expandInputValueSync(input); + expect(expandedValue).to.equal('RANDOM'); + input.setAttribute('data-amp-replace', 'CANONICAL_URL RANDOM'); + expandedValue = urlReplacements.expandInputValueSync(input); + expect(expandedValue).to.match(/(\d+(\.\d+)?)/); + expect(input.value).to.match(/(\d+(\.\d+)?)/); + expect(input['amp-original-value']).to.equal('RANDOM'); + }); + + it('should replace input value with var subs - sync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const input = document.createElement('input'); + input.value = 'RANDOM'; + input.type = 'hidden'; + input.setAttribute('data-amp-replace', 'RANDOM'); + let expandedValue = urlReplacements.expandInputValueSync(input); + expect(expandedValue).to.match(/(\d+(\.\d+)?)/); + + input['amp-original-value'] = 'RANDOM://example.com/RANDOM'; + expandedValue = urlReplacements.expandInputValueSync(input); + expect(expandedValue).to.match( + /(\d+(\.\d+)?):\/\/example\.com\/(\d+(\.\d+)?)$/ + ); + expect(input.value).to.match( + /(\d+(\.\d+)?):\/\/example\.com\/(\d+(\.\d+)?)$/ + ); + expect(input['amp-original-value']).to.equal( + 'RANDOM://example.com/RANDOM' + ); }); - describe('extractClientIdFromGaCookie', () => { - it('should extract correct Client ID', () => { - expect( - extractClientIdFromGaCookie('GA1.2.430749005.1489527047') - ).to.equal('430749005.1489527047'); - expect( - extractClientIdFromGaCookie('GA1.12.430749005.1489527047') - ).to.equal('430749005.1489527047'); - expect( - extractClientIdFromGaCookie('GA1.1-2.430749005.1489527047') - ).to.equal('430749005.1489527047'); - expect( - extractClientIdFromGaCookie('1.1.430749005.1489527047') - ).to.equal('430749005.1489527047'); - expect( - extractClientIdFromGaCookie( - 'GA1.3.amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + - 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ) - ).to.equal( - 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + + it('should replace input value with var subs - sync', () => { + const win = getFakeWindow(); + const {documentElement} = win.document; + const urlReplacements = Services.urlReplacementsForDoc(documentElement); + const input = document.createElement('input'); + input.value = 'RANDOM'; + input.type = 'hidden'; + input.setAttribute('data-amp-replace', 'RANDOM'); + return urlReplacements + .expandInputValueAsync(input) + .then((expandedValue) => { + expect(input['amp-original-value']).to.equal('RANDOM'); + expect(input.value).to.match(/(\d+(\.\d+)?)/); + expect(expandedValue).to.match(/(\d+(\.\d+)?)/); + }); + }); + }); + + describe('extractClientIdFromGaCookie', () => { + it('should extract correct Client ID', () => { + expect( + extractClientIdFromGaCookie('GA1.2.430749005.1489527047') + ).to.equal('430749005.1489527047'); + expect( + extractClientIdFromGaCookie('GA1.12.430749005.1489527047') + ).to.equal('430749005.1489527047'); + expect( + extractClientIdFromGaCookie('GA1.1-2.430749005.1489527047') + ).to.equal('430749005.1489527047'); + expect( + extractClientIdFromGaCookie('1.1.430749005.1489527047') + ).to.equal('430749005.1489527047'); + expect( + extractClientIdFromGaCookie( + 'GA1.3.amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ); - expect( - extractClientIdFromGaCookie( - '1.3.amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + - 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ) - ).to.equal( - 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + + ) + ).to.equal( + 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' + ); + expect( + extractClientIdFromGaCookie( + '1.3.amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ); - expect( - extractClientIdFromGaCookie( - 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + - 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ) - ).to.equal( + ) + ).to.equal( + 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' + ); + expect( + extractClientIdFromGaCookie( 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' - ); - }); + ) + ).to.equal( + 'amp-JTHCVn-4iMhzv5oEIZIspaXUSnEF0PwNVoxs' + + 'NDrFP4BtPQJMyxE4jb9FDlp37OJL' + ); }); }); + }); }); diff --git a/test/unit/test-url-rewrite.js b/test/unit/test-url-rewrite.js index e694c0c0f13e..405a4ee9f73d 100644 --- a/test/unit/test-url-rewrite.js +++ b/test/unit/test-url-rewrite.js @@ -117,18 +117,16 @@ describes.sandboxed('resolveUrlAttr', {}, () => { ).to.equal('http://acme.org/image1'); }); - it.configure() - .skipFirefox() - .run('should NOT rewrite image data src', () => { - expect( - resolveUrlAttr( - 'amp-img', - 'src', - 'data:12345', - 'https://cdn.ampproject.org/c/acme.org/doc1' - ) - ).to.equal('data:12345'); - }); + it('should NOT rewrite image data src', () => { + expect( + resolveUrlAttr( + 'amp-img', + 'src', + 'data:12345', + 'https://cdn.ampproject.org/c/acme.org/doc1' + ) + ).to.equal('data:12345'); + }); }); describes.sandboxed('rewriteAttributesForElement', {}, () => { diff --git a/test/unit/test-url.js b/test/unit/test-url.js index dc1d82385c64..0dd8fde0dcd7 100644 --- a/test/unit/test-url.js +++ b/test/unit/test-url.js @@ -114,8 +114,7 @@ describes.sandboxed('parseUrlDeprecated', {}, () => { expect(a1).to.equal(a2); }); - // TODO(#14349): unskip flaky test - it.skip('caches up to 100 results', () => { + it('caches up to 100 results', () => { const url = 'https://foo.com:123/abc?123#foo'; const a1 = parseUrlDeprecated(url); @@ -632,17 +631,15 @@ describes.sandboxed('isLocalhostOrigin', {}, () => { describes.sandboxed('isProtocolValid', {}, () => { function testProtocolValid(href, bool) { - it.configure() - .skipFirefox() - .run( - 'should return that ' + - href + - (bool ? ' is' : ' is not') + - ' a valid protocol', - () => { - expect(isProtocolValid(href)).to.equal(bool); - } - ); + it( + 'should return that ' + + href + + (bool ? ' is' : ' is not') + + ' a valid protocol', + () => { + expect(isProtocolValid(href)).to.equal(bool); + } + ); } testProtocolValid('http://foo.com', true); @@ -822,24 +819,22 @@ describes.sandboxed('getSourceOrigin/Url', {}, () => { describes.sandboxed('resolveRelativeUrl', {}, () => { function testRelUrl(href, baseHref, resolvedHref) { - it.configure() - .skipFirefox() - .run( - 'should return the resolved rel url from ' + - href + - ' with base ' + - baseHref, - () => { - expect(resolveRelativeUrl(href, baseHref)).to.equal( - resolvedHref, - 'native or fallback' - ); - expect(resolveRelativeUrlFallback_(href, baseHref)).to.equal( - resolvedHref, - 'fallback' - ); - } - ); + it( + 'should return the resolved rel url from ' + + href + + ' with base ' + + baseHref, + () => { + expect(resolveRelativeUrl(href, baseHref)).to.equal( + resolvedHref, + 'native or fallback' + ); + expect(resolveRelativeUrlFallback_(href, baseHref)).to.equal( + resolvedHref, + 'fallback' + ); + } + ); } // Absolute URL. diff --git a/test/unit/test-viewport-binding.js b/test/unit/test-viewport-binding.js index ee3d527e95e4..d134f142a9d4 100644 --- a/test/unit/test-viewport-binding.js +++ b/test/unit/test-viewport-binding.js @@ -84,14 +84,11 @@ describes.realWin('ViewportBindingNatural', {ampCss: true}, (env) => { expect(win.document.documentElement.style.paddingTop).to.equal('31px'); }); - // TODO(zhouyx, #11827): Make this test work on Safari. - it.configure() - .skipSafari() - .run('should calculate size', () => { - const size = binding.getSize(); - expect(size.width).to.equal(100); - expect(size.height).to.equal(200); - }); + it('should calculate size', () => { + const size = binding.getSize(); + expect(size.width).to.equal(100); + expect(size.height).to.equal(200); + }); it('should calculate scrollTop from scrollElement', () => { win.pageYOffset = 11; @@ -330,48 +327,46 @@ describes.realWin('ViewportBindingIosEmbedWrapper', {ampCss: true}, (env) => { expect(binding.wrapper_).to.have.class('top'); }); - it.configure() - .skipFirefox() - .run('should have CSS setup', () => { - win.document.body.style.display = 'table'; - const htmlCss = win.getComputedStyle(win.document.documentElement); - const wrapperCss = win.getComputedStyle(binding.wrapper_); - const bodyCss = win.getComputedStyle(win.document.body); - - // `` must have `position: static` or layout is broken. - expect(htmlCss.position).to.equal('static'); - - // `` and `` must be scrollable, but not `body`. - // Unfortunately, we can't test here `-webkit-overflow-scrolling`. - expect(htmlCss.overflowY).to.equal('auto'); - expect(htmlCss.overflowX).to.equal('hidden'); - expect(wrapperCss.overflowY).to.equal('auto'); - expect(wrapperCss.overflowX).to.equal('hidden'); - expect(bodyCss.overflowY).to.equal('visible'); - expect(bodyCss.overflowX).to.equal('visible'); - - // Wrapper must be a block and positioned absolute at 0/0/0/0. - expect(wrapperCss.display).to.equal('block'); - expect(wrapperCss.position).to.equal('absolute'); - expect(wrapperCss.top).to.equal('0px'); - expect(wrapperCss.left).to.equal('0px'); - expect(wrapperCss.right).to.equal('0px'); - expect(wrapperCss.bottom).to.equal('0px'); - expect(wrapperCss.margin).to.equal('0px'); - - // `body` must have `relative` positioning and `block` display. - expect(bodyCss.position).to.equal('relative'); - // Preserve the customized `display` value. - expect(bodyCss.display).to.equal('table'); - - // `body` must have a 1px transparent border for two purposes: - // (1) to cancel out margin collapse in body's children; - // (2) to offset scroll adjustment to 1 to avoid scroll freeze problem. - expect( - bodyCss.borderTop.replace('rgba(0, 0, 0, 0)', 'transparent') - ).to.equal('1px solid transparent'); - expect(bodyCss.margin).to.equal('0px'); - }); + it('should have CSS setup', () => { + win.document.body.style.display = 'table'; + const htmlCss = win.getComputedStyle(win.document.documentElement); + const wrapperCss = win.getComputedStyle(binding.wrapper_); + const bodyCss = win.getComputedStyle(win.document.body); + + // `` must have `position: static` or layout is broken. + expect(htmlCss.position).to.equal('static'); + + // `` and `` must be scrollable, but not `body`. + // Unfortunately, we can't test here `-webkit-overflow-scrolling`. + expect(htmlCss.overflowY).to.equal('auto'); + expect(htmlCss.overflowX).to.equal('hidden'); + expect(wrapperCss.overflowY).to.equal('auto'); + expect(wrapperCss.overflowX).to.equal('hidden'); + expect(bodyCss.overflowY).to.equal('visible'); + expect(bodyCss.overflowX).to.equal('visible'); + + // Wrapper must be a block and positioned absolute at 0/0/0/0. + expect(wrapperCss.display).to.equal('block'); + expect(wrapperCss.position).to.equal('absolute'); + expect(wrapperCss.top).to.equal('0px'); + expect(wrapperCss.left).to.equal('0px'); + expect(wrapperCss.right).to.equal('0px'); + expect(wrapperCss.bottom).to.equal('0px'); + expect(wrapperCss.margin).to.equal('0px'); + + // `body` must have `relative` positioning and `block` display. + expect(bodyCss.position).to.equal('relative'); + // Preserve the customized `display` value. + expect(bodyCss.display).to.equal('table'); + + // `body` must have a 1px transparent border for two purposes: + // (1) to cancel out margin collapse in body's children; + // (2) to offset scroll adjustment to 1 to avoid scroll freeze problem. + expect( + bodyCss.borderTop.replace('rgba(0, 0, 0, 0)', 'transparent') + ).to.equal('1px solid transparent'); + expect(bodyCss.margin).to.equal('0px'); + }); it('should be immediately scrolled to 1 to avoid freeze', () => { expect(binding.wrapper_.scrollTop).to.equal(1); @@ -401,14 +396,11 @@ describes.realWin('ViewportBindingIosEmbedWrapper', {ampCss: true}, (env) => { expect(win.document.documentElement.style.paddingTop).to.equal(''); }); - // TODO(zhouyx, #11827): Make this test work on Safari. - it.configure() - .skipSafari() - .run('should calculate size', () => { - const size = binding.getSize(); - expect(size.width).to.equal(100); - expect(size.height).to.equal(100); - }); + it('should calculate size', () => { + const size = binding.getSize(); + expect(size.width).to.equal(100); + expect(size.height).to.equal(100); + }); it('should calculate scrollTop from wrapper', () => { binding.wrapper_.scrollTop = 17; diff --git a/test/unit/test-viewport.js b/test/unit/test-viewport.js index 63e9f8e2ae6b..dba6059a0940 100644 --- a/test/unit/test-viewport.js +++ b/test/unit/test-viewport.js @@ -1269,25 +1269,19 @@ describes.fakeWin('Viewport', {}, (env) => { root.className = ''; }); - // TODO(zhouyx, #11827): Make this test work on Safari. - it.configure() - .skipSafari() - .run('should not set pan-y when not embedded', () => { - viewer.isEmbedded = () => false; - viewport = new ViewportImpl(ampdoc, binding, viewer); - expect(win.getComputedStyle(root)['touch-action']).to.equal('auto'); - }); + it('should not set pan-y when not embedded', () => { + viewer.isEmbedded = () => false; + viewport = new ViewportImpl(ampdoc, binding, viewer); + expect(win.getComputedStyle(root)['touch-action']).to.equal('auto'); + }); - // TODO(zhouyx, #11827): Make this test work on Safari. - it.configure() - .skipSafari() - .run('should set pan-y with experiment', () => { - viewer.isEmbedded = () => true; - viewport = new ViewportImpl(ampdoc, binding, viewer); - expect(win.getComputedStyle(root)['touch-action']).to.equal( - 'pan-y pinch-zoom' - ); - }); + it('should set pan-y with experiment', () => { + viewer.isEmbedded = () => true; + viewport = new ViewportImpl(ampdoc, binding, viewer); + expect(win.getComputedStyle(root)['touch-action']).to.equal( + 'pan-y pinch-zoom' + ); + }); }); describe('for child window', () => { diff --git a/test/unit/test-xhr.js b/test/unit/test-xhr.js index 39d69b6c56f3..b8c085856fad 100644 --- a/test/unit/test-xhr.js +++ b/test/unit/test-xhr.js @@ -14,819 +14,835 @@ import {getCookie} from '../../src/cookies'; import {createFormDataWrapper} from '../../src/form-data-wrapper'; import * as mode from '../../src/mode'; -// TODO(jridgewell, #11827): Make this test work on Safari. -describes.sandboxed - .configure() - .skipSafari() - .run('XHR', {}, function (env) { - let ampdocServiceForStub; - let ampdoc; - let ampdocViewerStub; - let xhrCreated; - let viewer; - - const location = {href: 'https://acme.com/path'}; - const nativeWin = { - location, - fetch: window.fetch, - }; - - const polyfillWin = { - location, - fetch: fetchPolyfill, - }; - - // Given XHR calls give tests more time. - this.timeout(5000); - - const scenarios = [ - { - win: nativeWin, - desc: 'Native', - }, - { - win: polyfillWin, - desc: 'Polyfill', - }, - ]; - - function setupMockXhr() { - const mockXhr = env.sandbox.useFakeXMLHttpRequest(); - xhrCreated = new Promise((resolve) => (mockXhr.onCreate = resolve)); +describes.sandboxed('XHR', {}, function (env) { + let ampdocServiceForStub; + let ampdoc; + let ampdocViewerStub; + let xhrCreated; + let viewer; + + const location = {href: 'https://acme.com/path'}; + const nativeWin = { + location, + fetch: window.fetch, + }; + + const polyfillWin = { + location, + fetch: fetchPolyfill, + }; + + // Given XHR calls give tests more time. + this.timeout(5000); + + const scenarios = [ + { + win: nativeWin, + desc: 'Native', + }, + { + win: polyfillWin, + desc: 'Polyfill', + }, + ]; + + function setupMockXhr() { + const mockXhr = env.sandbox.useFakeXMLHttpRequest(); + xhrCreated = new Promise((resolve) => (mockXhr.onCreate = resolve)); + } + + function noOrigin(url) { + let index = url.indexOf('//'); + if (index == -1) { + return url; } + index = url.indexOf('/', index + 2); + return url.substring(index); + } + + beforeEach(() => { + ampdocServiceForStub = env.sandbox.stub(Services, 'ampdocServiceFor'); + ampdoc = { + getRootNode: () => null, + whenFirstVisible: () => Promise.resolve(), + }; + ampdocServiceForStub.returns({ + isSingleDoc: () => false, + getAmpDoc: () => ampdoc, + getSingleDoc: () => ampdoc, + }); + ampdocViewerStub = env.sandbox.stub(Services, 'viewerForDoc'); + ampdocViewerStub.returns({}); - function noOrigin(url) { - let index = url.indexOf('//'); - if (index == -1) { - return url; - } - index = url.indexOf('/', index + 2); - return url.substring(index); - } + location.href = 'https://acme.com/path'; + }); + scenarios.forEach((test) => { + let xhr; beforeEach(() => { - ampdocServiceForStub = env.sandbox.stub(Services, 'ampdocServiceFor'); - ampdoc = { - getRootNode: () => null, - whenFirstVisible: () => Promise.resolve(), - }; - ampdocServiceForStub.returns({ - isSingleDoc: () => false, - getAmpDoc: () => ampdoc, - getSingleDoc: () => ampdoc, - }); - ampdocViewerStub = env.sandbox.stub(Services, 'viewerForDoc'); - ampdocViewerStub.returns({}); - - location.href = 'https://acme.com/path'; + xhr = xhrServiceForTesting(test.win); }); + // Since it's the Native fetch, it won't use the XHR object so + // mocking and testing the request becomes not doable. + if (test.desc != 'Native') { + describe('#XHR', () => { + beforeEach(() => { + setupMockXhr(); + }); - scenarios.forEach((test) => { - let xhr; - beforeEach(() => { - xhr = xhrServiceForTesting(test.win); - }); - // Since it's the Native fetch, it won't use the XHR object so - // mocking and testing the request becomes not doable. - if (test.desc != 'Native') { - describe('#XHR', () => { - beforeEach(() => { - setupMockXhr(); + it('should allow GET and POST methods', () => { + const get = xhr.fetchJson.bind(xhr, '/get?k=v1'); + const post = xhr.fetchJson.bind(xhr, '/post', { + method: 'POST', + body: { + hello: 'world', + }, }); - - it('should allow GET and POST methods', () => { - const get = xhr.fetchJson.bind(xhr, '/get?k=v1'); - const post = xhr.fetchJson.bind(xhr, '/post', { - method: 'POST', - body: { - hello: 'world', - }, - }); - const put = xhr.fetchJson.bind(xhr, '/put', { - method: 'PUT', - body: { - hello: 'world', - }, - }); - const patch = xhr.fetchJson.bind(xhr, '/patch', { - method: 'PATCH', - body: { - hello: 'world', - }, - }); - const deleteMethod = xhr.fetchJson.bind(xhr, '/delete', { - method: 'DELETE', - body: { - id: 3, - }, - }); - - expect(get).to.not.throw(); - expect(post).to.not.throw(); - allowConsoleError(() => { - expect(put).to.throw(); - }); - allowConsoleError(() => { - expect(patch).to.throw(); - }); - allowConsoleError(() => { - expect(deleteMethod).to.throw(); - }); + const put = xhr.fetchJson.bind(xhr, '/put', { + method: 'PUT', + body: { + hello: 'world', + }, }); - - it('should allow FormData as body', () => { - const fakeWin = null; - env.sandbox.stub(Services, 'platformFor').returns({ - isIos() { - return false; - }, - }); - - const formData = createFormDataWrapper(fakeWin); - env.sandbox.stub(JSON, 'stringify'); - formData.append('name', 'John Miller'); - formData.append('age', 56); - const post = xhr.fetchJson.bind(xhr, '/post', { - method: 'POST', - body: formData, - }); - expect(post).to.not.throw(); - expect(JSON.stringify.called).to.be.false; + const patch = xhr.fetchJson.bind(xhr, '/patch', { + method: 'PATCH', + body: { + hello: 'world', + }, }); - - it('should do `GET` as default method', () => { - xhr.fetchJson('/get?k=v1'); - return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); + const deleteMethod = xhr.fetchJson.bind(xhr, '/delete', { + method: 'DELETE', + body: { + id: 3, + }, }); - it('should normalize GET method name to uppercase', () => { - xhr.fetchJson('/abc'); - return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); + expect(get).to.not.throw(); + expect(post).to.not.throw(); + allowConsoleError(() => { + expect(put).to.throw(); }); - - it('should normalize POST method name to uppercase', () => { - xhr.fetchJson('/abc', { - method: 'post', - body: { - hello: 'world', - }, - }); - return xhrCreated.then((xhr) => - expect(xhr.method).to.equal('POST') - ); + allowConsoleError(() => { + expect(patch).to.throw(); }); - - it('should inject source origin query parameter', () => { - xhr.fetchJson('/get?k=v1#h1'); - return xhrCreated.then((xhr) => - expect(noOrigin(xhr.url)).to.equal( - '/get?k=v1&__amp_source_origin=https%3A%2F%2Facme.com#h1' - ) - ); + allowConsoleError(() => { + expect(deleteMethod).to.throw(); }); + }); - it('should inject source origin query parameter w/o query', () => { - xhr.fetchJson('/get'); - return xhrCreated.then((xhr) => - expect(noOrigin(xhr.url)).to.equal( - '/get?__amp_source_origin=https%3A%2F%2Facme.com' - ) - ); + it('should allow FormData as body', () => { + const fakeWin = null; + env.sandbox.stub(Services, 'platformFor').returns({ + isIos() { + return false; + }, }); - it('should defend against invalid source origin query parameter', () => { - allowConsoleError(() => { - expect(() => { - xhr.fetchJson('/get?k=v1&__amp_source_origin=invalid#h1'); - }).to.throw(/Source origin is not allowed/); - }); + const formData = createFormDataWrapper(fakeWin); + env.sandbox.stub(JSON, 'stringify'); + formData.append('name', 'John Miller'); + formData.append('age', 56); + const post = xhr.fetchJson.bind(xhr, '/post', { + method: 'POST', + body: formData, }); + expect(post).to.not.throw(); + expect(JSON.stringify.called).to.be.false; + }); - it('should defend against empty source origin query parameter', () => { - allowConsoleError(() => { - expect(() => { - xhr.fetchJson('/get?k=v1&__amp_source_origin=#h1'); - }).to.throw(/Source origin is not allowed/); - }); - }); + it('should do `GET` as default method', () => { + xhr.fetchJson('/get?k=v1'); + return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); + }); - it('should defend against re-encoded source origin parameter', () => { - allowConsoleError(() => { - expect(() => { - xhr.fetchJson('/get?k=v1&_%5famp_source_origin=#h1'); - }).to.throw(/Source origin is not allowed/); - }); + it('should normalize GET method name to uppercase', () => { + xhr.fetchJson('/abc'); + return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); + }); + + it('should normalize POST method name to uppercase', () => { + xhr.fetchJson('/abc', { + method: 'post', + body: { + hello: 'world', + }, }); + return xhrCreated.then((xhr) => expect(xhr.method).to.equal('POST')); + }); - it( - 'should not include __amp_source_origin if ampCors ' + - 'set to false', - () => { - xhr.fetchJson('/get', {ampCors: false}); - return xhrCreated.then((xhr) => - expect(noOrigin(xhr.url)).to.equal('/get') - ); - } + it('should inject source origin query parameter', () => { + xhr.fetchJson('/get?k=v1#h1'); + return xhrCreated.then((xhr) => + expect(noOrigin(xhr.url)).to.equal( + '/get?k=v1&__amp_source_origin=https%3A%2F%2Facme.com#h1' + ) ); + }); - it('should accept AMP origin when received in response', () => { - const promise = xhr.fetchJson('/get'); - xhrCreated.then((xhr) => - xhr.respond( - 200, - { - 'Content-Type': 'application/json', - }, - '{}' - ) - ); - return promise; - }); + it('should inject source origin query parameter w/o query', () => { + xhr.fetchJson('/get'); + return xhrCreated.then((xhr) => + expect(noOrigin(xhr.url)).to.equal( + '/get?__amp_source_origin=https%3A%2F%2Facme.com' + ) + ); + }); - describe('doc visibility', () => { - afterEach(() => { - test.win.fetch.restore(); - }); - it('should not call fetch if view is not visible ', () => { - const fetchCall = env.sandbox.spy(test.win, 'fetch'); - ampdoc.whenFirstVisible = () => Promise.reject(); - xhr.fetchJson('/get', {ampCors: false}); - expect(fetchCall.notCalled).to.be.true; - }); - it('should call fetch if view is visible ', () => { - const fetchCall = env.sandbox.spy(test.win, 'fetch'); - ampdoc.whenFirstVisible = () => Promise.resolve(); - const fetch = xhr.fetchJson('/get', {ampCors: false}); - fetch.then(() => { - expect(fetchCall.calledOnce).to.be.true; - }); - }); + it('should defend against invalid source origin query parameter', () => { + allowConsoleError(() => { + expect(() => { + xhr.fetchJson('/get?k=v1&__amp_source_origin=invalid#h1'); + }).to.throw(/Source origin is not allowed/); }); }); - } - describe('AMP-Same-Origin', () => { - beforeEach(() => { - xhr = xhrServiceForTesting(test.win); + it('should defend against empty source origin query parameter', () => { + allowConsoleError(() => { + expect(() => { + xhr.fetchJson('/get?k=v1&__amp_source_origin=#h1'); + }).to.throw(/Source origin is not allowed/); + }); }); - it('should not be set for cross origin requests', () => { - const init = {}; - xhr.fetchJson('/whatever', init); - expect(init['headers']['AMP-Same-Origin']).to.be.undefined; + it('should defend against re-encoded source origin parameter', () => { + allowConsoleError(() => { + expect(() => { + xhr.fetchJson('/get?k=v1&_%5famp_source_origin=#h1'); + }).to.throw(/Source origin is not allowed/); + }); }); - it('should be set for all same origin GET requests', () => { - const init = {}; - location.href = '/path'; - xhr.fetchJson('/whatever', init); - expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + it('should not include __amp_source_origin if ampCors set to false', () => { + xhr.fetchJson('/get', {ampCors: false}); + return xhrCreated.then((xhr) => + expect(noOrigin(xhr.url)).to.equal('/get') + ); }); - it('should be set for all same origin POST requests', () => { - const init = {method: 'post', body: {}}; - location.href = '/path'; - xhr.fetchJson('/whatever', init); - expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + it('should accept AMP origin when received in response', () => { + const promise = xhr.fetchJson('/get'); + xhrCreated.then((xhr) => + xhr.respond( + 200, + { + 'Content-Type': 'application/json', + }, + '{}' + ) + ); + return promise; }); - it('should check origin not source origin', () => { - let init = {method: 'post', body: {}}; - location.href = - 'https://cdn.ampproject.org/c/s/example.com/hello/path'; - xhr.fetchJson('https://example.com/what/ever', init); - expect(init['headers']['AMP-Same-Origin']).to.be.undefined; - - init = {method: 'post', body: {}}; - location.href = 'https://example.com/hello/path'; - xhr.fetchJson('https://example.com/what/ever', init); - expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + describe('doc visibility', () => { + afterEach(() => { + test.win.fetch.restore(); + }); + it('should not call fetch if view is not visible ', () => { + const fetchCall = env.sandbox.spy(test.win, 'fetch'); + ampdoc.whenFirstVisible = () => Promise.reject(); + xhr.fetchJson('/get', {ampCors: false}); + expect(fetchCall.notCalled).to.be.true; + }); + it('should call fetch if view is visible ', () => { + const fetchCall = env.sandbox.spy(test.win, 'fetch'); + ampdoc.whenFirstVisible = () => Promise.resolve(); + const fetch = xhr.fetchJson('/get', {ampCors: false}); + fetch.then(() => { + expect(fetchCall.calledOnce).to.be.true; + }); + }); }); }); + } - describe(test.desc, () => { - const {testServerPort} = window.ampTestRuntimeConfig; - const baseUrl = `http://localhost:${testServerPort}`; + describe('AMP-Same-Origin', () => { + beforeEach(() => { + xhr = xhrServiceForTesting(test.win); + }); - beforeEach(() => { - xhr = xhrServiceForTesting(test.win); - }); + it('should not be set for cross origin requests', () => { + const init = {}; + xhr.fetchJson('/whatever', init); + expect(init['headers']['AMP-Same-Origin']).to.be.undefined; + }); - describe('assertSuccess', () => { - function createResponseInstance(body, init) { - return new Response(body, init); - } - const mockXhr = { - status: 200, - headers: { - 'Content-Type': 'plain/text', - }, - getResponseHeader: () => '', - }; - - it('should resolve if success', () => { - mockXhr.status = 200; - return assertSuccess(createResponseInstance('', mockXhr)).then( - (response) => { - expect(response.status).to.equal(200); - } - ).should.not.be.rejected; - }); + it('should be set for all same origin GET requests', () => { + const init = {}; + location.href = '/path'; + xhr.fetchJson('/whatever', init); + expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + }); - it('should reject if error', () => { - mockXhr.status = 500; - return assertSuccess(createResponseInstance('', mockXhr)).should.be - .rejected; - }); + it('should be set for all same origin POST requests', () => { + const init = {method: 'post', body: {}}; + location.href = '/path'; + xhr.fetchJson('/whatever', init); + expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + }); - it('should include response in error', () => { - mockXhr.status = 500; - return assertSuccess(createResponseInstance('', mockXhr)).catch( - (error) => { - expect(error.response).to.exist; - expect(error.response.status).to.equal(500); - } - ); - }); + it('should check origin not source origin', () => { + let init = {method: 'post', body: {}}; + location.href = 'https://cdn.ampproject.org/c/s/example.com/hello/path'; + xhr.fetchJson('https://example.com/what/ever', init); + expect(init['headers']['AMP-Same-Origin']).to.be.undefined; - it('should not resolve after rejecting promise', () => { - mockXhr.status = 500; - mockXhr.responseText = '{"a": "hello"}'; - mockXhr.headers['Content-Type'] = 'application/json'; - mockXhr.getResponseHeader = () => 'application/json'; - return assertSuccess(createResponseInstance('{"a": 2}', mockXhr)) - .should.not.be.fulfilled; - }); - }); + init = {method: 'post', body: {}}; + location.href = 'https://example.com/hello/path'; + xhr.fetchJson('https://example.com/what/ever', init); + expect(init['headers']['AMP-Same-Origin']).to.equal('true'); + }); + }); - it('should do simple JSON fetch', () => { - env.sandbox.stub(user(), 'assert'); - return xhr - .fetchJson(`${baseUrl}/get?k=v1`) - .then((res) => res.json()) - .then((res) => { - expect(res).to.exist; - expect(res['args']['k']).to.equal('v1'); - }); + describe(test.desc, () => { + const {testServerPort} = window.ampTestRuntimeConfig; + const baseUrl = `http://localhost:${testServerPort}`; + + beforeEach(() => { + xhr = xhrServiceForTesting(test.win); + }); + + describe('assertSuccess', () => { + function createResponseInstance(body, init) { + return new Response(body, init); + } + const mockXhr = { + status: 200, + headers: { + 'Content-Type': 'plain/text', + }, + getResponseHeader: () => '', + }; + + it('should resolve if success', () => { + mockXhr.status = 200; + return assertSuccess(createResponseInstance('', mockXhr)).then( + (response) => { + expect(response.status).to.equal(200); + } + ).should.not.be.rejected; }); - it('should redirect fetch', () => { - const url = - `${baseUrl}/redirect-to?url=` + - encodeURIComponent(`${baseUrl}/get?k=v2`); - return xhr - .fetchJson(url, {ampCors: false}) - .then((res) => res.json()) - .then((res) => { - expect(res).to.exist; - expect(res['args']['k']).to.equal('v2'); - }); + it('should reject if error', () => { + mockXhr.status = 500; + return assertSuccess(createResponseInstance('', mockXhr)).should.be + .rejected; }); - it('should fail fetch for 400-error', () => { - const url = `${baseUrl}/status/404`; - return xhr.fetchJson(url).then( - () => { - throw new Error('UNREACHABLE'); - }, + it('should include response in error', () => { + mockXhr.status = 500; + return assertSuccess(createResponseInstance('', mockXhr)).catch( (error) => { - expect(error.message).to.contain('HTTP error 404'); + expect(error.response).to.exist; + expect(error.response.status).to.equal(500); } ); }); - it('should fail fetch for 500-error', () => { - const url = `${baseUrl}/status/500?CID=cid`; - return xhr.fetchJson(url).then( - () => { - throw new Error('UNREACHABLE'); - }, - (error) => { - expect(error.message).to.contain('HTTP error 500'); - } - ); + it('should not resolve after rejecting promise', () => { + mockXhr.status = 500; + mockXhr.responseText = '{"a": "hello"}'; + mockXhr.headers['Content-Type'] = 'application/json'; + mockXhr.getResponseHeader = () => 'application/json'; + return assertSuccess(createResponseInstance('{"a": 2}', mockXhr)) + .should.not.be.fulfilled; }); + }); - it('should NOT succeed CORS setting cookies without credentials', () => { - const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); - const url = `${baseUrl}/cookies/set?${cookieName}=v1`; - return xhr.fetchJson(url).then((res) => { + it('should do simple JSON fetch', () => { + env.sandbox.stub(user(), 'assert'); + return xhr + .fetchJson(`${baseUrl}/get?k=v1`) + .then((res) => res.json()) + .then((res) => { expect(res).to.exist; - expect(getCookie(window, cookieName)).to.be.null; + expect(res['args']['k']).to.equal('v1'); }); - }); + }); - it('should succeed CORS setting cookies with credentials', () => { - const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); - const url = `${baseUrl}/cookies/set?${cookieName}=v1`; - return xhr.fetchJson(url, {credentials: 'include'}).then((res) => { + it('should redirect fetch', () => { + const url = + `${baseUrl}/redirect-to?url=` + + encodeURIComponent(`${baseUrl}/get?k=v2`); + return xhr + .fetchJson(url, {ampCors: false}) + .then((res) => res.json()) + .then((res) => { expect(res).to.exist; - expect(getCookie(window, cookieName)).to.equal('v1'); + expect(res['args']['k']).to.equal('v2'); }); + }); + + it('should fail fetch for 400-error', () => { + const url = `${baseUrl}/status/404`; + return xhr.fetchJson(url).then( + () => { + throw new Error('UNREACHABLE'); + }, + (error) => { + expect(error.message).to.contain('HTTP error 404'); + } + ); + }); + + it('should fail fetch for 500-error', () => { + const url = `${baseUrl}/status/500?CID=cid`; + return xhr.fetchJson(url).then( + () => { + throw new Error('UNREACHABLE'); + }, + (error) => { + expect(error.message).to.contain('HTTP error 500'); + } + ); + }); + + it('should NOT succeed CORS setting cookies without credentials', () => { + const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); + const url = `${baseUrl}/cookies/set?${cookieName}=v1`; + return xhr.fetchJson(url).then((res) => { + expect(res).to.exist; + expect(getCookie(window, cookieName)).to.be.null; }); + }); - it('should ignore CORS setting cookies w/omit credentials', () => { - const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); - const url = `${baseUrl}/cookies/set?${cookieName}=v1`; - return xhr.fetchJson(url, {credentials: 'omit'}).then((res) => { - expect(res).to.exist; - expect(getCookie(window, cookieName)).to.be.null; - }); + it('should succeed CORS setting cookies with credentials', () => { + const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); + const url = `${baseUrl}/cookies/set?${cookieName}=v1`; + return xhr.fetchJson(url, {credentials: 'include'}).then((res) => { + expect(res).to.exist; + expect(getCookie(window, cookieName)).to.equal('v1'); }); + }); - it('should NOT succeed CORS with invalid credentials', () => { - allowConsoleError(() => { - expect(() => { - xhr.fetchJson('https://acme.org/', {credentials: null}); - }).to.throw(/Only credentials=include|omit support: null/); - }); + it('should ignore CORS setting cookies w/omit credentials', () => { + const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); + const url = `${baseUrl}/cookies/set?${cookieName}=v1`; + return xhr.fetchJson(url, {credentials: 'omit'}).then((res) => { + expect(res).to.exist; + expect(getCookie(window, cookieName)).to.be.null; }); + }); - it('should expose HTTP headers', () => { - const url = - `${baseUrl}/response-headers?AMP-Header=Value1&` + - 'Access-Control-Expose-Headers=AMP-Header'; - return xhr.fetchAmpCors_(url, {ampCors: false}).then((res) => { - expect(res.headers.get('AMP-Header')).to.equal('Value1'); - }); + it('should NOT succeed CORS with invalid credentials', () => { + allowConsoleError(() => { + expect(() => { + xhr.fetchJson('https://acme.org/', {credentials: null}); + }).to.throw(/Only credentials=include|omit support: null/); }); + }); - it('should omit request details for privacy', () => { - // NOTE THIS IS A BAD PORT ON PURPOSE. - return xhr.fetchJson('http://localhost:31863/status/500').then( - () => { - throw new Error('UNREACHABLE'); - }, - (error) => { - const {message} = error; - expect(message).to.contain('http://localhost:31863'); - expect(message).not.to.contain('status/500'); - expect(message).not.to.contain('CID'); - } - ); + it('should expose HTTP headers', () => { + const url = + `${baseUrl}/response-headers?AMP-Header=Value1&` + + 'Access-Control-Expose-Headers=AMP-Header'; + return xhr.fetchAmpCors_(url, {ampCors: false}).then((res) => { + expect(res.headers.get('AMP-Header')).to.equal('Value1'); }); }); - describe('#fetchText', () => { - const TEST_TEXT = 'test text'; - let fetchStub; + it('should omit request details for privacy', () => { + // NOTE THIS IS A BAD PORT ON PURPOSE. + return xhr.fetchJson('http://localhost:31863/status/500').then( + () => { + throw new Error('UNREACHABLE'); + }, + (error) => { + const {message} = error; + expect(message).to.contain('http://localhost:31863'); + expect(message).not.to.contain('status/500'); + expect(message).not.to.contain('CID'); + } + ); + }); + }); - beforeEach(() => { - xhr = xhrServiceForTesting(test.win); - fetchStub = env.sandbox - .stub(xhr, 'fetchAmpCors_') - .callsFake(() => Promise.resolve(new Response(TEST_TEXT))); - }); + describe('#fetchText', () => { + const TEST_TEXT = 'test text'; + let fetchStub; - it('should be able to fetch a document', () => { - const promise = xhr.fetchText('/text.html'); - expect(fetchStub).to.be.calledWith('/text.html', { - method: 'GET', - headers: {'Accept': 'text/plain'}, - }); - return promise - .then((res) => { - return res.text(); - }) - .then((text) => { - expect(text).to.equal(TEST_TEXT); - }); - }); + beforeEach(() => { + xhr = xhrServiceForTesting(test.win); + fetchStub = env.sandbox + .stub(xhr, 'fetchAmpCors_') + .callsFake(() => Promise.resolve(new Response(TEST_TEXT))); }); - describe('#fetch ' + test.desc, () => { - const creative = 'This is a creative简'; - - beforeEach(() => (xhr = xhrServiceForTesting(test.win))); - - // Using the Native fetch, we can't mock the XHR request, so an actual - // HTTP request would be sent to the server. Only execute this test - // when we're on the PolyFill case, so that we can mock the XHR and - // control the response. - if (test.desc != 'Native') { - it('should be able to fetch a response', () => { - setupMockXhr(); - const promise = xhr.fetch('/index.html').then((response) => { - expect(response.headers.get('X-foo-header')).to.equal('foo data'); - expect(response.headers.get('X-bar-header')).to.equal('bar data'); - response - .arrayBuffer() - .then((bytes) => utf8Decode(bytes)) - .then((text) => { - expect(text).to.equal(creative); - }); - }); - xhrCreated.then((xhr) => - xhr.respond( - 200, - { - 'Content-Type': 'text/xml', - 'X-foo-header': 'foo data', - 'X-bar-header': 'bar data', - }, - creative - ) - ); - return promise; + it('should be able to fetch a document', () => { + const promise = xhr.fetchText('/text.html'); + expect(fetchStub).to.be.calledWith('/text.html', { + method: 'GET', + headers: {'Accept': 'text/plain'}, + }); + return promise + .then((res) => { + return res.text(); + }) + .then((text) => { + expect(text).to.equal(TEST_TEXT); }); - } }); }); - scenarios.forEach((test) => { - const {testServerPort} = window.ampTestRuntimeConfig; - const url = `http://localhost:${testServerPort}/post`; - - describe(test.desc + ' POST', () => { - let xhr; + describe('#fetch ' + test.desc, () => { + const creative = 'This is a creative简'; - beforeEach(() => (xhr = xhrServiceForTesting(test.win))); + beforeEach(() => (xhr = xhrServiceForTesting(test.win))); - if (test.desc != 'Native') { - it('should have required json POST headers by default', () => { - setupMockXhr(); - xhr.fetchJson(url, { - method: 'POST', - body: { - hello: 'world', - }, - headers: { - 'Other': 'another', - }, - }); - return xhrCreated.then((xhr) => - expect(xhr.requestHeaders).to.deep.equal({ - 'Accept': 'application/json', - 'Content-Type': 'text/plain;charset=utf-8', - 'Other': 'another', // Not removed when other headers set. - }) - ); + // Using the Native fetch, we can't mock the XHR request, so an actual + // HTTP request would be sent to the server. Only execute this test + // when we're on the PolyFill case, so that we can mock the XHR and + // control the response. + if (test.desc != 'Native') { + it('should be able to fetch a response', () => { + setupMockXhr(); + const promise = xhr.fetch('/index.html').then((response) => { + expect(response.headers.get('X-foo-header')).to.equal('foo data'); + expect(response.headers.get('X-bar-header')).to.equal('bar data'); + response + .arrayBuffer() + .then((bytes) => utf8Decode(bytes)) + .then((text) => { + expect(text).to.equal(creative); + }); }); - } - - it("should get an echo'd response back", () => { - return xhr - .fetchJson(url, { - method: 'POST', - body: { - hello: 'world', - }, - headers: { - 'Content-Type': 'application/json;charset=utf-8', + xhrCreated.then((xhr) => + xhr.respond( + 200, + { + 'Content-Type': 'text/xml', + 'X-foo-header': 'foo data', + 'X-bar-header': 'bar data', }, - }) - .then((res) => res.json()) - .then((res) => { - expect(res.json).to.jsonEqual({ - hello: 'world', - }); - }); + creative + ) + ); + return promise; }); + } + }); + }); + + scenarios.forEach((test) => { + const {testServerPort} = window.ampTestRuntimeConfig; + const url = `http://localhost:${testServerPort}/post`; + + describe(test.desc + ' POST', () => { + let xhr; - it('should throw when `body` is not an object or array', () => { - const objectFn = xhr.fetchJson.bind(xhr, url, { + beforeEach(() => (xhr = xhrServiceForTesting(test.win))); + + if (test.desc != 'Native') { + it('should have required json POST headers by default', () => { + setupMockXhr(); + xhr.fetchJson(url, { method: 'POST', body: { hello: 'world', }, + headers: { + 'Other': 'another', + }, }); - const arrayFn = xhr.fetchJson.bind(xhr, url, { - method: 'POST', - body: ['hello', 'world'], - }); - const stringFn = xhr.fetchJson.bind(xhr, url, { - method: 'POST', - body: 'hello world', - }); - const numberFn = xhr.fetchJson.bind(xhr, url, { - method: 'POST', - body: 3, - }); - const booleanFn = xhr.fetchJson.bind(xhr, url, { - method: 'POST', - body: true, - }); - const nullFn = xhr.fetchJson.bind(xhr, url, { + return xhrCreated.then((xhr) => + expect(xhr.requestHeaders).to.deep.equal({ + 'Accept': 'application/json', + 'Content-Type': 'text/plain;charset=utf-8', + 'Other': 'another', // Not removed when other headers set. + }) + ); + }); + } + + it("should get an echo'd response back", () => { + return xhr + .fetchJson(url, { method: 'POST', - body: null, + body: { + hello: 'world', + }, + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }) + .then((res) => res.json()) + .then((res) => { + expect(res.json).to.jsonEqual({ + hello: 'world', + }); }); + }); - expect(objectFn).to.not.throw(); - expect(arrayFn).to.not.throw(); - allowConsoleError(() => { - expect(stringFn).to.throw(); - }); - allowConsoleError(() => { - expect(numberFn).to.throw(); - }); - allowConsoleError(() => { - expect(booleanFn).to.throw(); - }); - allowConsoleError(() => { - expect(nullFn).to.throw(); - }); + it('should throw when `body` is not an object or array', () => { + const objectFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: { + hello: 'world', + }, + }); + const arrayFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: ['hello', 'world'], + }); + const stringFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: 'hello world', + }); + const numberFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: 3, + }); + const booleanFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: true, + }); + const nullFn = xhr.fetchJson.bind(xhr, url, { + method: 'POST', + body: null, + }); + + expect(objectFn).to.not.throw(); + expect(arrayFn).to.not.throw(); + allowConsoleError(() => { + expect(stringFn).to.throw(); + }); + allowConsoleError(() => { + expect(numberFn).to.throw(); + }); + allowConsoleError(() => { + expect(booleanFn).to.throw(); + }); + allowConsoleError(() => { + expect(nullFn).to.throw(); }); }); }); + }); - describe('interceptor', () => { - const origin = 'https://acme.com'; - - let interceptionEnabledWin; - let optedInDoc; - let sendMessageStub; - function getDefaultResponseOptions() { - return { - headers: [], - }; - } + describe('interceptor', () => { + const origin = 'https://acme.com'; - function getDefaultResponsePromise() { - return Promise.resolve({init: getDefaultResponseOptions()}); - } + let interceptionEnabledWin; + let optedInDoc; + let sendMessageStub; + function getDefaultResponseOptions() { + return { + headers: [], + }; + } - beforeEach(() => { - optedInDoc = window.document.implementation.createHTMLDocument(''); - optedInDoc.documentElement.setAttribute('allow-xhr-interception', ''); + function getDefaultResponsePromise() { + return Promise.resolve({init: getDefaultResponseOptions()}); + } - const ampdoc = { - getRootNode: () => optedInDoc, - whenFirstVisible: () => Promise.resolve(), - }; - ampdocServiceForStub.returns({ - isSingleDoc: () => true, - getAmpDoc: () => ampdoc, - getSingleDoc: () => ampdoc, - }); - viewer = { - hasCapability: () => true, - isTrustedViewer: () => Promise.resolve(true), - sendMessageAwaitResponse: getDefaultResponsePromise, - whenFirstVisible: () => Promise.resolve(), - }; - sendMessageStub = env.sandbox.stub(viewer, 'sendMessageAwaitResponse'); - sendMessageStub.returns(getDefaultResponsePromise()); - ampdocViewerStub.returns(viewer); - interceptionEnabledWin = { - location: { - href: `${origin}/path`, - }, - fetch: () => - Promise.resolve(new Response('', getDefaultResponseOptions())), - }; - }); + beforeEach(() => { + optedInDoc = window.document.implementation.createHTMLDocument(''); + optedInDoc.documentElement.setAttribute('allow-xhr-interception', ''); - afterEach(() => { - toggleExperiment( - interceptionEnabledWin, - 'untrusted-xhr-interception', - false - ); + const ampdoc = { + getRootNode: () => optedInDoc, + whenFirstVisible: () => Promise.resolve(), + }; + ampdocServiceForStub.returns({ + isSingleDoc: () => true, + getAmpDoc: () => ampdoc, + getSingleDoc: () => ampdoc, }); + viewer = { + hasCapability: () => true, + isTrustedViewer: () => Promise.resolve(true), + sendMessageAwaitResponse: getDefaultResponsePromise, + whenFirstVisible: () => Promise.resolve(), + }; + sendMessageStub = env.sandbox.stub(viewer, 'sendMessageAwaitResponse'); + sendMessageStub.returns(getDefaultResponsePromise()); + ampdocViewerStub.returns(viewer); + interceptionEnabledWin = { + location: { + href: `${origin}/path`, + }, + fetch: () => + Promise.resolve(new Response('', getDefaultResponseOptions())), + }; + }); - it('should not intercept if AMP doc is not single', () => { - const ampdoc = { - getRootNode: () => optedInDoc, - whenFirstVisible: () => Promise.resolve(), - }; - ampdocServiceForStub.returns({ - isSingleDoc: () => false, - getAmpDoc: () => ampdoc, - getSingleDoc: () => ampdoc, - }); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + afterEach(() => { + toggleExperiment( + interceptionEnabledWin, + 'untrusted-xhr-interception', + false + ); + }); - return xhr - .fetch('https://cdn.ampproject.org') - .then(() => expect(sendMessageStub).to.not.have.been.called); + it('should not intercept if AMP doc is not single', () => { + const ampdoc = { + getRootNode: () => optedInDoc, + whenFirstVisible: () => Promise.resolve(), + }; + ampdocServiceForStub.returns({ + isSingleDoc: () => false, + getAmpDoc: () => ampdoc, + getSingleDoc: () => ampdoc, }); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - it('should not intercept if AMP doc does not opt in', () => { - const nonOptedInDoc = - window.document.implementation.createHTMLDocument(''); - const ampdoc = { - getRootNode: () => nonOptedInDoc, - whenFirstVisible: () => Promise.resolve(), - }; - ampdocServiceForStub.returns({ - isSingleDoc: () => true, - getAmpDoc: () => ampdoc, - getSingleDoc: () => ampdoc, - }); - - const xhr = xhrServiceForTesting(interceptionEnabledWin); + return xhr + .fetch('https://cdn.ampproject.org') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - return xhr - .fetch('https://cdn.ampproject.org') - .then(() => expect(sendMessageStub).to.not.have.been.called); + it('should not intercept if AMP doc does not opt in', () => { + const nonOptedInDoc = + window.document.implementation.createHTMLDocument(''); + const ampdoc = { + getRootNode: () => nonOptedInDoc, + whenFirstVisible: () => Promise.resolve(), + }; + ampdocServiceForStub.returns({ + isSingleDoc: () => true, + getAmpDoc: () => ampdoc, + getSingleDoc: () => ampdoc, }); - it('should not intercept if viewer is not capable', () => { - env.sandbox - .stub(viewer, 'hasCapability') - .withArgs('xhrInterceptor') - .returns(false); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://cdn.ampproject.org') - .then(() => expect(sendMessageStub).to.not.have.been.called); - }); + return xhr + .fetch('https://cdn.ampproject.org') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - it('should not intercept if viewer untrusted and non-dev mode', () => { - env.sandbox - .stub(viewer, 'isTrustedViewer') - .returns(Promise.resolve(false)); - interceptionEnabledWin.AMP_DEV_MODE = false; + it('should not intercept if viewer is not capable', () => { + env.sandbox + .stub(viewer, 'hasCapability') + .withArgs('xhrInterceptor') + .returns(false); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + return xhr + .fetch('https://cdn.ampproject.org') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - return xhr - .fetch('https://cdn.ampproject.org') - .then(() => expect(sendMessageStub).to.not.have.been.called); - }); + it('should not intercept if viewer untrusted and non-dev mode', () => { + env.sandbox + .stub(viewer, 'isTrustedViewer') + .returns(Promise.resolve(false)); + interceptionEnabledWin.AMP_DEV_MODE = false; - it('should not intercept a 1p cdn from subdomain', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://subdomain-model.cdn.ampproject.org/ww.js') - .then(() => expect(sendMessageStub).to.not.have.been.called); - }); + return xhr + .fetch('https://cdn.ampproject.org') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - it('should not intercept a 1p cdn resource', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should not intercept a 1p cdn from subdomain', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://cdn.ampproject.org/ww.js') - .then(() => expect(sendMessageStub).to.not.have.been.called); - }); + return xhr + .fetch('https://subdomain-model.cdn.ampproject.org/ww.js') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - it('should intercept if viewer untrusted but in local dev mode', () => { - env.sandbox - .stub(viewer, 'isTrustedViewer') - .returns(Promise.resolve(false)); - env.sandbox.stub(mode, 'getMode').returns({localDev: true}); + it('should not intercept a 1p cdn resource', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + return xhr + .fetch('https://cdn.ampproject.org/ww.js') + .then(() => expect(sendMessageStub).to.not.have.been.called); + }); - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then(() => expect(sendMessageStub).to.have.been.called); - }); + it('should intercept if viewer untrusted but in local dev mode', () => { + env.sandbox + .stub(viewer, 'isTrustedViewer') + .returns(Promise.resolve(false)); + env.sandbox.stub(mode, 'getMode').returns({localDev: true}); - it('should intercept if untrusted-xhr-interception experiment enabled', () => { - env.sandbox - .stub(viewer, 'isTrustedViewer') - .returns(Promise.resolve(false)); - env.sandbox.stub(mode, 'getMode').returns({localDev: false}); - env.sandbox - .stub(viewer, 'hasCapability') - .withArgs('xhrInterceptor') - .returns(true); - toggleExperiment( - interceptionEnabledWin, - 'untrusted-xhr-interception', - true - ); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then(() => expect(sendMessageStub).to.have.been.called); + }); - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then(() => expect(sendMessageStub).to.have.been.called); - }); + it('should intercept if untrusted-xhr-interception experiment enabled', () => { + env.sandbox + .stub(viewer, 'isTrustedViewer') + .returns(Promise.resolve(false)); + env.sandbox.stub(mode, 'getMode').returns({localDev: false}); + env.sandbox + .stub(viewer, 'hasCapability') + .withArgs('xhrInterceptor') + .returns(true); + toggleExperiment( + interceptionEnabledWin, + 'untrusted-xhr-interception', + true + ); + + const xhr = xhrServiceForTesting(interceptionEnabledWin); + + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then(() => expect(sendMessageStub).to.have.been.called); + }); - it('should intercept if non-dev mode but viewer trusted', () => { - env.sandbox - .stub(viewer, 'isTrustedViewer') - .returns(Promise.resolve(true)); - interceptionEnabledWin.AMP_DEV_MODE = false; + it('should intercept if non-dev mode but viewer trusted', () => { + env.sandbox + .stub(viewer, 'isTrustedViewer') + .returns(Promise.resolve(true)); + interceptionEnabledWin.AMP_DEV_MODE = false; - const xhr = xhrServiceForTesting(interceptionEnabledWin); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then(() => expect(sendMessageStub).to.have.been.called); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then(() => expect(sendMessageStub).to.have.been.called); + }); - it('should send viewer message named `xhr`', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should send viewer message named `xhr`', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then(() => - expect(sendMessageStub).to.have.been.calledWithMatch( - 'xhr', - env.sandbox.match.any - ) - ); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then(() => + expect(sendMessageStub).to.have.been.calledWithMatch( + 'xhr', + env.sandbox.match.any + ) + ); + }); - it('should post correct structurally-cloneable GET request', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should post correct structurally-cloneable GET request', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); + + return xhr.fetch('https://www.some-url.org/some-resource/').then(() => + expect(sendMessageStub).to.have.been.calledWithMatch( + env.sandbox.match.any, + { + originalRequest: { + input: + 'https://www.some-url.org/some-resource/' + + '?__amp_source_origin=https%3A%2F%2Facme.com', + init: { + headers: {}, + method: 'GET', + }, + }, + } + ) + ); + }); + + it('should post correct structurally-cloneable JSON request', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr.fetch('https://www.some-url.org/some-resource/').then(() => + return xhr + .fetch('https://www.some-url.org/some-resource/', { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'}, + body: JSON.stringify({a: 42, b: [24, true]}), + }) + .then(() => expect(sendMessageStub).to.have.been.calledWithMatch( env.sandbox.match.any, { @@ -835,334 +851,297 @@ describes.sandboxed 'https://www.some-url.org/some-resource/' + '?__amp_source_origin=https%3A%2F%2Facme.com', init: { - headers: {}, - method: 'GET', + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + body: '{"a":42,"b":[24,true]}', + method: 'POST', }, }, } ) ); - }); + }); - it('should post correct structurally-cloneable JSON request', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should post correct structurally-cloneable FormData request', () => { + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/', { - method: 'POST', - headers: {'Content-Type': 'application/json;charset=utf-8'}, - body: JSON.stringify({a: 42, b: [24, true]}), - }) - .then(() => - expect(sendMessageStub).to.have.been.calledWithMatch( - env.sandbox.match.any, - { - originalRequest: { - input: - 'https://www.some-url.org/some-resource/' + - '?__amp_source_origin=https%3A%2F%2Facme.com', - init: { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - body: '{"a":42,"b":[24,true]}', - method: 'POST', + const fakeWin = null; + env.sandbox.stub(Services, 'platformFor').returns({ + isIos() { + return false; + }, + }); + + const formData = createFormDataWrapper(fakeWin); + formData.append('a', 42); + formData.append('b', '24'); + formData.append('b', true); + + return xhr + .fetch('https://www.some-url.org/some-resource/', { + method: 'POST', + body: formData, + }) + .then(() => + expect(sendMessageStub).to.have.been.calledWithMatch( + env.sandbox.match.any, + { + originalRequest: { + input: + 'https://www.some-url.org/some-resource/' + + '?__amp_source_origin=https%3A%2F%2Facme.com', + init: { + headers: { + 'Content-Type': 'multipart/form-data;charset=utf-8', }, + body: [ + ['a', '42'], + ['b', '24'], + ['b', 'true'], + ], + method: 'POST', }, - } - ) - ); - }); + }, + } + ) + ); + }); - it('should post correct structurally-cloneable FormData request', () => { - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should be rejected when response undefined', () => { + expectAsyncConsoleError(/Object expected/); + sendMessageStub.returns(Promise.resolve()); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - const fakeWin = null; - env.sandbox.stub(Services, 'platformFor').returns({ - isIos() { - return false; - }, - }); + return expect( + xhr.fetch('https://www.some-url.org/some-resource/') + ).to.eventually.be.rejectedWith(Error, 'Object expected: undefined'); + }); - const formData = createFormDataWrapper(fakeWin); - formData.append('a', 42); - formData.append('b', '24'); - formData.append('b', true); + it('should be rejected when response null', () => { + expectAsyncConsoleError(/Object expected/); + sendMessageStub.returns(Promise.resolve(null)); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/', { - method: 'POST', - body: formData, - }) - .then(() => - expect(sendMessageStub).to.have.been.calledWithMatch( - env.sandbox.match.any, - { - originalRequest: { - input: - 'https://www.some-url.org/some-resource/' + - '?__amp_source_origin=https%3A%2F%2Facme.com', - init: { - headers: { - 'Content-Type': 'multipart/form-data;charset=utf-8', - }, - body: [ - ['a', '42'], - ['b', '24'], - ['b', 'true'], - ], - method: 'POST', - }, - }, - } - ) - ); - }); + return expect( + xhr.fetch('https://www.some-url.org/some-resource/') + ).to.eventually.be.rejectedWith(Error, 'Object expected: null'); + }); - it('should be rejected when response undefined', () => { - expectAsyncConsoleError(/Object expected/); - sendMessageStub.returns(Promise.resolve()); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should be rejected when response is string', () => { + expectAsyncConsoleError(/Object expected/); + sendMessageStub.returns(Promise.resolve('response text')); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return expect( - xhr.fetch('https://www.some-url.org/some-resource/') - ).to.eventually.be.rejectedWith(Error, 'Object expected: undefined'); - }); + return expect( + xhr.fetch('https://www.some-url.org/some-resource/') + ).to.eventually.be.rejectedWith(Error, 'Object expected: response text'); + }); - it('should be rejected when response null', () => { - expectAsyncConsoleError(/Object expected/); - sendMessageStub.returns(Promise.resolve(null)); + describe('when native Response type is available', () => { + beforeEach(() => (interceptionEnabledWin.Response = window.Response)); + + it('should return correct non-document response', () => { + sendMessageStub.returns( + Promise.resolve({ + body: '{"content":32}', + init: { + status: 242, + statusText: 'Magic status', + headers: [ + ['a', 2], + ['b', false], + ], + }, + }) + ); const xhr = xhrServiceForTesting(interceptionEnabledWin); - return expect( - xhr.fetch('https://www.some-url.org/some-resource/') - ).to.eventually.be.rejectedWith(Error, 'Object expected: null'); + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then((response) => { + expect(response.headers.get('a')).to.equal('2'); + expect(response.headers.get('b')).to.equal('false'); + expect(response).to.have.property('ok').that.is.true; + expect(response).to.have.property('status').that.equals(242); + expect(response) + .to.have.property('statusText') + .that.equals('Magic status'); + return expect(response.text()).to.eventually.equal( + '{"content":32}' + ); + return expect(response.json()).to.eventually.deep.equal({ + content: 32, + }); + }); }); + }); - it('should be rejected when response is string', () => { - expectAsyncConsoleError(/Object expected/); - sendMessageStub.returns(Promise.resolve('response text')); + describe('when native Response type is unavailable', () => { + beforeEach(() => (interceptionEnabledWin.Response = undefined)); + + it('should return correct non-document response', () => { + sendMessageStub.returns( + Promise.resolve({ + body: '{"content":32}', + init: { + status: 242, + statusText: 'Magic status', + headers: [ + ['a', 2], + ['b', false], + ], + }, + }) + ); const xhr = xhrServiceForTesting(interceptionEnabledWin); - return expect( - xhr.fetch('https://www.some-url.org/some-resource/') - ).to.eventually.be.rejectedWith( - Error, - 'Object expected: response text' - ); + return xhr + .fetch('https://www.some-url.org/some-resource/') + .then((response) => { + expect(response.headers.get('a')).to.equal('2'); + expect(response.headers.get('b')).to.equal('false'); + expect(response).to.have.property('ok').that.is.true; + expect(response).to.have.property('status').that.equals(242); + return expect(response.json()).to.eventually.deep.equal({ + content: 32, + }); + }); }); - describe('when native Response type is available', () => { - beforeEach(() => (interceptionEnabledWin.Response = window.Response)); + it('should return default response when body/init missing', () => { + sendMessageStub.returns(Promise.resolve({})); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - it('should return correct non-document response', () => { - sendMessageStub.returns( - Promise.resolve({ - body: '{"content":32}', - init: { - status: 242, - statusText: 'Magic status', - headers: [ - ['a', 2], - ['b', false], - ], - }, - }) - ); - const xhr = xhrServiceForTesting(interceptionEnabledWin); - - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then((response) => { - expect(response.headers.get('a')).to.equal('2'); - expect(response.headers.get('b')).to.equal('false'); - expect(response).to.have.property('ok').that.is.true; - expect(response).to.have.property('status').that.equals(242); - expect(response) - .to.have.property('statusText') - .that.equals('Magic status'); - return expect(response.text()).to.eventually.equal( - '{"content":32}' - ); - return expect(response.json()).to.eventually.deep.equal({ - content: 32, - }); - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + expect(response.headers.get('a')).to.be.null; + expect(response.headers.has('a')).to.be.false; + expect(response).to.have.property('ok').that.is.true; + expect(response).to.have.property('status').that.equals(200); + return expect(response.text()).to.eventually.be.empty; + }); }); - describe('when native Response type is unavailable', () => { - beforeEach(() => (interceptionEnabledWin.Response = undefined)); - - it('should return correct non-document response', () => { - sendMessageStub.returns( - Promise.resolve({ - body: '{"content":32}', - init: { - status: 242, - statusText: 'Magic status', - headers: [ - ['a', 2], - ['b', false], - ], - }, - }) - ); - const xhr = xhrServiceForTesting(interceptionEnabledWin); - - return xhr - .fetch('https://www.some-url.org/some-resource/') - .then((response) => { - expect(response.headers.get('a')).to.equal('2'); - expect(response.headers.get('b')).to.equal('false'); - expect(response).to.have.property('ok').that.is.true; - expect(response).to.have.property('status').that.equals(242); - return expect(response.json()).to.eventually.deep.equal({ - content: 32, - }); - }); - }); + it('should return default response when status/headers missing', () => { + sendMessageStub.returns(Promise.resolve({body: '', init: {}})); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - it('should return default response when body/init missing', () => { - sendMessageStub.returns(Promise.resolve({})); - const xhr = xhrServiceForTesting(interceptionEnabledWin); - - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - expect(response.headers.get('a')).to.be.null; - expect(response.headers.has('a')).to.be.false; - expect(response).to.have.property('ok').that.is.true; - expect(response).to.have.property('status').that.equals(200); - return expect(response.text()).to.eventually.be.empty; - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + expect(response.headers.get('a')).to.be.null; + expect(response.headers.has('a')).to.be.false; + expect(response).to.have.property('status').that.equals(200); + }); + }); - it('should return default response when status/headers missing', () => { - sendMessageStub.returns(Promise.resolve({body: '', init: {}})); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should convert body to string', () => { + sendMessageStub.returns(Promise.resolve({body: 32})); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - expect(response.headers.get('a')).to.be.null; - expect(response.headers.has('a')).to.be.false; - expect(response).to.have.property('status').that.equals(200); - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + return expect(response.text()).to.eventually.equal('32'); + }); + }); - it('should convert body to string', () => { - sendMessageStub.returns(Promise.resolve({body: 32})); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should convert status to int', () => { + sendMessageStub.returns(Promise.resolve({init: {status: '209.6'}})); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - return expect(response.text()).to.eventually.equal('32'); - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + return expect(response).to.have.property('status').that.equals(209); + }); + }); - it('should convert status to int', () => { - sendMessageStub.returns(Promise.resolve({init: {status: '209.6'}})); - const xhr = xhrServiceForTesting(interceptionEnabledWin); + it('should convert headers to string', () => { + sendMessageStub.returns( + Promise.resolve({ + init: { + headers: [ + [1, true], + [false, NaN], + [undefined, null], + ], + }, + }) + ); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - return expect(response) - .to.have.property('status') - .that.equals(209); - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + expect(response.headers.get(1)).to.equal('true'); + expect(response.headers.get('false')).to.equal('NaN'); + expect(response.headers.get('undefined')).to.equal('null'); + }); + }); - it('should convert headers to string', () => { - sendMessageStub.returns( - Promise.resolve({ - init: { - headers: [ - [1, true], - [false, NaN], - [undefined, null], - ], - }, - }) - ); - const xhr = xhrServiceForTesting(interceptionEnabledWin); - - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - expect(response.headers.get(1)).to.equal('true'); - expect(response.headers.get('false')).to.equal('NaN'); - expect(response.headers.get('undefined')).to.equal('null'); - }); - }); + it('should support case-insensitive header search', () => { + sendMessageStub.returns( + Promise.resolve({ + init: { + headers: [ + ['Content-Type', 'text/plain'], + ['ACCEPT', 'text/plain'], + ['x-amp-custom', 'foo'], + ], + }, + }) + ); + const xhr = xhrServiceForTesting(interceptionEnabledWin); - it('should support case-insensitive header search', () => { - sendMessageStub.returns( - Promise.resolve({ - init: { - headers: [ - ['Content-Type', 'text/plain'], - ['ACCEPT', 'text/plain'], - ['x-amp-custom', 'foo'], - ], - }, - }) - ); - const xhr = xhrServiceForTesting(interceptionEnabledWin); - - return xhr - .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) - .then((response) => { - expect(response.headers.get('content-type')).to.equal( - 'text/plain' - ); - expect(response.headers.get('Accept')).to.equal('text/plain'); - expect(response.headers.get('X-AMP-CUSTOM')).to.equal('foo'); - }); - }); + return xhr + .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) + .then((response) => { + expect(response.headers.get('content-type')).to.equal('text/plain'); + expect(response.headers.get('Accept')).to.equal('text/plain'); + expect(response.headers.get('X-AMP-CUSTOM')).to.equal('foo'); + }); }); }); + }); - describe('#xssiJson', () => { - let xhr; - beforeEach(() => { - xhr = xhrServiceForTesting(polyfillWin); - setupMockXhr(); - }); + describe('#xssiJson', () => { + let xhr; + beforeEach(() => { + xhr = xhrServiceForTesting(polyfillWin); + setupMockXhr(); + }); - it('should call response.json() if prefix is either missing or the empty string', () => { - xhrCreated.then((mock) => mock.respond(200, [], '{a: 1}')); - const response = { - json: () => Promise.resolve(), - text: () => Promise.reject(new Error('should not be called')), - }; + it('should call response.json() if prefix is either missing or the empty string', () => { + xhrCreated.then((mock) => mock.respond(200, [], '{a: 1}')); + const response = { + json: () => Promise.resolve(), + text: () => Promise.reject(new Error('should not be called')), + }; - return Promise.all([ - xhr.xssiJson(response), - xhr.xssiJson(response, ''), - ]); - }); + return Promise.all([xhr.xssiJson(response), xhr.xssiJson(response, '')]); + }); - it('should not strip characters if the prefix is not present', () => { - xhrCreated.then((mock) => mock.respond(200, [], '{"a": 1}')); - return xhr - .fetchJson('/abc') - .then((res) => xhr.xssiJson(res, 'while(1)')) - .then((json) => { - expect(json).to.be.deep.equal({a: 1}); - }); - }); + it('should not strip characters if the prefix is not present', () => { + xhrCreated.then((mock) => mock.respond(200, [], '{"a": 1}')); + return xhr + .fetchJson('/abc') + .then((res) => xhr.xssiJson(res, 'while(1)')) + .then((json) => { + expect(json).to.be.deep.equal({a: 1}); + }); + }); - it('should strip prefix from the response text if prefix is present', () => { - xhrCreated.then((mock) => mock.respond(200, [], 'while(1){"a": 1}')); - return xhr - .fetchJson('/abc') - .then((res) => xhr.xssiJson(res, 'while(1)')) - .then((json) => { - expect(json).to.be.deep.equal({a: 1}); - }); - }); + it('should strip prefix from the response text if prefix is present', () => { + xhrCreated.then((mock) => mock.respond(200, [], 'while(1){"a": 1}')); + return xhr + .fetchJson('/abc') + .then((res) => xhr.xssiJson(res, 'while(1)')) + .then((json) => { + expect(json).to.be.deep.equal({a: 1}); + }); }); }); +}); diff --git a/test/unit/utils/test-dom-writer.js b/test/unit/utils/test-dom-writer.js index eaf865f04a2b..e55c89cf391e 100644 --- a/test/unit/utils/test-dom-writer.js +++ b/test/unit/utils/test-dom-writer.js @@ -1,136 +1,133 @@ import {DomWriterBulk, DomWriterStreamer} from '#utils/dom-writer'; describes.fakeWin('DomWriterStreamer', {amp: true}, (env) => { - describe - .configure() - .skipFirefox() - .run('DomWriterStreamer', () => { - let win; - let writer; - let onBodySpy, onBodyChunkSpy; - let onBodyPromise, onBodyChunkPromiseResolver, onEndPromise; - - beforeEach(() => { - win = env.win; - writer = new DomWriterStreamer(win); - onBodySpy = env.sandbox.spy(); - onBodyChunkSpy = env.sandbox.spy(); - onBodyPromise = new Promise((resolve) => { - writer.onBody((parsedDoc) => { - resolve(parsedDoc.body); - onBodySpy(); - return win.document.body; - }); - }); - writer.onBodyChunk(() => { - if (onBodyChunkPromiseResolver) { - onBodyChunkPromiseResolver(); - onBodyChunkPromiseResolver = null; - } - onBodyChunkSpy(); - }); - onEndPromise = new Promise((resolve) => { - writer.onEnd(resolve); + describe('DomWriterStreamer', () => { + let win; + let writer; + let onBodySpy, onBodyChunkSpy; + let onBodyPromise, onBodyChunkPromiseResolver, onEndPromise; + + beforeEach(() => { + win = env.win; + writer = new DomWriterStreamer(win); + onBodySpy = env.sandbox.spy(); + onBodyChunkSpy = env.sandbox.spy(); + onBodyPromise = new Promise((resolve) => { + writer.onBody((parsedDoc) => { + resolve(parsedDoc.body); + onBodySpy(); + return win.document.body; }); }); + writer.onBodyChunk(() => { + if (onBodyChunkPromiseResolver) { + onBodyChunkPromiseResolver(); + onBodyChunkPromiseResolver = null; + } + onBodyChunkSpy(); + }); + onEndPromise = new Promise((resolve) => { + writer.onEnd(resolve); + }); + }); - function waitForNextBodyChunk() { - return new Promise((resolve) => { - onBodyChunkPromiseResolver = resolve; - }); - } - - it('should complete when writer has been closed', () => { - writer.close(); - return onEndPromise.then(() => { - expect(onBodySpy).to.be.calledOnce; - env.flushVsync(); - expect(onBodyChunkSpy).to.not.be.called; - }); + function waitForNextBodyChunk() { + return new Promise((resolve) => { + onBodyChunkPromiseResolver = resolve; + }); + } + + it('should complete when writer has been closed', () => { + writer.close(); + return onEndPromise.then(() => { + expect(onBodySpy).to.be.calledOnce; + env.flushVsync(); + expect(onBodyChunkSpy).to.not.be.called; }); + }); - it('should resolve body as soon as available', () => { - writer.write(''); - expect(onBodySpy).to.not.be.called; - return onBodyPromise.then((body) => { - expect(body.getAttribute('class')).to.equal('b'); - expect(onBodySpy).to.be.calledOnce; - }); + it('should resolve body as soon as available', () => { + writer.write(''); + expect(onBodySpy).to.not.be.called; + return onBodyPromise.then((body) => { + expect(body.getAttribute('class')).to.equal('b'); + expect(onBodySpy).to.be.calledOnce; }); + }); - it('should schedule body chunk', () => { - writer.write(''); - return onBodyPromise.then(() => { + it('should schedule body chunk', () => { + writer.write(''); + return onBodyPromise.then(() => { + expect(onBodySpy).to.be.calledOnce; + writer.write(''); + expect(onBodyChunkSpy).to.not.be.called; + return waitForNextBodyChunk().then(() => { + env.flushVsync(); expect(onBodySpy).to.be.calledOnce; - writer.write(''); - expect(onBodyChunkSpy).to.not.be.called; + expect(onBodyChunkSpy).to.be.calledOnce; + expect(win.document.body.querySelector('child')).to.exist; + + writer.write(''); return waitForNextBodyChunk().then(() => { env.flushVsync(); - expect(onBodySpy).to.be.calledOnce; - expect(onBodyChunkSpy).to.be.calledOnce; - expect(win.document.body.querySelector('child')).to.exist; - - writer.write(''); - return waitForNextBodyChunk().then(() => { - env.flushVsync(); - expect(win.document.body.querySelector('child2')).to.exist; - }); + expect(win.document.body.querySelector('child2')).to.exist; }); }); }); + }); - it('should schedule several body chunks together', () => { - writer.write(''); - return onBodyPromise.then(() => { - expect(onBodySpy).to.be.calledOnce; - writer.write(''); - expect(onBodyChunkSpy).to.not.be.called; - const promise = waitForNextBodyChunk(); - writer.write(''); - return promise.then(() => { - expect(onBodyChunkSpy).to.be.calledOnce; - expect(win.document.body.querySelector('child')).to.exist; - expect(win.document.body.querySelector('child2')).to.exist; - }); + it('should schedule several body chunks together', () => { + writer.write(''); + return onBodyPromise.then(() => { + expect(onBodySpy).to.be.calledOnce; + writer.write(''); + expect(onBodyChunkSpy).to.not.be.called; + const promise = waitForNextBodyChunk(); + writer.write(''); + return promise.then(() => { + expect(onBodyChunkSpy).to.be.calledOnce; + expect(win.document.body.querySelector('child')).to.exist; + expect(win.document.body.querySelector('child2')).to.exist; }); }); + }); - it('should not parse noscript as markup', () => { - writer.write( - '' - ); - return waitForNextBodyChunk().then(() => { - expect(win.document.body.querySelector('child1')).to.exist; - expect(win.document.body.querySelector('child2')).not.to.exist; - writer.write(''); - writer.write(''); - writer.close(); - env.flushVsync(); + it('should not parse noscript as markup', () => { + writer.write( + '' + ); + return waitForNextBodyChunk().then(() => { + expect(win.document.body.querySelector('child1')).to.exist; + expect(win.document.body.querySelector('child2')).not.to.exist; + writer.write(''); + writer.write(''); + writer.close(); + env.flushVsync(); - return onEndPromise.then(() => { - expect(win.document.body.querySelector('child3')).not.to.exist; - expect(win.document.body.querySelector('child4')).to.exist; - }); + return onEndPromise.then(() => { + expect(win.document.body.querySelector('child3')).not.to.exist; + expect(win.document.body.querySelector('child4')).to.exist; }); }); + }); - it('should not parse noscript as markup across writes', () => { - writer.write(''); - writer.write(''); - writer.close(); - env.flushVsync(); + it('should not parse noscript as markup across writes', () => { + writer.write(''); + writer.write(''); + writer.close(); + env.flushVsync(); - return onEndPromise.then(() => { - expect(win.document.body.querySelector('child1')).to.exist; - expect(win.document.body.querySelector('child2')).not.to.exist; - expect(win.document.body.querySelector('child3')).to.exist; - }); + return onEndPromise.then(() => { + expect(win.document.body.querySelector('child1')).to.exist; + expect(win.document.body.querySelector('child2')).not.to.exist; + expect(win.document.body.querySelector('child3')).to.exist; }); }); }); + }); }); describes.fakeWin('DomWriterBulk', {amp: true}, (env) => { diff --git a/test/visual-diff/visual-tests.jsonc b/test/visual-diff/visual-tests.jsonc index 1fcf90083016..75e0571bf36e 100644 --- a/test/visual-diff/visual-tests.jsonc +++ b/test/visual-diff/visual-tests.jsonc @@ -618,6 +618,12 @@ "name": "amp-form", "interactive_tests": "examples/visual-tests/amp-form/amp-form.js" }, + { + "url": "examples/visual-tests/amp-form/amp-form-server-error.amp.html", + "name": "amp-form (server error)", + "interactive_tests": "examples/visual-tests/amp-form/amp-form-server-error.js", + "no_base_test": true + }, { "url": "examples/visual-tests/amp-accordion/amp-accordion.html", "name": "amp-accordion: page loads",