diff --git a/.github/ISSUE_TEMPLATE/cherry_pick_template.md b/.github/ISSUE_TEMPLATE/cherry_pick_template.md index d29afe0edaf8..b37c6689f6bb 100644 --- a/.github/ISSUE_TEMPLATE/cherry_pick_template.md +++ b/.github/ISSUE_TEMPLATE/cherry_pick_template.md @@ -1,6 +1,6 @@ --- name: Cherry-pick template -about: Used to request a cherry-pick. See bit.ly/amp-cherry-pick +about: Used to request a cherry-pick. See go.amp.dev/cherry-picks title: "\U0001F338 Cherry-pick request for # into # (Pending)" labels: 'Type: Release' --- diff --git a/.github/ISSUE_TEMPLATE/release-tracking-issue.md b/.github/ISSUE_TEMPLATE/release-tracking-issue.md index 1aedf6f6994e..d096fa316657 100644 --- a/.github/ISSUE_TEMPLATE/release-tracking-issue.md +++ b/.github/ISSUE_TEMPLATE/release-tracking-issue.md @@ -49,4 +49,4 @@ If you perform cherry picks, add/update the checkboxes above as needed e.g. See the [release documentation](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md) for more information on the release process, including how to test changes in the Dev Channel. -If you find a bug in this build, please file an [issue](https://github.com/ampproject/amphtml/issues/new). If you believe the bug should be fixed in this build, follow the instructions in the [cherry picks documentation](https://bit.ly/amp-cherry-pick). +If you find a bug in this build, please file an [issue](https://github.com/ampproject/amphtml/issues/new). If you believe the bug should be fixed in this build, follow the instructions in the [cherry picks documentation](https://go.amp.dev/cherry-picks). diff --git a/.github/ISSUE_TEMPLATE/release_issue_template.md b/.github/ISSUE_TEMPLATE/release_issue_template.md index 5834b8c12f75..a8a6aa383cce 100644 --- a/.github/ISSUE_TEMPLATE/release_issue_template.md +++ b/.github/ISSUE_TEMPLATE/release_issue_template.md @@ -34,4 +34,4 @@ If you perform cherry picks, add/update the checkboxes above as needed e.g. See the [release documentation](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md) for more information on the release process, including how to test changes in the Dev Channel. -If you find a bug in this build, please file an [issue](https://github.com/ampproject/amphtml/issues/new). If you believe the bug should be fixed in this build, follow the instructions in the [cherry picks documentation](https://bit.ly/amp-cherry-pick). +If you find a bug in this build, please file an [issue](https://github.com/ampproject/amphtml/issues/new). If you believe the bug should be fixed in this build, follow the instructions in the [cherry picks documentation](https://go.amp.dev/cherry-picks). diff --git a/.renovaterc.json b/.renovaterc.json index 2e487ecad398..a2a3e8157792 100644 --- a/.renovaterc.json +++ b/.renovaterc.json @@ -12,6 +12,7 @@ "package.json", "build-system/tasks/e2e/package.json", "build-system/tasks/visual-diff/package.json", + "src/purifier/package.json", "validator/package.json" ] } diff --git a/ads/readmo.js b/ads/readmo.js index 9e8b0cfde907..12685b07313c 100644 --- a/ads/readmo.js +++ b/ads/readmo.js @@ -29,7 +29,7 @@ export function readmo(global, data) { const {section, module, sponsoredByLabel, infinite, title, url} = data; - window.publisherUrl = url; + global.publisherUrl = url; (global.readmo = global.readmo || []).push({ section, @@ -41,21 +41,6 @@ export function readmo(global, data) { amp: true, }); - global.context.observeIntersection( - entries => { - entries.forEach(entry => { - if (global.Readmo) { - global.Readmo.onViewChange({ - intersectionRatio: entry.intersectionRatio, - }); - } - }); - }, - { - threshold: [0, 0.5, 1], - } - ); - loadScript(global, 'https://s.yimg.com/dy/ads/readmo.js', () => global.context.renderStart() ); diff --git a/ads/readmo.md b/ads/readmo.md index 1aee88f7c56f..b8af4ee4c951 100644 --- a/ads/readmo.md +++ b/ads/readmo.md @@ -28,7 +28,7 @@ ReadMo only requires a section code to run. Please work your account manager to height="400" type="readmo" layout="responsive" - data-url="https://yoursite.com" + data-url="https://yourdomain.com" data-infinite="true" data-section="1234567" > diff --git a/build-system/compile/bundles.config.js b/build-system/compile/bundles.config.js index 1c55ef4dd65f..1eb94f427590 100644 --- a/build-system/compile/bundles.config.js +++ b/build-system/compile/bundles.config.js @@ -489,7 +489,7 @@ exports.extensionBundles = [ }, { name: 'amp-date-display', - version: '0.1', + version: ['0.1', '0.2'], latestVersion: '0.1', type: TYPES.MISC, }, @@ -753,7 +753,7 @@ exports.extensionBundles = [ }, { name: 'amp-next-page', - version: ['0.1', '0.2'], + version: ['0.1', '1.0'], latestVersion: '0.1', options: {hasCss: true}, type: TYPES.MISC, diff --git a/build-system/compile/check-for-unknown-deps.js b/build-system/compile/check-for-unknown-deps.js index b46b5422b7aa..3f2e743d1a57 100644 --- a/build-system/compile/check-for-unknown-deps.js +++ b/build-system/compile/check-for-unknown-deps.js @@ -17,7 +17,7 @@ const log = require('fancy-log'); const through = require('through2'); -const {red, cyan} = require('ansi-colors'); +const {red, cyan, yellow} = require('ansi-colors'); /** * Searches for the identifier "$$module$", which Closure uses to uniquely @@ -44,6 +44,7 @@ exports.checkForUnknownDeps = function() { red('Error:'), `Unknown dependency ${cyan(match[0])} found in ${cyan(file.relative)}` ); + log(yellow(contents)); const err = new Error('Compilation failed due to unknown dependency'); err.showStack = false; cb(err, file); diff --git a/build-system/compile/compile.js b/build-system/compile/compile.js index 1dc65f53d972..6d17ec626eb1 100644 --- a/build-system/compile/compile.js +++ b/build-system/compile/compile.js @@ -139,6 +139,7 @@ function compile( 'third_party/web-animations-externs/web_animations.js', 'third_party/moment/moment.extern.js', 'third_party/react-externs/externs.js', + 'build-system/externs/preact.extern.js', ]; const define = [`VERSION=${internalRuntimeVersion}`]; if (argv.pseudo_names) { diff --git a/build-system/compile/sources.js b/build-system/compile/sources.js index 666040cd40fd..cc9f758f1891 100644 --- a/build-system/compile/sources.js +++ b/build-system/compile/sources.js @@ -51,6 +51,10 @@ const COMMON_GLOBS = [ 'node_modules/@ampproject/animations/dist/animations.mjs', 'node_modules/@ampproject/worker-dom/package.json', 'node_modules/@ampproject/worker-dom/dist/amp/main.mjs', + 'node_modules/preact/package.json', + 'node_modules/preact/dist/*.js', + 'node_modules/preact/hooks/package.json', + 'node_modules/preact/hooks/dist/*.js', ]; /** diff --git a/build-system/eslint-rules/no-import.js b/build-system/eslint-rules/no-import.js index e694330aaa8d..daf26d4640bd 100644 --- a/build-system/eslint-rules/no-import.js +++ b/build-system/eslint-rules/no-import.js @@ -14,19 +14,41 @@ * limitations under the License. */ 'use strict'; -const imports = ['sinon']; + +const imports = [ + {import: 'sinon', message: 'Importing sinon is forbidden'}, + { + import: 'preact', + message: + "Please import preact from 'src/preact'. This gives us type safety.", + }, + { + import: 'preact/hooks', + message: + "Please import preact/hooks from 'src/preact'. This gives us type safety.", + }, +]; + module.exports = function(context) { return { ImportDeclaration(node) { - const comments = context.getCommentsBefore(node); + const comments = context.getCommentsBefore(node.source); const ok = comments.some(comment => comment.value === 'OK'); if (ok) { return; } const name = node.source.value; - if (imports.includes(name)) { - context.report({node, message: `Importing ${name} is forbidden.`}); + + for (const forbidden of imports) { + if (name !== forbidden.import) { + continue; + } + + context.report({ + node, + message: forbidden.message, + }); } }, }; diff --git a/build-system/externs/preact.extern.js b/build-system/externs/preact.extern.js new file mode 100644 index 000000000000..1b2a37c981c6 --- /dev/null +++ b/build-system/externs/preact.extern.js @@ -0,0 +1,56 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @externs */ + +/** @const */ +var Preact = {} + +/** + * @typedef {function(!JsonObject):Preact.Renderable} + */ +Preact.FunctionalComponent + +/** + * @interface + */ +Preact.VNode = function() {} + +/** + * @interface + */ +Preact.Context = function() {} + +/** + * @param {!JsonObject} props + * @return {Preact.Renderable} + */ +Preact.Context.prototype.Provider = function(props) {}; + +/** + * @interface + */ +Preact.Context.prototype.Consumer = function() {}; + +/** + * @typedef {string|number|boolean|null|undefined} + */ +Preact.SimpleRenderable; + +/** + * @typedef {Preact.SimpleRenderable|!Preact.VNode|!Array>} + */ +Preact.Renderable; diff --git a/build-system/tasks/check-types.js b/build-system/tasks/check-types.js index 06e567b7f78e..368f7cfc7665 100644 --- a/build-system/tasks/check-types.js +++ b/build-system/tasks/check-types.js @@ -82,7 +82,7 @@ async function checkTypes() { { include3pDirectories: true, includePolyfills: true, - extraGlobs: ['src/inabox/*.js'], + extraGlobs: ['src/inabox/*.js', '!node_modules/preact'], typeCheckOnly: true, } ), diff --git a/build-system/tasks/e2e/describes-e2e.js b/build-system/tasks/e2e/describes-e2e.js index ad9b9ed9300d..a42315ca75dd 100644 --- a/build-system/tasks/e2e/describes-e2e.js +++ b/build-system/tasks/e2e/describes-e2e.js @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {RequestBankE2E} from './request-bank'; - // import to install chromedriver and geckodriver require('chromedriver'); // eslint-disable-line no-unused-vars require('geckodriver'); // eslint-disable-line no-unused-vars @@ -40,7 +38,7 @@ const {PuppeteerController} = require('./puppeteer-controller'); const SUB = ' '; const TEST_TIMEOUT = 40000; const SETUP_TIMEOUT = 30000; -const SETUP_RETRIES = 1; +const SETUP_RETRIES = 3; const DEFAULT_E2E_INITIAL_RECT = {width: 800, height: 600}; const supportedBrowsers = new Set(['chrome', 'firefox', 'safari']); /** @@ -49,7 +47,15 @@ const supportedBrowsers = new Set(['chrome', 'firefox', 'safari']); * {@link https://github.com/GoogleChrome/puppeteer/blob/master/experimental/puppeteer-firefox/README.md} */ const PUPPETEER_BROWSERS = new Set(['chrome']); -const REQUESTBANK_URL_PREFIX = 'http://localhost:8000'; + +/** + * Engine types for e2e testing. + * @enum {string} + */ +const EngineType = { + SELENIUM: 'selenium', + PUPPETEER: 'puppeteer', +}; /** * @typedef {{ @@ -124,9 +130,9 @@ async function createPuppeteer(opt_config = {}) { * @param {string} browserName * @param {!SeleniumConfigDef=} args * @param {?string} deviceName - * @return {!SeleniumDriver} + * @return {!WebDriver} */ -async function createSelenium(browserName, args = {}, deviceName) { +function createSelenium(browserName, args = {}, deviceName) { switch (browserName) { case 'safari': // Safari's only option is setTechnologyPreview @@ -139,13 +145,19 @@ async function createSelenium(browserName, args = {}, deviceName) { } } -async function createDriver(browserName, args, deviceName) { +/** + * + * @param {string} browserName + * @param {!SeleniumConfigDef=} args + * @param {?string} deviceName + * @return {!WebDriver} + */ +function createDriver(browserName, args, deviceName) { const capabilities = Capabilities[browserName](); const prefs = new logging.Preferences(); prefs.setLevel(logging.Type.PERFORMANCE, logging.Level.ALL); capabilities.setLoggingPrefs(prefs); - let builder; switch (browserName) { case 'firefox': const firefoxOptions = new firefox.Options(); @@ -154,21 +166,22 @@ async function createDriver(browserName, args, deviceName) { width: DEFAULT_E2E_INITIAL_RECT.width, height: DEFAULT_E2E_INITIAL_RECT.height, }); - builder = new Builder() + return new Builder() .forBrowser('firefox') - .setFirefoxOptions(firefoxOptions); + .setFirefoxOptions(firefoxOptions) + .build(); case 'chrome': const chromeOptions = new chrome.Options(capabilities); chromeOptions.addArguments(args); if (deviceName) { chromeOptions.setMobileEmulation({deviceName}); } - builder = new Builder() - .forBrowser('chrome') - .setChromeOptions(chromeOptions); + const driver = chrome.Driver.createSession(chromeOptions); + //TODO(estherkim): workaround. `onQuit()` was added in selenium-webdriver v4.0.0-alpha.5 + //which is also when `Server terminated early with status 1` began appearing. Coincidence? Maybe. + driver.onQuit = null; + return driver; } - - return await builder.build(); } /** @@ -371,7 +384,7 @@ function describeEnv(factory) { ? new Set(browsers.split(',').map(x => x.trim())) : supportedBrowsers; - if (engine === 'puppeteer') { + if (engine === EngineType.PUPPETEER) { const result = intersect(allowedBrowsers, PUPPETEER_BROWSERS); if (result.size === 0) { const browsersList = Array.from(allowedBrowsers).join(','); @@ -483,36 +496,20 @@ class EndToEndFixture { * @param {number} retries */ async setup(env, browserName, retries = 0) { - try { - const {testUrl, experiments = [], initialRect, deviceName} = this.spec; - const config = getConfig(); - const controller = await getController(config, browserName, deviceName); - const ampDriver = new AmpDriver(controller); - const requestBank = new RequestBankE2E(REQUESTBANK_URL_PREFIX, 'e2e'); - env.controller = controller; - env.ampDriver = ampDriver; - env.requestBank = requestBank; - const {environment} = env; - - installBrowserAssertions(controller.networkLogger); - - const url = new URL(testUrl); - if (experiments.length > 0) { - if (environment.includes('inabox')) { - // inabox experiments are toggled at server side using tag - url.searchParams.set('exp', experiments.join(',')); - } else { - // AMP doc experiments are toggled via cookies - await toggleExperiments(ampDriver, url.href, experiments); - } - } - - if (initialRect) { - const {width, height} = initialRect; - await controller.setWindowRect({width, height}); - } + const config = getConfig(); + const driver = getDriver(config, browserName, this.spec.deviceName); + const controller = + config.engine == EngineType.PUPPETEER + ? new PuppeteerController(driver) + : new SeleniumWebDriverController(driver); + const ampDriver = new AmpDriver(controller); + env.controller = controller; + env.ampDriver = ampDriver; + + installBrowserAssertions(controller.networkLogger); - await ampDriver.navigateToEnvironment(environment, url.href); + try { + await setUpTest(env, this.spec); } catch (ex) { if (retries > 0) { await this.setup(env, browserName, --retries); @@ -523,38 +520,58 @@ class EndToEndFixture { } async teardown(env) { - const {controller, requestBank} = env; - if (controller) { + const {controller} = env; + if (controller && controller.driver) { await controller.switchToParent(); await controller.dispose(); } - await requestBank.tearDown(); } } /** - * Get the controller object for the configured engine. + * Get the driver for the configured engine. * @param {!DescribesConfigDef} describesConfig * @param {string} browserName * @param {?string} deviceName - * @return {!SeleniumWebDriverController} + * @return {!ThenableWebDriver} */ -async function getController( - {engine = 'selenium', headless = false}, +function getDriver( + {engine = EngineType.SELENIUM, headless = false}, browserName, deviceName ) { - if (engine == 'puppeteer') { - const browser = await createPuppeteer({headless}); - return new PuppeteerController(browser); + if (engine == EngineType.PUPPETEER) { + return createPuppeteer({headless}); } - if (engine == 'selenium') { - const driver = await createSelenium(browserName, {headless}, deviceName); - return new SeleniumWebDriverController(driver); + if (engine == EngineType.SELENIUM) { + return createSelenium(browserName, {headless}, deviceName); } } +async function setUpTest( + {environment, ampDriver, controller}, + {testUrl, experiments = [], initialRect} +) { + const url = new URL(testUrl); + if (experiments.length > 0) { + if (environment.includes('inabox')) { + // inabox experiments are toggled at server side using tag + url.searchParams.set('exp', experiments.join(',')); + } else { + // AMP doc experiments are toggled via cookies + await toggleExperiments(ampDriver, url.href, experiments); + } + } + + if (initialRect) { + const {width, height} = initialRect; + await controller.setWindowRect({width, height}); + } + + await ampDriver.navigateToEnvironment(environment, url.href); +} + /** * Toggle the given experiments for the given test URL domain. * @param {!AmpDriver} ampDriver diff --git a/build-system/tasks/e2e/request-bank.js b/build-system/tasks/e2e/request-bank.js deleted file mode 100644 index 97b2bc830b6f..000000000000 --- a/build-system/tasks/e2e/request-bank.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright 2019 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import requestPromise from 'request-promise'; - -/** - * A server side temporary request storage which is useful for testing - * browser sent HTTP requests. This class is expected to be ran in NodeJS. - * See testing/test-helper.js for the implementation for running within Karma. - */ -export class RequestBankE2E { - /** - * @param {string} homeUrl The URL of the dev server. - * @param {?string} bankId The unique identifier of a specific instance to - * prevent interference between tests. - */ - constructor(homeUrl, bankId) { - /** @private {string} */ - this.homeUrl_ = homeUrl || 'http://localhost:8000'; - - /** @private {string} */ - this.bankId_ = bankId || (Date.now() + Math.random()).toString(32); - } - - /** - * Returns the URL for depositing a request. - * - * @param {number|string|undefined} requestId - * @return {string} - */ - getUrl(requestId) { - return `${this.homeUrl_}/amp4test/request-bank/${this.bankId_}/deposit/${requestId}/`; - } - - /** - * Returns a Promise that resolves when the request of given ID is deposited. - * The returned promise resolves to an JsonObject contains the request info: - * { - * url: string - * headers: JsonObject - * body: string - * } - * @param {number|string|undefined} requestId - * @return {Promise} - */ - withdraw(requestId) { - const url = `${this.homeUrl_}/amp4test/request-bank/${this.bankId_}/withdraw/${requestId}/`; - return this.fetch_(url).then(body => JSON.parse(body)); - } - - /** - * @return {Promise} - */ - tearDown() { - const url = `${this.homeUrl_}/amp4test/request-bank/${this.bankId_}/teardown/`; - return this.fetch_(url); - } - - /** - * @param {string} url - * @return {Promise} - */ - fetch_(url) { - return requestPromise.get({ - url, - timeout: 15000, - }); - } -} diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index 722c6d7b77f1..ff735fcb1f0e 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -315,7 +315,7 @@ const forbiddenTerms = { 'extensions/amp-fx-collection/0.1/providers/fx-provider.js', 'extensions/amp-list/0.1/amp-list.js', 'extensions/amp-next-page/0.1/next-page-service.js', - 'extensions/amp-next-page/0.2/visibility-observer.js', + 'extensions/amp-next-page/1.0/visibility-observer.js', 'extensions/amp-position-observer/0.1/amp-position-observer.js', 'extensions/amp-video-docking/0.1/amp-video-docking.js', 'src/service/position-observer/position-observer-impl.js', @@ -383,6 +383,7 @@ const forbiddenTerms = { 'extensions/amp-subscriptions/0.1/viewer-subscription-platform.js', 'extensions/amp-viewer-integration/0.1/highlight-handler.js', 'extensions/amp-consent/0.1/consent-ui.js', + 'extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js', // iframe-messaging-client.sendMessage '3p/iframe-messaging-client.js', diff --git a/build-system/tasks/visual-diff/package.json b/build-system/tasks/visual-diff/package.json index abdc0f1a2951..c6dc26d88e34 100644 --- a/build-system/tasks/visual-diff/package.json +++ b/build-system/tasks/visual-diff/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "description": "Gulp visual diff", "devDependencies": { - "@percy/agent": "0.20.6", + "@percy/agent": "0.20.8", "@percy/puppeteer": "1.0.8", "puppeteer": "2.0.0" } diff --git a/build-system/tasks/visual-diff/yarn.lock b/build-system/tasks/visual-diff/yarn.lock index 055262fe7293..3ddd7c756393 100644 --- a/build-system/tasks/visual-diff/yarn.lock +++ b/build-system/tasks/visual-diff/yarn.lock @@ -42,7 +42,7 @@ debug "^4.1.1" semver "^5.6.0" -"@oclif/command@^1.5.13", "@oclif/command@^1.5.3": +"@oclif/command@1.5.19", "@oclif/command@^1.5.13", "@oclif/command@^1.5.3": version "1.5.19" resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.5.19.tgz#13f472450eb83bd6c6871a164c03eadb5e1a07ed" integrity sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ== @@ -118,12 +118,12 @@ resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== -"@percy/agent@0.20.6": - version "0.20.6" - resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.20.6.tgz#2962ce28ac02f0dee9edd0d4663728c664ed0b5e" - integrity sha512-E+zfeZAz1r0i3CkAJlkg/fb3vBmh2fHqY3f7PLhSNt2aQONspoSLNDQ63XSioMRWMApLCEO5lxQ6k3KbxK91Cg== +"@percy/agent@0.20.8": + version "0.20.8" + resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.20.8.tgz#de2cfe54883992c59eff94592a66f23446f7f11e" + integrity sha512-Zqlpfsq2nTzrDsoAbwOuaycraWtfL9/YANY9kogcWBMSSB56XOgXPRnEwgHFppxP4AA60qH8HPcFS++iTtywEQ== dependencies: - "@oclif/command" "1.5.16" + "@oclif/command" "1.5.19" "@oclif/config" "^1" "@oclif/plugin-help" "^2" "@oclif/plugin-not-found" "^1.2" @@ -135,13 +135,13 @@ cross-spawn "^6.0.5" deepmerge "^4.0.0" express "^4.16.3" - follow-redirects "1.5.10" + follow-redirects "1.9.0" generic-pool "^3.7.1" globby "^10.0.1" image-size "^0.8.2" js-yaml "^3.13.1" percy-client "^3.2.0" - puppeteer "^1.13.0" + puppeteer "^2.0.0" retry-axios "^1.0.1" which "^2.0.1" winston "^3.0.0" @@ -908,7 +908,7 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" -follow-redirects@^1.9.0: +follow-redirects@1.9.0, follow-redirects@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== @@ -1576,7 +1576,7 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer@2.0.0: +puppeteer@2.0.0, puppeteer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-2.0.0.tgz#0612992e29ec418e0a62c8bebe61af1a64d7ec01" integrity sha512-t3MmTWzQxPRP71teU6l0jX47PHXlc4Z52sQv4LJQSZLq1ttkKS2yGM3gaI57uQwZkNaoGd0+HPPMELZkcyhlqA== diff --git a/build-system/test-configs/conformance-config.textproto b/build-system/test-configs/conformance-config.textproto index 3923f22c23bb..d2538baa9f08 100644 --- a/build-system/test-configs/conformance-config.textproto +++ b/build-system/test-configs/conformance-config.textproto @@ -40,6 +40,7 @@ requirement: { error_message: 'History.p.state is broken in IE11. Please use the helper methods provided in src/history.js' value: 'History.prototype.state' whitelist: 'src/history.js' + whitelist: 'extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js' } # Strings diff --git a/build-system/test-configs/dep-check-config.js b/build-system/test-configs/dep-check-config.js index f5912545b41e..3bff9692d6bd 100644 --- a/build-system/test-configs/dep-check-config.js +++ b/build-system/test-configs/dep-check-config.js @@ -265,6 +265,7 @@ exports.rules = [ 'extensions/amp-story/1.0/animation.js->extensions/amp-animation/0.1/web-animation-types.js', // Story ads 'extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js->extensions/amp-story/1.0/amp-story-store-service.js', + 'extensions/amp-story-auto-ads/0.1/story-ad-page.js->extensions/amp-story/1.0/amp-story-store-service.js', 'extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js->extensions/amp-story/1.0/events.js', // TODO(#24080) Remove this when story ads have full ad network support. 'extensions/amp-story-auto-ads/0.1/story-ad-page.js->extensions/amp-ad-exit/0.1/config.js', @@ -364,11 +365,11 @@ exports.rules = [ 'src/service/position-observer/position-observer-impl.js', 'extensions/amp-next-page/0.1/next-page-service.js->' + 'src/service/position-observer/position-observer-worker.js', - 'extensions/amp-next-page/0.2/service.js->' + + 'extensions/amp-next-page/1.0/service.js->' + 'src/service/position-observer/position-observer-impl.js', - 'extensions/amp-next-page/0.2/visibility-observer.js->' + + 'extensions/amp-next-page/1.0/visibility-observer.js->' + 'src/service/position-observer/position-observer-worker.js', - 'extensions/amp-next-page/0.2/visibility-observer.js->' + + 'extensions/amp-next-page/1.0/visibility-observer.js->' + 'src/service/position-observer/position-observer-impl.js', 'extensions/amp-user-notification/0.1/amp-user-notification.js->' + 'src/service/notification-ui-manager.js', diff --git a/examples/ads.amp.html b/examples/ads.amp.html index 62fd1c72f4a4..d82fb582eb29 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -1818,8 +1818,8 @@

ReadMo

type="readmo" layout="responsive" heights="(min-width: 1200px) 50%, (min-width: 780px) 60%, (max-width: 480px) 180%, 100%" - data-section="5591639" - data-url="https://techcrunch.com/" + data-section="amp-test" + data-url="https://yourdomain.com/" data-sponsored-by-label="" data-module="end-of-article"> diff --git a/examples/amp-date-display.amp.html b/examples/amp-date-display.amp.html index f6a58871a243..c5fe7d1a465c 100644 --- a/examples/amp-date-display.amp.html +++ b/examples/amp-date-display.amp.html @@ -17,7 +17,7 @@ overflow: auto; } - + diff --git a/examples/amp-story/embed.html b/examples/amp-story/embed.html index 56eb7d783e04..413ffa353d92 100644 --- a/examples/amp-story/embed.html +++ b/examples/amp-story/embed.html @@ -1,6 +1,6 @@ - AMP Story Embed Example + Story Embed (Non-AMP) + + + + + + + + + + + + +

1

+
+
+ + + +

2

+
+
+ + + +

3

+
+
+ + + +

4

+
+
+ + + +

5

+
+
+ + + +

6

+
+
+ + + + +

7

+
+
+ + + +

8

+
+
+ + + +

9

+
+
+ + + +

10

+
+
+ +
+ + + diff --git a/examples/amp-subscriptions.amp.html b/examples/amp-subscriptions.amp.html index d41c910e0730..ab070b31f638 100644 --- a/examples/amp-subscriptions.amp.html +++ b/examples/amp-subscriptions.amp.html @@ -208,7 +208,7 @@ "request": "sendEvent", "vars": { "event": "subscriptions_landing_page", - "data": "{'source': 'email marketing campaign 2019', 'active': false}" + "data": "{\"source\": \"email marketing campaign 2019\", \"is_active\": false}" } }, "onPastSubscriber": { @@ -216,7 +216,7 @@ "request": "sendSubscriptionState", "vars": { "state": "past_subscriber", - "productId": "myPreviousProductId" + "products": "'product1', 'product2'" } }, "onAd": { @@ -225,7 +225,7 @@ "request": "sendEvent", "vars": { "event": "ad_shown", - "data": "{'ad_name': 'fall_ad or custom name'}" + "data": "{\"ad_name\": \"fall_ad or custom name\"}" } } } diff --git a/extensions/amp-a4a/0.1/callout-vendors.js b/extensions/amp-a4a/0.1/callout-vendors.js index dcb276f21861..481039c0c508 100644 --- a/extensions/amp-a4a/0.1/callout-vendors.js +++ b/extensions/amp-a4a/0.1/callout-vendors.js @@ -155,6 +155,11 @@ const RTC_VENDORS = jsonConfiguration({ macros: ['TAG_ID'], disableKeyAppend: true, }, + prebidflux: { + url: 'https://prebid-server.flux-adserver.com/openrtb2/amp?tag_id=TAG_ID', + macros: ['TAG_ID'], + disableKeyAppend: true, + }, }); // DO NOT MODIFY: Setup for tests diff --git a/extensions/amp-a4a/rtc-documentation.md b/extensions/amp-a4a/rtc-documentation.md index e2fe20220e91..24cce5f60f60 100644 --- a/extensions/amp-a4a/rtc-documentation.md +++ b/extensions/amp-a4a/rtc-documentation.md @@ -127,6 +127,7 @@ The `errorReportingUrl` property is optional. The only available macros are ERRO - APS - Automatad - Criteo +- FLUX - IndexExchange - Lotame - Media.net diff --git a/extensions/amp-a4a/rtc-publisher-implementation-guide.md b/extensions/amp-a4a/rtc-publisher-implementation-guide.md index 51c67c8f384c..f47858171ad2 100644 --- a/extensions/amp-a4a/rtc-publisher-implementation-guide.md +++ b/extensions/amp-a4a/rtc-publisher-implementation-guide.md @@ -23,6 +23,7 @@ To use RTC, you must meet the following requirements: - APS - Automatad - Criteo +- FLUX - IndexExchange - Lotame - Media.net diff --git a/extensions/amp-action-macro/0.1/amp-action-macro.js b/extensions/amp-action-macro/0.1/amp-action-macro.js index c6193b8f6ea2..c48efff7baf2 100644 --- a/extensions/amp-action-macro/0.1/amp-action-macro.js +++ b/extensions/amp-action-macro/0.1/amp-action-macro.js @@ -103,6 +103,11 @@ export class AmpActionMacro extends AMP.BaseElement { return true; } + /** @override */ + isLayoutSupported(unusedLayout) { + return true; + } + /** * Checks if the invoking element is defined after the action being invoked. * This constraint is to prevent possible recursive calls. diff --git a/extensions/amp-analytics/0.1/test/test-variables.js b/extensions/amp-analytics/0.1/test/test-variables.js index 8d3ead988f84..5aac712754b1 100644 --- a/extensions/amp-analytics/0.1/test/test-variables.js +++ b/extensions/amp-analytics/0.1/test/test-variables.js @@ -140,16 +140,21 @@ describes.fakeWin('amp-analytics.VariableService', {amp: true}, env => { }); it('expands array vars', () => { - check('${array}', 'xy%26x,MACRO(abc,def),MACRO(abc%2Cdef)%26123,bar,', { - 'foo': 'bar', - 'array': [ - 'xy&x', // special chars should be encoded - 'MACRO(abc,def)', // do not encode macro - 'MACRO(abc,def)&123', // this is not a macro - '${foo}', // vars in array should be expanded - '${bar}', // undefined vars should be empty - ], - }); + check( + '${array}', + '123,xy%26x,MACRO(abc,def),MACRO(abc%2Cdef)%26123,bar,', + { + 'foo': 'bar', + 'array': [ + 123, + 'xy&x', // special chars should be encoded + 'MACRO(abc,def)', // do not encode macro + 'MACRO(abc,def)&123', // this is not a macro + '${foo}', // vars in array should be expanded + '${bar}', // undefined vars should be empty + ], + } + ); }); it('handles array with no vars', () => { diff --git a/extensions/amp-analytics/0.1/test/vendor-requests.json b/extensions/amp-analytics/0.1/test/vendor-requests.json index e637da632332..e343efcaf53d 100644 --- a/extensions/amp-analytics/0.1/test/vendor-requests.json +++ b/extensions/amp-analytics/0.1/test/vendor-requests.json @@ -282,7 +282,7 @@ }, "nielsen": { "session": "https://!prefixuaid-linkage.imrworldwide.com/cgi-bin/gn?prd=session&c13=asid,P!apid&sessionId=_client_id___page_view_id_&pingtype=4&enc=false&c61=createtm,_timestamp_&rnd=_random_", - "cloudapi": "https://!prefixcloudapi.imrworldwide.com/nmapi/v2/!apid/_client_id___page_view_id_/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22_client_id___page_view_id_%22%2C%22apn%22%3A%22!apn%22%2C%22apv%22%3A%22!apv%22%2C%22apid%22%3A%22!apid%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22!section%22%2C%22assetid%22%3A%22_page_view_id_%22%2C%22segA%22%3A%22!segA%22%2C%22segB%22%3A%22!segB%22%2C%22segC%22%3A%22!segC%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22_timestamp_%22%2C%22data%22%3A%7B%22hidden%22%3A%22_background_state_%22%2C%22blur%22%3A%22_background_state_%22%2C%22position%22%3A%22_timestamp_%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22_timestamp_%22%2C%22index%22%3A%221%22%2C%22pageURL%22%3A%22_canonical_url_%22%7D" + "cloudapi": "https://!prefixcloudapi.imrworldwide.com/nmapi/v2/!apid/_client_id___page_view_id_/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22_client_id___page_view_id_%22%2C%22apn%22%3A%22!apn%22%2C%22apv%22%3A%22!apv%22%2C%22apid%22%3A%22!apid%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22!section%22%2C%22assetid%22%3A%22_page_view_id_%22%2C%22segA%22%3A%22!segA%22%2C%22segB%22%3A%22!segB%22%2C%22segC%22%3A%22!segC%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22_timestamp_%22%2C%22data%22%3A%7B%22hidden%22%3A%22_background_state_%22%2C%22blur%22%3A%22_background_state_%22%2C%22position%22%3A%22_timestamp_%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22_timestamp_%22%2C%22index%22%3A%221%22%2C%22pageURL%22%3A%22_ampdoc_url_%22%7D" }, "nielsen-marketing-cloud": { "host": "loadeu.exelator.com", @@ -360,13 +360,10 @@ "conversion": "https://adserver.pressboard.ca/track/attention-amp?&=1&url=_canonical_url_&referrer=_document_referrer_&ts=_timestamp_&ua=_user_agent_&rand=_random_&uid=_client_id_&mid=!mediaId&cid=!campaignId&sid=!storyRequestId&geoid=!geoNameId&cn=!country&rg=!region&ct=!city&dbi=!dbInstance&tz=!timeZoneOffset&hbt=1&pvid=_page_view_id_&asurl=_source_url_&ash=_scroll_height_&asnh=_screen_height_&aasnh=_available_screen_height_&avh=_viewport_height_&ast=_scroll_top_&atet=_total_engaged_time_" }, "subscriptions-propensity": { - "sendEvent": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&events=!publicationId%3A!event%3A!data", - "sendSubscriptionState": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&states=!publicationId%3A!state%3A!productId", - "eventParams": "events=!publicationId%3A!event%3A!data", - "stateParams": "states=!publicationId%3A!state%3A!productId", - "sendBase": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_", - "baseParams": "u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_", - "baseUrl": "https://pubads.g.doubleclick.net/subopt" + "baseParams": "u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&extrainfo=", + "sendBase": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&extrainfo=", + "sendEvent": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&extrainfo=!data&events=!publicationId%3A!event", + "sendSubscriptionState": "https://pubads.g.doubleclick.net/subopt/data?u_tz=240&v=1&cookie=_client_id_&cdm=!sourceHostName&_amp_source_origin=_source_host_&extrainfo=%7B%22product%22%3A%20%5B!products%5D%7D&states=!publicationId%3A!state" }, "quantcast": { "host": "https://pixel.quantserve.com/pixel", diff --git a/extensions/amp-analytics/0.1/variables.js b/extensions/amp-analytics/0.1/variables.js index c2723d59ff5c..224ed6399588 100644 --- a/extensions/amp-analytics/0.1/variables.js +++ b/extensions/amp-analytics/0.1/variables.js @@ -281,7 +281,10 @@ export class VariableService { } else if (isArray(value)) { // Treat each value as a template and expand for (let i = 0; i < value.length; i++) { - value[i] = this.expandValue_(value[i], options); + value[i] = + typeof value[i] == 'string' + ? this.expandValue_(value[i], options) + : value[i]; } } diff --git a/extensions/amp-analytics/0.1/vendors/nielsen.js b/extensions/amp-analytics/0.1/vendors/nielsen.js index 6306157f1757..03535d06f462 100644 --- a/extensions/amp-analytics/0.1/vendors/nielsen.js +++ b/extensions/amp-analytics/0.1/vendors/nielsen.js @@ -25,7 +25,7 @@ const NIELSEN_CONFIG = jsonLiteral({ 'session': 'https://${prefix}uaid-linkage.imrworldwide.com/cgi-bin/gn?prd=session&c13=asid,P${apid}&sessionId=${sessionId}_${pageViewId}&pingtype=4&enc=false&c61=createtm,${timestamp}&rnd=${random}', 'cloudapi': - 'https://${prefix}cloudapi.imrworldwide.com/nmapi/v2/${apid}/${sessionId}_${pageViewId}/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22${sessionId}_${pageViewId}%22%2C%22apn%22%3A%22${apn}%22%2C%22apv%22%3A%22${apv}%22%2C%22apid%22%3A%22${apid}%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22${section}%22%2C%22assetid%22%3A%22${pageViewId}%22%2C%22segA%22%3A%22${segA}%22%2C%22segB%22%3A%22${segB}%22%2C%22segC%22%3A%22${segC}%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22${timestamp}%22%2C%22data%22%3A%7B%22hidden%22%3A%22${backgroundState}%22%2C%22blur%22%3A%22${backgroundState}%22%2C%22position%22%3A%22${timestamp}%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22${timestamp}%22%2C%22index%22%3A%22${requestCount}%22%2C%22pageURL%22%3A%22${canonicalUrl}%22%7D', + 'https://${prefix}cloudapi.imrworldwide.com/nmapi/v2/${apid}/${sessionId}_${pageViewId}/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22${sessionId}_${pageViewId}%22%2C%22apn%22%3A%22${apn}%22%2C%22apv%22%3A%22${apv}%22%2C%22apid%22%3A%22${apid}%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22${section}%22%2C%22assetid%22%3A%22${pageViewId}%22%2C%22segA%22%3A%22${segA}%22%2C%22segB%22%3A%22${segB}%22%2C%22segC%22%3A%22${segC}%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22${timestamp}%22%2C%22data%22%3A%7B%22hidden%22%3A%22${backgroundState}%22%2C%22blur%22%3A%22${backgroundState}%22%2C%22position%22%3A%22${timestamp}%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22${timestamp}%22%2C%22index%22%3A%22${requestCount}%22%2C%22pageURL%22%3A%22${ampdocUrl}%22%7D', }, 'triggers': { 'visible': { diff --git a/extensions/amp-analytics/0.1/vendors/nielsen.json b/extensions/amp-analytics/0.1/vendors/nielsen.json index d5c99a90d3dc..e88bc580250c 100644 --- a/extensions/amp-analytics/0.1/vendors/nielsen.json +++ b/extensions/amp-analytics/0.1/vendors/nielsen.json @@ -5,7 +5,7 @@ }, "requests": { "session": "https://${prefix}uaid-linkage.imrworldwide.com/cgi-bin/gn?prd=session&c13=asid,P${apid}&sessionId=${sessionId}_${pageViewId}&pingtype=4&enc=false&c61=createtm,${timestamp}&rnd=${random}", - "cloudapi": "https://${prefix}cloudapi.imrworldwide.com/nmapi/v2/${apid}/${sessionId}_${pageViewId}/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22${sessionId}_${pageViewId}%22%2C%22apn%22%3A%22${apn}%22%2C%22apv%22%3A%22${apv}%22%2C%22apid%22%3A%22${apid}%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22${section}%22%2C%22assetid%22%3A%22${pageViewId}%22%2C%22segA%22%3A%22${segA}%22%2C%22segB%22%3A%22${segB}%22%2C%22segC%22%3A%22${segC}%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22${timestamp}%22%2C%22data%22%3A%7B%22hidden%22%3A%22${backgroundState}%22%2C%22blur%22%3A%22${backgroundState}%22%2C%22position%22%3A%22${timestamp}%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22${timestamp}%22%2C%22index%22%3A%22${requestCount}%22%2C%22pageURL%22%3A%22${canonicalUrl}%22%7D" + "cloudapi": "https://${prefix}cloudapi.imrworldwide.com/nmapi/v2/${apid}/${sessionId}_${pageViewId}/a?b=%7B%22devInfo%22%3A%7B%22devId%22%3A%22${sessionId}_${pageViewId}%22%2C%22apn%22%3A%22${apn}%22%2C%22apv%22%3A%22${apv}%22%2C%22apid%22%3A%22${apid}%22%7D%2C%22metadata%22%3A%7B%22static%22%3A%7B%22type%22%3A%22static%22%2C%22section%22%3A%22${section}%22%2C%22assetid%22%3A%22${pageViewId}%22%2C%22segA%22%3A%22${segA}%22%2C%22segB%22%3A%22${segB}%22%2C%22segC%22%3A%22${segC}%22%2C%22adModel%22%3A%220%22%2C%22dataSrc%22%3A%22cms%22%7D%2C%22content%22%3A%7B%7D%2C%22ad%22%3A%7B%7D%7D%2C%22event%22%3A%22playhead%22%2C%22position%22%3A%22${timestamp}%22%2C%22data%22%3A%7B%22hidden%22%3A%22${backgroundState}%22%2C%22blur%22%3A%22${backgroundState}%22%2C%22position%22%3A%22${timestamp}%22%7D%2C%22type%22%3A%22static%22%2C%22utc%22%3A%22${timestamp}%22%2C%22index%22%3A%22${requestCount}%22%2C%22pageURL%22%3A%22${ampdocUrl}%22%7D" }, "triggers": { "visible": { diff --git a/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.js b/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.js index d7023beff9d8..68daf0dcafc4 100644 --- a/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.js +++ b/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.js @@ -19,19 +19,17 @@ import {jsonLiteral} from '../../../../src/json'; const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'vars': { 'clientId': 'CLIENT_ID(__gads)', - 'data': - '{"skus": "${skus}","source": "${source}","active": "${active}","product": "${product}"}', + 'productObj': '{"product": [${products}]}', + 'stateParams': '${publicationId}:${state}', + 'eventParams': '${publicationId}:${event}', }, 'requests': { 'baseUrl': 'https://pubads.g.doubleclick.net/subopt', 'baseParams': - 'u_tz=240&v=1&cookie=${clientId}&cdm=${sourceHostName}&' + - '_amp_source_origin=${sourceHost}', + 'u_tz=240&v=1&cookie=${clientId}&cdm=${sourceHostName}&_amp_source_origin=${sourceHost}&extrainfo=', 'sendBase': '${baseUrl}/data?${baseParams}', - 'stateParams': 'states=${publicationId}%3A${state}%3A${productId}', - 'eventParams': 'events=${publicationId}%3A${event}%3A${data}', - 'sendSubscriptionState': '${sendBase}&${stateParams}', - 'sendEvent': '${sendBase}&${eventParams}', + 'sendSubscriptionState': '${sendBase}${productObj}&states=${stateParams}', + 'sendEvent': '${sendBase}${data}&events=${eventParams}', }, 'triggers': { 'onSubscribed': { @@ -39,6 +37,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendSubscriptionState', 'vars': { 'state': 'subscriber', + 'products': '"${productId}"', }, }, 'onNotSubscribed': { @@ -46,6 +45,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendSubscriptionState', 'vars': { 'state': 'non_subscriber', + 'products': '"${productId}"', }, }, 'onShowOffers': { @@ -53,6 +53,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendEvent', 'vars': { 'event': 'offers_shown', + 'data': '{"skus": "${skus}","source": "${source}"}', }, }, 'onPaywall': { @@ -60,7 +61,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendEvent', 'vars': { 'event': 'paywall', - 'active': 'false', + 'data': '{"is_active": false, "source": "${source}"}', }, }, 'onSelectOffer': { @@ -68,7 +69,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendEvent', 'vars': { 'event': 'offer_selected', - 'active': 'true', + 'data': '{"is_active": true,"product": "${product}"}', }, }, 'onStartBuyflow': { @@ -76,7 +77,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendEvent', 'vars': { 'event': 'payment_flow_start', - 'active': 'true', + 'data': '{"is_active": true,"product": "${product}"}', }, }, 'onPaymentComplete': { @@ -84,7 +85,7 @@ const SUBSCRIPTIONS_PROPENSITY_CONFIG = jsonLiteral({ 'request': 'sendEvent', 'vars': { 'event': 'payment_complete', - 'active': 'true', + 'data': '{"is_active": true,"product": "${product}"}', }, }, }, diff --git a/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.json b/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.json index 135a60773204..f3c0caf84c1d 100644 --- a/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.json +++ b/extensions/amp-analytics/0.1/vendors/subscriptions-propensity.json @@ -1,29 +1,31 @@ { "vars": { - "clientId": "CLIENT_ID(__gads)" + "clientId": "CLIENT_ID(__gads)", + "productObj": "{\"product\": [${products}]}", + "stateParams": "${publicationId}:${state}", + "eventParams": "${publicationId}:${event}" }, "requests": { - "baseUrl": "https://pubads.g.doubleclick.net/subopt", - "baseParams": "u_tz=240&v=1&cookie=${clientId}&cdm=${sourceHostName}&_amp_source_origin=${sourceHost}", - "sendBase": "${baseUrl}/data?${baseParams}", - "stateParams": "states=${publicationId}%3A${state}%3A${productId}", - "eventParams": "events=${publicationId}%3A${event}%3A${data}", - "sendSubscriptionState": "${sendBase}&${stateParams}", - "sendEvent": "${sendBase}&${eventParams}" + "baseParams": "u_tz=240&v=1&cookie=${clientId}&cdm=${sourceHostName}&_amp_source_origin=${sourceHost}&extrainfo=", + "sendBase": "https://pubads.g.doubleclick.net/subopt/data?${baseParams}", + "sendSubscriptionState": "${sendBase}${productObj}&states=${stateParams}", + "sendEvent": "${sendBase}${data}&events=${eventParams}" }, "triggers": { "onSubscribed": { "on": "subscriptions-access-granted", "request": "sendSubscriptionState", "vars": { - "state": "subscriber" + "state": "subscriber", + "products": "\"${productId}\"" } }, "onNotSubscribed": { "on": "subscriptions-access-denied", "request": "sendSubscriptionState", "vars": { - "state": "non_subscriber" + "state": "non_subscriber", + "products": "\"${productId}\"" } }, "onShowOffers": { @@ -31,7 +33,7 @@ "request": "sendEvent", "vars": { "event": "offers_shown", - "data": "{\"skus\": \"${skus}\",\"source\": \"${source}\",\"active\": \"${active}\"}" + "data": "{\"skus\": \"${skus}\",\"source\": \"${source}\"}" } }, "onPaywall": { @@ -39,7 +41,7 @@ "request": "sendEvent", "vars": { "event": "paywall", - "data": "{\"source\": \"${source}\",\"active\": \"false\"}" + "data": "{\"is_active\": false, \"source\": \"${source}\"}" } }, "onSelectOffer": { @@ -47,7 +49,7 @@ "request": "sendEvent", "vars": { "event": "offer_selected", - "data": "{\"active\": \"true\",\"product\": \"${product}\"}" + "data": "{\"is_active\": true,\"product\": \"${product}\"}" } }, "onStartBuyflow": { @@ -55,7 +57,7 @@ "request": "sendEvent", "vars": { "event": "payment_flow_start", - "data": "{\"active\": \"true\",\"product\": \"${product}\"}" + "data": "{\"is_active\": true,\"product\": \"${product}\"}" } }, "onPaymentComplete": { @@ -63,7 +65,7 @@ "request": "sendEvent", "vars": { "event": "payment_complete", - "data": "{\"active\": \"true\",\"product\": \"${product}\"}" + "data": "{\"is_active\": true,\"product\": \"${product}\"}" } } }, diff --git a/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js b/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js index 254867d2d54c..a469500b947c 100644 --- a/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js +++ b/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js @@ -137,14 +137,18 @@ describes.realWin( it(`${accepts ? 'accepts' : 'rejects'} ${accepts || rejects}`, () => { [ html` - + `, html` -
+
+ +
`, html`
-
+
+ +
`, ].forEach(unwrapped => { @@ -446,7 +450,7 @@ describes.realWin( mockCandidates([ mockLoadedSignal( html` - + `, true ), @@ -466,7 +470,7 @@ describes.realWin( mockCandidates([ mockLoadedSignal( html` - + `, true ), @@ -483,19 +487,19 @@ describes.realWin( it('sets attribute only for candidates that meet criteria', async () => { const a = mockLoadedSignal( html` - + `, true ); const b = mockLoadedSignal( html` - + `, true ); const c = mockLoadedSignal( html` - + `, true ); @@ -520,7 +524,7 @@ describes.realWin( [1, 2, 3].map(() => mockLoadedSignal( html` - + `, true ) @@ -543,14 +547,14 @@ describes.realWin( it('filters out candidates that fail to load', async () => { const shouldNotLoad = mockLoadedSignal( html` - + `, false ); const shouldLoad = mockLoadedSignal( html` - + `, true ); @@ -681,6 +685,7 @@ describes.realWin( const lightboxable = createElementWithAttributes(doc, 'amp-img', { [LIGHTBOXABLE_ATTR]: '', + layout: 'flex-item', }); doc.head.appendChild(extensionScript); @@ -715,6 +720,7 @@ describes.realWin( const lightboxable = createElementWithAttributes(doc, 'amp-img', { [LIGHTBOXABLE_ATTR]: '', + layout: 'flex-item', }); doc.head.appendChild(extensionScript); @@ -730,7 +736,7 @@ describes.realWin( describe('apply', () => { it('sets attribute', async () => { const element = html` - + `; await apply(env.ampdoc, element); @@ -742,7 +748,7 @@ describes.realWin( const candidates = [1, 2, 3].map( () => html` - + ` ); @@ -757,7 +763,7 @@ describes.realWin( it('dispatches event', async () => { const element = html` - + `; element.dispatchCustomEvent = env.sandbox.spy(); diff --git a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-client-side.js b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-client-side.js index 761c087fd25f..c53a81ec2e5c 100644 --- a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-client-side.js +++ b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-client-side.js @@ -32,11 +32,9 @@ describes.endtoend( }, env => { let controller; - let requestBank; beforeEach(() => { controller = env.controller; - requestBank = env.requestBank; }); it('should work with client side decision', async () => { @@ -99,8 +97,9 @@ describes.endtoend( }); // Check the analytics request consentState - const req = await requestBank.withdraw('tracking'); - await expect(req.url).to.match(/consentState=sufficient/); + await expect( + 'http://localhost:8000/amp4test/request-bank/e2e/deposit/tracking?consentState=sufficient' + ).to.have.been.sent; }); } ); diff --git a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side-expire-cache.js b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side-expire-cache.js index d27a8e2a1421..e3776822e7a1 100644 --- a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side-expire-cache.js +++ b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side-expire-cache.js @@ -32,18 +32,21 @@ describes.endtoend( }, env => { let controller; - let requestBank; beforeEach(() => { controller = env.controller; - requestBank = env.requestBank; }); - it.skip('should respect server side decision and clear on next visit', async () => { + it('should respect server side decision and clear on next visit', async () => { resetAllElements(); const currentUrl = await controller.getCurrentUrl(); const nextGeoUrl = currentUrl.replace('mx', 'ca'); + // Check the analytics request consentState + await expect( + 'http://localhost:8000/amp4test/request-bank/e2e/deposit/tracking?consentState=insufficient' + ).to.have.been.sent; + // Block/unblock elements based off of 'reject' from response await findElements(controller); await verifyElementsBuilt(controller, { @@ -84,8 +87,9 @@ describes.endtoend( }); // Check the analytics request consentState - const req = await requestBank.withdraw('tracking'); - await expect(req.url).to.match(/consentState=sufficient/); + await expect( + 'http://localhost:8000/amp4test/request-bank/e2e/deposit/tracking?consentState=sufficient' + ).to.have.been.sent; }); } ); diff --git a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side.js b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side.js index fc64188c95d7..eff4929e98bb 100644 --- a/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side.js +++ b/extensions/amp-consent/0.1/test-e2e/test-amp-consent-server-side.js @@ -32,11 +32,9 @@ describes.endtoend( }, env => { let controller; - let requestBank; beforeEach(() => { controller = env.controller; - requestBank = env.requestBank; }); it('should respect server side decision and persist it', async () => { @@ -59,7 +57,6 @@ describes.endtoend( 'ui2': true, 'postPromptUi': false, }); - // [true, true, false]); // Navigate away to random page await controller.navigateTo('http://localhost:8000/'); @@ -83,8 +80,9 @@ describes.endtoend( }); // Check the analytics request consentState - const req = await requestBank.withdraw('tracking'); - await expect(req.url).to.match(/consentState=insufficient/); + await expect( + 'http://localhost:8000/amp4test/request-bank/e2e/deposit/tracking?consentState=insufficient' + ).to.have.been.sent; }); } ); diff --git a/extensions/amp-date-display/0.2/amp-date-display.js b/extensions/amp-date-display/0.2/amp-date-display.js new file mode 100644 index 000000000000..5645bd63749a --- /dev/null +++ b/extensions/amp-date-display/0.2/amp-date-display.js @@ -0,0 +1,97 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AsyncRender} from './async-render'; +import {DateDisplay} from './date-display'; +import {Fragment, createElement} from '../../../src/preact'; +import {PreactBaseElement} from '../../../src/preact/base-element'; +import {RenderDomTree} from './render-dom-tree'; +import {Services} from '../../../src/services'; +import {dict} from '../../../src/utils/object'; +import {isExperimentOn} from '../../../src/experiments'; +import {isLayoutSizeDefined} from '../../../src/layout'; +import {userAssert} from '../../../src/log'; + +/** @const {string} */ +const TAG = 'amp-date-display'; + +class AmpDateDisplay extends PreactBaseElement { + /** @override */ + init() { + const templates = Services.templatesFor(this.win); + let rendered = false; + + return dict({ + /** + * @param {!JsonObject} data + * @param {*} children + * @return {*} + */ + 'render': (data, children) => { + // We only render once in AMP mode, but React mode may rerender + // serveral times. + if (rendered) { + return children; + } + rendered = true; + + const host = this.element; + const domPromise = templates + .findAndRenderTemplate(host, data) + .then(rendered => { + const container = document.createElement('div'); + container.appendChild(rendered); + + return createElement(RenderDomTree, { + 'dom': container, + 'host': host, + }); + }); + const asyncRender = createElement(AsyncRender, null, domPromise); + return createElement(Fragment, null, children, asyncRender); + }, + }); + } + + /** @override */ + isLayoutSupported(layout) { + userAssert( + isExperimentOn(this.win, 'amp-date-display-v2'), + 'expected amp-date-display-v2 experiment to be enabled' + ); + return isLayoutSizeDefined(layout); + } +} + +/** @override */ +AmpDateDisplay.Component = DateDisplay; + +/** @override */ +AmpDateDisplay.passthrough = true; + +/** @override */ +AmpDateDisplay.props = { + 'displayIn': {attr: 'display-in'}, + 'offsetSeconds': {attr: 'offset-seconds', type: 'number'}, + 'locale': {attr: 'locale'}, + 'datetime': {attr: 'datetime'}, + 'timestampMs': {attr: 'timestamp-ms', type: 'number'}, + 'timestampSeconds': {attr: 'timestamp-seconds', type: 'number'}, +}; + +AMP.extension(TAG, '0.2', AMP => { + AMP.registerElement(TAG, AmpDateDisplay); +}); diff --git a/extensions/amp-date-display/0.2/async-render.js b/extensions/amp-date-display/0.2/async-render.js new file mode 100644 index 000000000000..662b779195ce --- /dev/null +++ b/extensions/amp-date-display/0.2/async-render.js @@ -0,0 +1,37 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {useResourcesNotify} from '../../../src/preact/utils'; +import {useState} from '../../../src/preact'; + +/** + * Renders the children prop, waiting for it to resolve if it is a promise. + * + * @param {!JsonObject} props + * @return {Preact.Renderable} + */ +export function AsyncRender(props) { + const children = props['children']; + const {0: state, 1: set} = useState(children); + useResourcesNotify(); + + if (state && state.then) { + Promise.resolve(children).then(set); + return null; + } + + return state; +} diff --git a/extensions/amp-date-display/0.2/date-display.js b/extensions/amp-date-display/0.2/date-display.js new file mode 100644 index 000000000000..37d0a0178b99 --- /dev/null +++ b/extensions/amp-date-display/0.2/date-display.js @@ -0,0 +1,221 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {useResourcesNotify} from '../../../src/preact/utils'; +import {userAssert} from '../../../src/log'; + +/** @const {string} */ +const DEFAULT_LOCALE = 'en'; + +/** @const {number} */ +const DEFAULT_OFFSET_SECONDS = 0; + +/** @typedef {{ + year: number, + month: number, + monthName: string, + monthNameShort: string, + day: number, + dayName: string, + dayNameShort: string, + hour: number, + minute: number, + second: number, + iso: string, +}} */ +let VariablesV2Def; + +/** @typedef {{ + year: number, + month: number, + monthName: string, + monthNameShort: string, + day: number, + dayName: string, + dayNameShort: string, + hour: number, + minute: number, + second: number, + iso: string, + yearTwoDigit: string, + monthTwoDigit: string, + dayTwoDigit: string, + hourTwoDigit: string, + hour12: string, + hour12TwoDigit: string, + minuteTwoDigit: string, + secondTwoDigit: string, + dayPeriod: string, + }} */ +let EnhancedVariablesV2Def; + +/** + * @param {!JsonObject} props + * @return {Preact.Renderable} + */ +export function DateDisplay(props) { + const render = props['render']; + const data = /** @type {!JsonObject} */ (getDataForTemplate(props)); + useResourcesNotify(); + + return render(data, props['children']); +} + +/** + * @param {!JsonObject} props + * @return {!EnhancedVariablesV2Def} + */ +function getDataForTemplate(props) { + const { + 'displayIn': displayIn = '', + 'locale': locale = DEFAULT_LOCALE, + 'offsetSeconds': offsetSeconds = DEFAULT_OFFSET_SECONDS, + } = props; + + const epoch = getEpoch(props); + const offset = offsetSeconds * 1000; + const date = new Date(epoch + offset); + + const basicData = + displayIn.toLowerCase() === 'utc' + ? getVariablesInUTC(date, locale) + : getVariablesInLocal(date, locale); + + return enhanceBasicVariables(basicData); +} + +/** + * @param {!JsonObject} props + * @return {number|undefined} + */ +function getEpoch(props) { + const { + 'datetime': datetime = '', + 'timestampMs': timestampMs = 0, + 'timestampSeconds': timestampSeconds = 0, + } = props; + + let epoch; + if (datetime.toLowerCase() === 'now') { + epoch = Date.now(); + } else if (datetime) { + epoch = Date.parse(datetime); + userAssert(!isNaN(epoch), 'Invalid date: %s', datetime); + } else if (timestampMs) { + epoch = timestampMs; + } else if (timestampSeconds) { + epoch = timestampSeconds * 1000; + } + + userAssert( + epoch !== undefined, + 'One of datetime, timestamp-ms, or timestamp-seconds is required' + ); + + return epoch; +} + +/** + * @param {number} input + * @return {string} + */ +function padStart(input) { + if (input > 9) { + return input.toString(); + } + + return '0' + input; +} + +/** + * @param {!VariablesV2Def} data + * @return {!EnhancedVariablesV2Def} + */ +function enhanceBasicVariables(data) { + const hour12 = data.hour % 12 || 12; + + // Override type since Object.assign is not understood + return /** @type {!EnhancedVariablesV2Def} */ ({ + ...data, + 'yearTwoDigit': padStart(data.year % 100), + 'monthTwoDigit': padStart(data.month), + 'dayTwoDigit': padStart(data.day), + 'hourTwoDigit': padStart(data.hour), + 'hour12': hour12, + 'hour12TwoDigit': padStart(hour12), + 'minuteTwoDigit': padStart(data.minute), + 'secondTwoDigit': padStart(data.second), + 'dayPeriod': data.hour < 12 ? 'am' : 'pm', + }); +} + +/** + * @param {!Date} date + * @param {string} locale + * @return {!VariablesV2Def} + */ +function getVariablesInLocal(date, locale) { + return { + 'year': date.getFullYear(), + 'month': date.getMonth() + 1, + 'monthName': date.toLocaleDateString(locale, {month: 'long'}), + 'monthNameShort': date.toLocaleDateString(locale, { + month: 'short', + }), + 'day': date.getDate(), + 'dayName': date.toLocaleDateString(locale, {weekday: 'long'}), + 'dayNameShort': date.toLocaleDateString(locale, { + weekday: 'short', + }), + 'hour': date.getHours(), + 'minute': date.getMinutes(), + 'second': date.getSeconds(), + 'iso': date.toISOString(), + }; +} + +/** + * @param {!Date} date + * @param {string} locale + * @return {!VariablesV2Def} + */ +function getVariablesInUTC(date, locale) { + return { + 'year': date.getUTCFullYear(), + 'month': date.getUTCMonth() + 1, + 'monthName': date.toLocaleDateString(locale, { + month: 'long', + timeZone: 'UTC', + }), + 'monthNameShort': date.toLocaleDateString(locale, { + month: 'short', + timeZone: 'UTC', + }), + 'day': date.getUTCDate(), + 'dayName': date.toLocaleDateString(locale, { + weekday: 'long', + timeZone: 'UTC', + }), + 'dayNameShort': date.toLocaleDateString(locale, { + weekday: 'short', + timeZone: 'UTC', + }), + 'hour': date.getUTCHours(), + 'minute': date.getUTCMinutes(), + 'second': date.getUTCSeconds(), + 'iso': date.toISOString(), + }; +} diff --git a/extensions/amp-date-display/0.2/render-dom-tree.js b/extensions/amp-date-display/0.2/render-dom-tree.js new file mode 100644 index 000000000000..bdfafd5a4916 --- /dev/null +++ b/extensions/amp-date-display/0.2/render-dom-tree.js @@ -0,0 +1,47 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AmpEvents} from '../../../src/amp-events'; +import {createCustomEvent} from '../../../src/event-helper'; +import {dev, devAssert} from '../../../src/log'; +import {removeChildren} from '../../../src/dom'; +import {useResourcesNotify} from '../../../src/preact/utils'; + +/** + * Clears the host element and appends the DOM tree into it. + * + * @param {!JsonObject} props + * @return {null} + */ +export function RenderDomTree(props) { + const {'dom': dom, 'host': host} = props; + useResourcesNotify(); + + removeChildren(dev().assertElement(host)); + if (dom) { + host.appendChild(dom); + } + + const event = createCustomEvent( + devAssert(host.ownerDocument.defaultView), + AmpEvents.DOM_UPDATE, + /* detail */ null, + {bubbles: true} + ); + host.dispatchEvent(event); + + return null; +} diff --git a/extensions/amp-date-display/0.2/test/test-amp-date-display.js b/extensions/amp-date-display/0.2/test/test-amp-date-display.js new file mode 100644 index 000000000000..2ed1100bb289 --- /dev/null +++ b/extensions/amp-date-display/0.2/test/test-amp-date-display.js @@ -0,0 +1,246 @@ +/** + * Copyright 2019 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../../../amp-mustache/0.2/amp-mustache'; +import '../amp-date-display'; +import * as lolex from 'lolex'; +import {waitForChildPromise} from '../../../../src/dom'; + +describes.realWin( + 'amp-date-display', + { + amp: { + extensions: ['amp-mustache:0.2', 'amp-date-display:0.2'], + }, + }, + env => { + let win; + let element; + let clock; + + async function getRenderedData() { + await waitForChildPromise(element, () => { + // The rendered container inserts a div element. + return element.querySelector('div'); + }); + + return JSON.parse(element.textContent); + } + + beforeEach(() => { + win = env.win; + clock = lolex.install({ + target: window, + now: new Date('2018-01-01T08:00:00Z'), + }); + + element = win.document.createElement('amp-date-display'); + const template = document.createElement('template'); + template.setAttribute('type', 'amp-mustache'); + template.content.textContent = JSON.stringify({ + year: '{{year}}', + yearTwoDigit: '{{yearTwoDigit}}', + month: '{{month}}', + monthTwoDigit: '{{monthTwoDigit}}', + monthName: '{{monthName}}', + monthNameShort: '{{monthNameShort}}', + day: '{{day}}', + dayTwoDigit: '{{dayTwoDigit}}', + dayName: '{{dayName}}', + dayNameShort: '{{dayNameShort}}', + hour: '{{hour}}', + hourTwoDigit: '{{hourTwoDigit}}', + hour12: '{{hour12}}', + hour12TwoDigit: '{{hour12TwoDigit}}', + minute: '{{minute}}', + minuteTwoDigit: '{{minuteTwoDigit}}', + second: '{{second}}', + secondTwoDigit: '{{secondTwoDigit}}', + dayPeriod: '{{dayPeriod}}', + iso: '{{iso}}', + }); + element.appendChild(template); + element.setAttribute('layout', 'nodisplay'); + win.document.body.appendChild(element); + }); + + afterEach(() => { + clock.uninstall(); + }); + + // Unfortunately, we cannot test the most interesting case of UTC datetime + // displayed in local, because the test would work in only one time zone. + + it('provides all variables in UTC and English (default)', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007Z'); + element.setAttribute('display-in', 'UTC'); + element.build(); + + const data = await getRenderedData(); + + expect(data.year).to.equal('2001'); + expect(data.yearTwoDigit).to.equal('01'); + expect(data.month).to.equal('2'); + expect(data.monthTwoDigit).to.equal('02'); + expect(data.monthName).to.equal('February'); + expect(data.monthNameShort).to.equal('Feb'); + expect(data.day).to.equal('3'); + expect(data.dayTwoDigit).to.equal('03'); + expect(data.dayName).to.equal('Saturday'); + expect(data.dayNameShort).to.equal('Sat'); + expect(data.hour).to.equal('4'); + expect(data.hourTwoDigit).to.equal('04'); + expect(data.hour12).to.equal('4'); + expect(data.hour12TwoDigit).to.equal('04'); + expect(data.minute).to.equal('5'); + expect(data.minuteTwoDigit).to.equal('05'); + expect(data.second).to.equal('6'); + expect(data.secondTwoDigit).to.equal('06'); + expect(data.dayPeriod).to.equal('am'); + }); + + it('provides all variables in local and English (default)', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007'); + element.build(); + + const data = await getRenderedData(); + + expect(data.year).to.equal('2001'); + expect(data.yearTwoDigit).to.equal('01'); + expect(data.month).to.equal('2'); + expect(data.monthTwoDigit).to.equal('02'); + expect(data.monthName).to.equal('February'); + expect(data.monthNameShort).to.equal('Feb'); + expect(data.day).to.equal('3'); + expect(data.dayTwoDigit).to.equal('03'); + expect(data.dayName).to.equal('Saturday'); + expect(data.dayNameShort).to.equal('Sat'); + expect(data.hour).to.equal('4'); + expect(data.hourTwoDigit).to.equal('04'); + expect(data.hour12).to.equal('4'); + expect(data.hour12TwoDigit).to.equal('04'); + expect(data.minute).to.equal('5'); + expect(data.minuteTwoDigit).to.equal('05'); + expect(data.second).to.equal('6'); + expect(data.secondTwoDigit).to.equal('06'); + expect(data.dayPeriod).to.equal('am'); + }); + + describe('correctly parses', () => { + it('now keyword', async () => { + element.setAttribute('datetime', 'now'); + element.build(); + + const {iso} = await getRenderedData(); + const dateFromParsed = new Date(iso); + + // Because of the runtime there could be a several ms difference. + expect(dateFromParsed.getTime()).to.equal(Date.now()); + }); + + it('day only ISO 8601 date', async () => { + element.setAttribute('datetime', '2001-02-03'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-03T00:00:00.000Z'); + }); + + it('full ISO 8601 date in UTC time zone', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007Z'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-03T04:05:06.007Z'); + }); + + it('full ISO 8601 date without time zone (interpreted as local)', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007'); + element.build(); + + const data = await getRenderedData(); + const result = + `${data.year}-${data.monthTwoDigit}-${data.dayTwoDigit}` + + `T${data.hourTwoDigit}:${data.minuteTwoDigit}:${data.secondTwoDigit}`; + + expect(result).to.equal('2001-02-03T04:05:06'); + }); + + it('full ISO 8601 date in a custom time zone', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007+08:00'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-02T20:05:06.007Z'); + }); + + it('seconds since the UNIX epoch', async () => { + element.setAttribute('timestamp-seconds', '981173106'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-03T04:05:06.000Z'); + }); + + it('miliseconds since the UNIX epoch', async () => { + element.setAttribute('timestamp-ms', '981173106007'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-03T04:05:06.007Z'); + }); + }); + + it('adds offset seconds', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007Z'); + element.setAttribute('offset-seconds', '1234567'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-02-17T11:01:13.007Z'); + }); + + it('subtracts offset seconds', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007Z'); + element.setAttribute('offset-seconds', '-1234567'); + element.build(); + + const data = await getRenderedData(); + + expect(data.iso).to.equal('2001-01-19T21:08:59.007Z'); + }); + + it('provides variables in Czech when "cs" locale is passed', async () => { + element.setAttribute('datetime', '2001-02-03T04:05:06.007Z'); + element.setAttribute('display-in', 'UTC'); + element.setAttribute('locale', 'cs'); + element.build(); + + const data = await getRenderedData(); + + expect(data.monthName).to.equal('únor'); + expect(data.monthNameShort).to.equal('úno'); + expect(data.dayName).to.equal('sobota'); + expect(data.dayNameShort).to.equal('so'); + }); + } +); diff --git a/extensions/amp-list/0.1/amp-list.js b/extensions/amp-list/0.1/amp-list.js index 4a177de9fc5d..3a4345d4158e 100644 --- a/extensions/amp-list/0.1/amp-list.js +++ b/extensions/amp-list/0.1/amp-list.js @@ -358,7 +358,6 @@ export class AmpList extends AMP.BaseElement { }; const src = mutations['src']; - const state = /** @type {!JsonObject} */ (mutations)['state']; if (src !== undefined) { if (typeof src === 'string') { // Defer to fetch in layoutCallback() before first layout. @@ -371,9 +370,6 @@ export class AmpList extends AMP.BaseElement { } else { this.user().error(TAG, 'Unexpected "src" type: ' + src); } - } else if (state !== undefined) { - user().error(TAG, '[state] is deprecated, please use [src] instead.'); - promise = renderLocalData(state); } const isLayoutContainer = mutations['is-layout-container']; diff --git a/extensions/amp-list/0.1/test/validator-amp-list.out b/extensions/amp-list/0.1/test/validator-amp-list.out index 7f4b7fae5170..c192e53f7737 100644 --- a/extensions/amp-list/0.1/test/validator-amp-list.out +++ b/extensions/amp-list/0.1/test/validator-amp-list.out @@ -48,7 +48,7 @@ FAIL | | > ^~~~~~~~~ -amp-list/0.1/test/validator-amp-list.html:48:2 The attribute '[state]' in tag 'amp-list' is deprecated - use '[src]' instead. +amp-list/0.1/test/validator-amp-list.html:48:2 The attribute '[state]' may not appear in tag 'amp-list'. (see https://amp.dev/documentation/components/amp-list) | src="https://data.com/articles.json?ref=CANONICAL_URL" | [src]="foo.bar" [state]="baz.qux"> |
diff --git a/extensions/amp-list/validator-amp-list.protoascii b/extensions/amp-list/validator-amp-list.protoascii index cd582bb04801..0d36870e253b 100644 --- a/extensions/amp-list/validator-amp-list.protoascii +++ b/extensions/amp-list/validator-amp-list.protoascii @@ -114,10 +114,6 @@ tags: { # with mandatory src and/or [src] attr name: "[src]" mandatory_anyof: "['src','[src]','data-amp-bind-src']" } - attrs: { - name: "[state]" - deprecation: "[src]" - } attr_lists: "extended-amp-global" amp_layout: { supported_layouts: FILL diff --git a/extensions/amp-mustache/0.2/amp-mustache.js b/extensions/amp-mustache/0.2/amp-mustache.js index 8ca626e67900..9f3b90157049 100644 --- a/extensions/amp-mustache/0.2/amp-mustache.js +++ b/extensions/amp-mustache/0.2/amp-mustache.js @@ -137,12 +137,9 @@ export class AmpMustache extends BaseTemplate { * @private */ purifyAndSetHtml_(html) { - const body = this.purifier_.purifyHtml(html); - // TODO(choumx): Remove innerHTML usage once DOMPurify bug is fixed. - // https://github.com/cure53/DOMPurify/pull/295 - const root = this.win.document.createElement('div'); - root./*OK*/ innerHTML = body./*OK*/ innerHTML; - return this.unwrap(root); + const body = this.purifier_.purifyHtml(`
${html}
`); + const div = body.firstElementChild; + return this.unwrap(div); } } diff --git a/extensions/amp-mustache/0.2/test/test-amp-mustache.js b/extensions/amp-mustache/0.2/test/test-amp-mustache.js index 4fae031fc197..b931aa26dc60 100644 --- a/extensions/amp-mustache/0.2/test/test-amp-mustache.js +++ b/extensions/amp-mustache/0.2/test/test-amp-mustache.js @@ -83,6 +83,23 @@ describes.repeated( expect(result./*OK*/ innerHTML).to.equal('value = abc'); }); + // https://github.com/ampproject/amphtml/pull/17401 + it('should render attrs with non-HTML namespaces', () => { + innerHtmlSetup( + '' + ); + template.compileCallback(); + const result = template.render({}); + expect(result./*OK*/ outerHTML).to.equal( + '' + ); + // Make sure [xlink:href] has the right namespace. + const image = result.querySelector('image'); + const href = image.getAttributeNode('xlink:href'); + expect(href.namespaceURI).to.equal('http://www.w3.org/1999/xlink'); + expect(href.value).to.equal('foo.svg'); + }); + it('should render {{.}} from string', () => { textContentSetup('value = {{.}}'); template.compileCallback(); diff --git a/extensions/amp-next-page/0.2/amp-next-page.css b/extensions/amp-next-page/1.0/amp-next-page.css similarity index 100% rename from extensions/amp-next-page/0.2/amp-next-page.css rename to extensions/amp-next-page/1.0/amp-next-page.css diff --git a/extensions/amp-next-page/0.2/amp-next-page.js b/extensions/amp-next-page/1.0/amp-next-page.js similarity index 94% rename from extensions/amp-next-page/0.2/amp-next-page.js rename to extensions/amp-next-page/1.0/amp-next-page.js index 6d0c97b00c0d..169e68559818 100644 --- a/extensions/amp-next-page/0.2/amp-next-page.js +++ b/extensions/amp-next-page/1.0/amp-next-page.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {CSS} from '../../../build/amp-next-page-0.2.css'; +import {CSS} from '../../../build/amp-next-page-1.0.css'; import {Layout} from '../../../src/layout'; import {NextPageService} from './service'; import {Services} from '../../../src/services'; @@ -50,7 +50,7 @@ export class AmpNextPage extends AMP.BaseElement { } } -AMP.extension(TAG, '0.2', AMP => { +AMP.extension(TAG, '1.0', AMP => { AMP.registerServiceForDoc(SERVICE, NextPageService); AMP.registerElement(TAG, AmpNextPage, CSS); }); diff --git a/extensions/amp-next-page/0.2/page.js b/extensions/amp-next-page/1.0/page.js similarity index 100% rename from extensions/amp-next-page/0.2/page.js rename to extensions/amp-next-page/1.0/page.js diff --git a/extensions/amp-next-page/0.2/service.js b/extensions/amp-next-page/1.0/service.js similarity index 97% rename from extensions/amp-next-page/0.2/service.js rename to extensions/amp-next-page/1.0/service.js index f52da0821c36..829231669452 100644 --- a/extensions/amp-next-page/0.2/service.js +++ b/extensions/amp-next-page/1.0/service.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {CSS} from '../../../build/amp-next-page-0.2.css'; +import {CSS} from '../../../build/amp-next-page-1.0.css'; import {HostPage, Page, PageState} from './page'; import {MultidocManager} from '../../../src/multidoc-manager'; import {Services} from '../../../src/services'; @@ -112,19 +112,16 @@ export class NextPageService { * @param {!AmpElement} element */ build(element) { - // Get the separator and more box (and remove the provided elements in the process) - const separator = this.getSeparatorElement_(element); - const moreBox = this.getMoreBoxElement_(element); - // Prevent multiple amp-next-page on the same document if (this.isBuilt()) { return; } - // Set the parsed elements as the choice for all subsequent elements this.element_ = element; - this.separator_ = separator; - this.moreBox_ = moreBox; + + // Get the separator and more box (and remove the provided elements in the process) + this.separator_ = this.getSeparatorElement_(element); + this.moreBox_ = this.getMoreBoxElement_(element); // Create a reference to the host page this.hostPage_ = this.createHostPage(); @@ -176,11 +173,12 @@ export class NextPageService { /** * @param {boolean=} force + * @return {!Promise} */ maybeFetchNext(force = false) { // If a page is already queued to be fetched, wait for it if (this.pages_.some(page => page.isFetching())) { - return; + return Promise.resolve(); } if (force || this.getViewportsAway_() <= PRERENDER_VIEWPORT_COUNT) { @@ -188,7 +186,7 @@ export class NextPageService { this.getPageIndex_(this.lastFetchedPage_) + 1 ]; if (nextPage) { - nextPage.fetch(); + return nextPage.fetch(); } } } @@ -224,7 +222,9 @@ export class NextPageService { .filter(page => page.isVisible()) .forEach(page => this.toggleHiddenAndReplaceableElements( - /** @type {!Document|!ShadowRoot} */ (dev().assert(page.document)) + /** @type {!Document|!ShadowRoot} */ (dev().assertElement( + page.document + )) ) ); } diff --git a/extensions/amp-next-page/1.0/test/test-amp-next-page.js b/extensions/amp-next-page/1.0/test/test-amp-next-page.js new file mode 100644 index 000000000000..3825b022dbab --- /dev/null +++ b/extensions/amp-next-page/1.0/test/test-amp-next-page.js @@ -0,0 +1,299 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '../amp-next-page'; +import {PageState} from '../page'; +import {Services} from '../../../../src/services'; +import {VisibilityState} from '../../../../src/visibility-state'; +import {setStyle} from '../../../../src/style'; +import {toggleExperiment} from '../../../../src/experiments'; + +const MOCK_NEXT_PAGE = `
Header
+
+
Footer
`; +const VALID_CONFIG = [ + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 1', + 'url': '/document1', + }, + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 2', + 'url': '/document2', + }, +]; + +describes.realWin( + 'amp-next-page component', + { + amp: { + extensions: ['amp-next-page:1.0'], + }, + }, + env => { + let win, doc, ampdoc; + + beforeEach(() => { + win = env.win; + doc = win.document; + ampdoc = env.ampdoc; + + toggleExperiment(win, 'amp-next-page-v2', true); + + // Mocks + ampdoc.getUrl = () => document.location.href; + win.document.title = 'Host page'; + }); + + async function getAMPNextPage(options) { + options = options || {}; + + const element = doc.createElement('amp-next-page'); + + // Ensure element is off screen when it renders. + setStyle(element, 'marginTop', '10000px'); + + // Add inline config if specified + if (options.inlineConfig) { + const configElement = document.createElement('script'); + configElement.setAttribute('id', 'next-page'); + configElement.setAttribute('type', 'application/json'); + configElement.textContent = JSON.stringify(options.inlineConfig); + element.appendChild(configElement); + } + + if (options.src) { + element.setAttribute('src', options.src); + } + + doc.body.appendChild(element); + + return element; + } + + afterEach(() => { + toggleExperiment(win, 'amp-next-page-v2', false); + }); + + describe('inline config', () => { + it('builds with valid inline config', async () => { + const element = await getAMPNextPage({ + inlineConfig: VALID_CONFIG, + }); + + await element.build(); + await element.layoutCallback(); + }); + + it('errors on invalid inline config (object instead of array)', async () => { + const element = await getAMPNextPage({ + inlineConfig: { + pages: [ + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 1', + 'ampUrl': '/document1', + }, + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 2', + 'ampUrl': '/document2', + }, + ], + }, + }); + + await allowConsoleError(() => + element.build().catch(err => { + expect(err.message).to.include( + 'amp-next-page page list should be an array' + ); + }) + ); + }); + + it('errors on invalid inline config (ampUrl instead of url)', async () => { + const element = await getAMPNextPage({ + inlineConfig: [ + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 1', + 'ampUrl': '/document1', + }, + { + 'image': '/examples/img/hero@1x.jpg', + 'title': 'Title 2', + 'ampUrl': '/document2', + }, + ], + }); + + await allowConsoleError(() => + element.build().catch(err => { + expect(err.message).to.include('page url must be a string'); + }) + ); + }); + + it('builds with valid inline config', async () => { + const element = await getAMPNextPage({ + inlineConfig: VALID_CONFIG, + }); + + await element.build(); + await element.layoutCallback(); + }); + }); + + describe('basic functionality', () => { + let element; + let service; + + beforeEach(async () => { + element = await getAMPNextPage({ + inlineConfig: VALID_CONFIG, + }); + + await element.build(); + await element.layoutCallback(); + + service = Services.nextPageServiceForDoc(doc); + }); + + afterEach(async () => { + element.parentNode.removeChild(element); + }); + + it('should register pages from the given config', async () => { + // Page 1 + expect(service.pages_[1].title).to.equal('Title 1'); + expect(service.pages_[1].url).to.include('/document1'); + expect(service.pages_[1].image).to.equal('/examples/img/hero@1x.jpg'); + // Page 2 + expect(service.pages_[2].title).to.equal('Title 2'); + expect(service.pages_[2].url).to.include('/document2'); + expect(service.pages_[2].image).to.equal('/examples/img/hero@1x.jpg'); + }); + + it('should internally register the host page', async () => { + expect(service.pages_[0].title).to.equal('Host page'); + expect(service.pages_[0].url).to.include('about:srcdoc'); + expect(service.pages_[0].state_).to.equal(PageState.INSERTED); + expect(service.pages_[0].visibilityState_).to.equal( + VisibilityState.VISIBLE + ); + }); + + it('should not fetch the next document before scrolling', async () => { + [1, 2].forEach(i => { + expect(service.pages_[i].state_).to.equal(PageState.QUEUED); + expect(service.pages_[i].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + }); + }); + + it('fetches the next document on scroll', async () => { + env.sandbox.stub(service, 'getViewportsAway_').returns(2); + const firstPageFetchSpy = env.sandbox.spy(service.pages_[1], 'fetch'); + const secondPageFetchSpy = env.sandbox.spy(service.pages_[2], 'fetch'); + + env.fetchMock.get(/\/document1/, MOCK_NEXT_PAGE); + await service.maybeFetchNext(); + + expect(firstPageFetchSpy).to.be.calledOnce; + expect(service.pages_[1].state_).to.equal(PageState.INSERTED); + expect(service.pages_[1].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + + expect(secondPageFetchSpy).to.not.be.called; + expect(service.pages_[2].state_).to.equal(PageState.QUEUED); + expect(service.pages_[2].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + }); + + it('fetches the second document on scroll', async () => { + env.sandbox.stub(service, 'getViewportsAway_').returns(2); + const firstPageFetchSpy = env.sandbox.spy(service.pages_[1], 'fetch'); + const secondPageFetchSpy = env.sandbox.spy(service.pages_[2], 'fetch'); + + env.fetchMock.get(/\/document1/, MOCK_NEXT_PAGE); + env.fetchMock.get(/\/document2/, MOCK_NEXT_PAGE); + await service.maybeFetchNext(); + await service.maybeFetchNext(); + + expect(firstPageFetchSpy).to.be.calledOnce; + expect(service.pages_[1].state_).to.equal(PageState.INSERTED); + expect(service.pages_[1].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + + expect(secondPageFetchSpy).to.be.calledOnce; + expect(service.pages_[2].state_).to.equal(PageState.INSERTED); + expect(service.pages_[2].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + }); + + it('blocks documents which resolve to a different origin when fetched ', async () => { + expectAsyncConsoleError( + /Invalid page URL supplied to amp-next-page, pages must be from the same origin as the current document/, + 2 + ); + + env.fetchMock.get(/\/document1/, { + redirectUrl: 'https://othersite.com/article', + body: MOCK_NEXT_PAGE, + }); + env.sandbox.stub(service, 'getViewportsAway_').returns(2); + + await service.maybeFetchNext(); + + expect(service.pages_[1].state_).to.equal(PageState.FAILED); + expect(service.pages_[1].visibilityState_).to.equal( + VisibilityState.PRERENDER + ); + }); + + it('adds the hidden class to elements that should be hidden', () => { + // TODO(wassgha): Implement once #26235 is merged + }); + + it('removes amp-analytics tags from child documents', async () => { + env.sandbox.stub(service, 'getViewportsAway_').returns(2); + + env.fetchMock.get( + /\/document1/, + `${MOCK_NEXT_PAGE} ` + ); + await service.maybeFetchNext(); + + // TODO(wassgha): Replace `.shadowDoc_.ampdoc.getRootNode()` with `.document` once #26235 is merged + expect( + service.pages_[1].shadowDoc_.ampdoc + .getRootNode() + .getElementById('analytics1') + ).to.be.null; + }); + }); + + describe('remote config', () => { + // TODO (wassgha): Implement once remote config is implemented + }); + } +); diff --git a/extensions/amp-next-page/1.0/test/test-config.js b/extensions/amp-next-page/1.0/test/test-config.js new file mode 100644 index 000000000000..ffaada74e815 --- /dev/null +++ b/extensions/amp-next-page/1.0/test/test-config.js @@ -0,0 +1,187 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Page} from '../page'; +import {validatePage} from '../utils'; + +describes.sandboxed('amp-next-page config', {}, () => { + const documentUrl = 'https://example.com/parent'; + const documentUrlCdn = + 'https://example-com.cdn.ampproject.org/c/s/example.com/parent'; + + it('rewrites relative URLs to absolute', () => { + const page = new Page(null, { + url: '/article1', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(page, documentUrl)).to.not.throw(); + expect(page.url).to.equal('https://example.com/article1'); + }); + + it('rewrites relative URLs when served from the cache', () => { + const page = new Page(null, { + url: '/article1', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(page, documentUrlCdn)).to.not.throw(); + expect(page.url).to.equal( + 'https://example-com.cdn.ampproject.org/c/s/example.com/article1' + ); + }); + + it('rewrites canonical URLs when served from the cache', () => { + const page = new Page(null, { + url: 'https://example.com/art2?x=1', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(page, documentUrlCdn)).to.not.throw(); + expect(page.url).to.equal( + 'https://example-com.cdn.ampproject.org/c/s/example.com/art2?x=1' + ); + + const pageWithCdn = new Page(null, { + url: 'https://example-com.cdn.ampproject.org/c/s/example.com/art1', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(pageWithCdn, documentUrlCdn)).to.not.throw(); + expect(pageWithCdn.url).to.equal( + 'https://example-com.cdn.ampproject.org/c/s/example.com/art1' + ); + }); + + it('rewrites non-HTTPS canonical URLs when served from the cache', () => { + const url = documentUrlCdn.replace('/s/', '/'); + + const page = new Page(null, { + url: 'http://example.com/art2?x=1', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(page, url)).to.not.throw(); + expect(page.url).to.equal( + 'https://example-com.cdn.ampproject.org/c/example.com/art2?x=1' + ); + }); + + it("doesn't rewrite URLs if sourceOrigin and origin match", () => { + const page = new Page(null, { + url: 'https://example.com/article', + image: '/image.png', + title: 'Article 1', + }); + expect(() => validatePage(page, documentUrl)).to.not.throw(); + expect(page.url).to.equal('https://example.com/article'); + }); + + it('throws on config with missing page title', () => { + const page = new Page(null, { + url: 'https://example.com/article', + image: '/image.png', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'title must be a string' + ); + }); + }); + + it('throws on config with non-string page title', () => { + const page = new Page(null, { + url: 'https://example.com/article', + image: '/image.png', + title: {}, + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'title must be a string' + ); + }); + }); + + it('throws on config with missing page image', () => { + const page = new Page(null, { + url: 'https://example.com/article', + title: 'Article 1', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'image must be a string' + ); + }); + }); + + it('throws on config with non-string recommendation image', () => { + const page = new Page(null, { + url: 'https://example.com/article', + image: {}, + title: 'Article 1', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'image must be a string' + ); + }); + }); + + it('throws on config with pages from different domains', () => { + const page = new Page(null, { + url: 'https://othersite.com/article1', + image: 'https://othersite.com/image.png', + title: 'Article 1', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'pages must be from the same origin as the current document' + ); + }); + }); + + it('throws on config with pages from different subdomains', () => { + const page = new Page(null, { + url: 'https://www.example.com/article1', + image: 'https://example.com/image.png', + title: 'Article 1', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'pages must be from the same origin as the current document' + ); + }); + }); + + it('throws on config with pages on different ports', () => { + const page = new Page(null, { + url: 'https://example.com:8080/article1', + image: 'https://example.com/image.png', + title: 'Article 1', + }); + + allowConsoleError(() => { + expect(() => validatePage(page, documentUrl)).to.throw( + 'pages must be from the same origin as the current document' + ); + }); + }); +}); diff --git a/extensions/amp-next-page/0.2/utils.js b/extensions/amp-next-page/1.0/utils.js similarity index 97% rename from extensions/amp-next-page/0.2/utils.js rename to extensions/amp-next-page/1.0/utils.js index 5470bda6f96d..17a94c3ae010 100644 --- a/extensions/amp-next-page/0.2/utils.js +++ b/extensions/amp-next-page/1.0/utils.js @@ -48,6 +48,7 @@ export function validatePage(page, hostUrl) { user().assertString(page.url, 'page url must be a string'); const base = getSourceUrl(hostUrl); + const {origin} = parseUrlDeprecated(hostUrl); page.url = resolveRelativeUrl(page.url, base); const url = validateUrl(page.url, hostUrl); diff --git a/extensions/amp-next-page/0.2/visibility-observer.js b/extensions/amp-next-page/1.0/visibility-observer.js similarity index 100% rename from extensions/amp-next-page/0.2/visibility-observer.js rename to extensions/amp-next-page/1.0/visibility-observer.js diff --git a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads-attribution.css b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads-attribution.css index 848c7712d9d7..f961c3390c50 100644 --- a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads-attribution.css +++ b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads-attribution.css @@ -22,3 +22,10 @@ /* 1 greater than cta layer. */ z-index: 4 !important; } + +.i-amphtml-story-ad-fullbleed.i-amphtml-story-ad-attribution { + /* Ad will be 75vh, align bottom of icon to bottom of ad. */ + bottom: 12.5vh !important; + left: 50% !important; + transform: translateX(-22.5vh) !important; +} diff --git a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.css b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.css index 742562df9ecb..49432d8fa166 100644 --- a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.css +++ b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.css @@ -14,8 +14,7 @@ * limitations under the License. */ - -[desktop] amp-story-page[i-amphtml-loading] { + .i-amphtml-story-desktop-panels amp-story-page[i-amphtml-loading][ad] { /* Move below viewport so that ad preloads */ transform: scale(1.0) translateX(-100%) translateY(200%) !important; } @@ -26,6 +25,13 @@ align-items: center !important; } +.i-amphtml-story-desktop-fullbleed .i-amphtml-cta-container { + /* Ad will be 75vh, align bottom of container to bottom of ad. */ + bottom: 12.5vh !important; + /* 80% (default) - 12.5 (new bottom)*/ + top: 67.5% !important; +} + /* If you are changing anything here that affects font, please update measurer in story-ad-button-text-fitter.js */ .i-amphtml-story-ad-link { @@ -77,9 +83,21 @@ amp-story-page[active] .i-amphtml-story-ad-link { } /* TODO(ccordry): refactor centering logic in amp-ad.css and remove this hack. */ -amp-ad[data-a4a-upgrade-type="amp-ad-network-doubleclick-impl"] > iframe, -amp-ad[type="adsense"] > iframe { +amp-story-page amp-ad[data-a4a-upgrade-type="amp-ad-network-doubleclick-impl"] > iframe, +amp-story-page amp-ad[type="adsense"] > iframe { top: 0 !important; left: 0 !important; transform: translate(0) !important; } + +/* TODO(ccordry) allow advertisers to opt-in to fullscreen ads. */ +.i-amphtml-story-desktop-fullbleed .i-amphtml-story-grid-template-fill > amp-ad > iframe { + left: 50% !important; + right: auto !important; + margin: auto !important; + min-height: 75vh !important; + max-height: 75vh !important; + min-width: calc(3/5 * 75vh) !important; + max-width: calc(3/5 * 75vh) !important; + transform: translateX(-50%) !important; +} diff --git a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js index 3cfa3c41c049..059b431e1cb2 100644 --- a/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js +++ b/extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js @@ -394,7 +394,8 @@ export class AmpStoryAutoAds extends AMP.BaseElement { this.config_, index, this.localizationService_, - devAssert(this.buttonFitter_) + devAssert(this.buttonFitter_), + devAssert(this.storeService_) ); this.maybeForceAdPlacement_(page); diff --git a/extensions/amp-story-auto-ads/0.1/story-ad-page.js b/extensions/amp-story-auto-ads/0.1/story-ad-page.js index 8fcc7092f6bd..c56ae25bac4e 100644 --- a/extensions/amp-story-auto-ads/0.1/story-ad-page.js +++ b/extensions/amp-story-auto-ads/0.1/story-ad-page.js @@ -21,6 +21,10 @@ import { } from './story-ad-analytics'; import {CommonSignals} from '../../../src/common-signals'; import {CtaTypes} from './story-ad-localization'; +import { + StateProperty, + UIType, +} from '../../amp-story/1.0/amp-story-store-service'; import {assertConfig} from '../../amp-ad-exit/0.1/config'; import {assertHttpsUrl} from '../../../src/url'; import {CSS as attributionCSS} from '../../../build/amp-story-auto-ads-attribution-0.1.css'; @@ -38,6 +42,7 @@ import {dict} from '../../../src/utils/object'; import {getA4AMetaTags, getFrameDoc} from './utils'; import {getServicePromiseForDoc} from '../../../src/service'; import {parseJson} from '../../../src/json'; +import {setStyle} from '../../../src/style'; /** @const {string} */ const TAG = 'amp-story-auto-ads:page'; @@ -48,6 +53,9 @@ const TIMEOUT_LIMIT = 10000; // 10 seconds /** @const {string} */ const GLASS_PANE_CLASS = 'i-amphtml-glass-pane'; +/** @const {string} */ +const DESKTOP_FULLBLEED_CLASS = 'i-amphtml-story-ad-fullbleed'; + /** @enum {string} */ const PageAttributes = { LOADING: 'i-amphtml-loading', @@ -75,8 +83,9 @@ export class StoryAdPage { * @param {number} index * @param {!./story-ad-localization.StoryAdLocalization} localization * @param {!./story-ad-button-text-fitter.ButtonTextFitter} buttonFitter + * @param {!../../amp-story/0.1/amp-story-store-service.AmpStoryStoreService|!../../amp-story/1.0/amp-story-store-service.AmpStoryStoreService} storeService */ - constructor(ampdoc, config, index, localization, buttonFitter) { + constructor(ampdoc, config, index, localization, buttonFitter, storeService) { /** @private @const {!JsonObject} */ this.config_ = config; @@ -107,6 +116,9 @@ export class StoryAdPage { /** @private {?Element} */ this.adElement_ = null; + /** @private {?Element} */ + this.adChoicesIcon_ = null; + /** @private {?Document} */ this.adDoc_ = null; @@ -127,6 +139,9 @@ export class StoryAdPage { /** @private {boolean} */ this.viewed_ = false; + + /** @private @const {!../../amp-story/0.1/amp-story-store-service.AmpStoryStoreService|!../../amp-story/1.0/amp-story-store-service.AmpStoryStoreService} */ + this.storeService_ = storeService; } /** @return {?Document} ad document within FIE */ @@ -299,7 +314,14 @@ export class StoryAdPage { 'id': this.id_, }); - return createElementWithAttributes(this.doc_, 'amp-story-page', attributes); + const page = createElementWithAttributes( + this.doc_, + 'amp-story-page', + attributes + ); + // TODO(ccordry): Allow creative to change default background color. + setStyle(page, 'background-color', '#212125'); + return page; } /** @@ -480,7 +502,7 @@ export class StoryAdPage { }) ); - const adChoicesIcon = createElementWithAttributes( + this.adChoicesIcon_ = createElementWithAttributes( this.doc_, 'img', dict({ @@ -488,16 +510,40 @@ export class StoryAdPage { 'src': src, }) ); + this.storeService_.subscribe( + StateProperty.UI_STATE, + uiState => { + this.onUIStateUpdate_(uiState); + }, + true /** callToInitialize */ + ); - adChoicesIcon.addEventListener( + this.adChoicesIcon_.addEventListener( 'click', this.handleAttributionClick_.bind(this, href) ); - createShadowRootWithStyle(root, adChoicesIcon, attributionCSS); + createShadowRootWithStyle(root, this.adChoicesIcon_, attributionCSS); this.pageElement_.appendChild(root); } + /** + * Reacts to UI state updates and passes the information along as + * attributes to the shadowed attribution icon. + * @param {!UIType} uiState + * @private + */ + onUIStateUpdate_(uiState) { + if (!this.adChoicesIcon_) { + return; + } + + this.adChoicesIcon_.classList.toggle( + DESKTOP_FULLBLEED_CLASS, + uiState === UIType.DESKTOP_FULLBLEED + ); + } + /** * @private * @param {string} href diff --git a/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-fullbleed.js b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-fullbleed.js new file mode 100644 index 000000000000..603971723bc2 --- /dev/null +++ b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-fullbleed.js @@ -0,0 +1,154 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + clickThroughPages, + switchToAdFrame, +} from './test-amp-story-auto-ads-utils'; + +const viewport = { + HEIGHT: 768, + WIDTH: 1024, +}; + +describes.endtoend( + 'amp-story-auto-ads:fullbleed', + { + testUrl: + 'http://localhost:8000/test/fixtures/e2e/amp-story-auto-ads/fullbleed.html', + initialRect: {width: viewport.WIDTH, height: viewport.HEIGHT}, + environments: ['single', 'viewer-demo'], + }, + env => { + let controller; + + beforeEach(() => { + controller = env.controller; + }); + + it('should render correctly', async () => { + await clickThroughPages(controller, /* numPages */ 7); + const activePage = await controller.findElement('[active]'); + await expect(controller.getElementAttribute(activePage, 'ad')).to.exist; + await validateAdSize(controller); + await validateAdOverlay(controller); + await validateAdAttribution( + controller, + '/test/fixtures/e2e/amphtml-ads/resource/icon.png' // iconUrl + ); + await validateCta( + controller, + 'https://www.amp.dev' // ctaUrl + ); + + await switchToAdFrame(controller); + const body = await controller.findElement('body'); + await expect(controller.getElementAttribute(body, 'amp-story-visible')).to + .exist; + await controller.switchToParent(); + }); + } +); + +async function validateAdSize(controller) { + const activeIframe = await controller.findElement('[active] iframe'); + // Ad should be centered, 75vh tall, and 3/5 * 75vh wide. + await expect(controller.getElementRect(activeIframe)).to.include({ + left: 339, + top: 96, + right: 685, + bottom: 672, + }); +} + +async function validateAdOverlay(controller) { + const overlayHost = await controller.findElement( + '.i-amphtml-ad-overlay-host' + ); + await controller.switchToShadowRoot(overlayHost); + + const adOverlayContainer = await controller.findElement( + '.i-amphtml-ad-overlay-container' + ); + await expect(controller.getElementAttribute(adOverlayContainer, 'ad-showing')) + .to.exist; + + const adBadge = await controller.findElement('.i-amphtml-story-ad-badge'); + await expect(controller.getElementText(adBadge)).to.equal('Ad'); + await expect(controller.getElementCssValue(adBadge, 'visibility')).to.equal( + 'visible' + ); + // Design spec is 14px from top, 16px from left. + await expect(controller.getElementRect(adBadge)).to.include({ + left: 16, + top: 14, + }); + + await controller.switchToLight(); +} + +async function validateCta(controller, ctaUrl) { + const ctaButton = await controller.findElement('.i-amphtml-story-ad-link'); + await expect(controller.getElementCssValue(ctaButton, 'visibility')).to.equal( + 'visible' + ); + await expect(controller.getElementAttribute(ctaButton, 'target')).to.equal( + '_blank' + ); + await expect(controller.getElementAttribute(ctaButton, 'href')).to.equal( + ctaUrl + ); + await expect(controller.getElementAttribute(ctaButton, 'role')).to.equal( + 'link' + ); + // Overlayed onto centered and resized iframe. + await expect(controller.getElementRect(ctaButton)).to.include({ + bottom: 640, + height: 36, + width: 120, + }); + await expect(controller.getElementCssValue(ctaButton, 'font-size')).to.equal( + '14px' + ); +} + +async function validateAdAttribution(controller, iconUrl) { + const attributionHost = await controller.findElement( + '.i-amphtml-attribution-host' + ); + await controller.switchToShadowRoot(attributionHost); + + const attribution = await controller.findElement( + '.i-amphtml-story-ad-attribution' + ); + await expect(controller.getElementAttribute(attribution, 'src')).to.equal( + iconUrl + ); + await expect( + controller.getElementCssValue(attribution, 'visibility') + ).to.equal('visible'); + + // Aligned to bottom-left of creative. Max height is 15px and asset will be + // scaled down proportionally to fit. + await expect(controller.getElementRect(attribution)).to.include({ + bottom: 672, + left: 339, + height: 15, + width: 15, + }); + + await controller.switchToLight(); +} diff --git a/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-utils.js b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-utils.js new file mode 100644 index 000000000000..b737438a58b0 --- /dev/null +++ b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads-utils.js @@ -0,0 +1,27 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export async function clickThroughPages(controller, numPages) { + for (let i = 0; i < numPages; i++) { + const page = await controller.findElement('[active]'); + await controller.click(page); + } +} + +export async function switchToAdFrame(controller) { + const frame = await controller.findElement('#i-amphtml-ad-page-1 iframe'); + await controller.switchToFrame(frame); +} diff --git a/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads.js b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads.js index 91ff5f017e0c..715ece3aae1c 100644 --- a/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads.js +++ b/extensions/amp-story-auto-ads/0.1/test-e2e/test-amp-story-auto-ads.js @@ -14,6 +14,11 @@ * limitations under the License. */ +import { + clickThroughPages, + switchToAdFrame, +} from './test-amp-story-auto-ads-utils'; + const viewport = { HEIGHT: 823, WIDTH: 500, @@ -100,13 +105,6 @@ describes.endtoend( } ); -async function clickThroughPages(controller, numPages) { - for (let i = 0; i < numPages; i++) { - const page = await controller.findElement('[active]'); - await controller.click(page); - } -} - async function validateAdOverlay(controller) { const overlayHost = await controller.findElement( '.i-amphtml-ad-overlay-host' @@ -187,8 +185,3 @@ async function validateAdAttribution(controller, iconUrl) { await controller.switchToLight(); } - -async function switchToAdFrame(controller) { - const frame = await controller.findElement('#i-amphtml-ad-page-1 iframe'); - await controller.switchToFrame(frame); -} diff --git a/extensions/amp-story-auto-ads/0.1/test/test-story-ad-page.js b/extensions/amp-story-auto-ads/0.1/test/test-story-ad-page.js index 862171a1fc39..ee4d1c1067e9 100644 --- a/extensions/amp-story-auto-ads/0.1/test/test-story-ad-page.js +++ b/extensions/amp-story-auto-ads/0.1/test/test-story-ad-page.js @@ -16,6 +16,11 @@ import * as dom from '../../../../src/dom'; import * as service from '../../../../src/service'; +import { + Action, + UIType, + getStoreService, +} from '../../../amp-story/1.0/amp-story-store-service'; import {ButtonTextFitter} from '../story-ad-button-text-fitter'; import {CommonSignals} from '../../../../src/common-signals'; import {StoryAdAnalytics} from '../story-ad-analytics'; @@ -42,6 +47,7 @@ describes.realWin('story-ad-page', {amp: true}, env => { let doc; let storyAutoAdsEl; let storyAdPage; + let storeService; beforeEach(() => { win = env.win; @@ -49,12 +55,14 @@ describes.realWin('story-ad-page', {amp: true}, env => { storyAutoAdsEl = doc.createElement('amp-story-auto-ads'); doc.body.appendChild(storyAutoAdsEl); storyAutoAdsEl.getAmpDoc = () => env.ampdoc; + storeService = getStoreService(win); storyAdPage = new StoryAdPage( storyAutoAdsEl.getAmpDoc(), baseConfig, 1, // index new StoryAdLocalization(win), - new ButtonTextFitter(env.ampdoc) + new ButtonTextFitter(env.ampdoc), + storeService ); }); @@ -373,6 +381,32 @@ describes.realWin('story-ad-page', {amp: true}, env => { ); }); + it('propagates fullbleed state to attribution icon', async () => { + storeService.dispatch(Action.TOGGLE_UI, UIType.DESKTOP_FULLBLEED); + + const iframe = doc.createElement('iframe'); + ampAdElement.appendChild(iframe); + iframe.contentDocument.write(` + + + + + + + `); + await ampAdElement.signals().signal(CommonSignals.INI_LOAD); + await storyAdPage.maybeCreateCta(); + + const attribution = doc.querySelector('.i-amphtml-story-ad-attribution'); + expect(attribution).to.have.class('i-amphtml-story-ad-fullbleed'); + + storeService.dispatch(Action.TOGGLE_UI, UIType.MOBILE); + expect(attribution).not.to.have.class('i-amphtml-story-ad-fullbleed'); + + storeService.dispatch(Action.TOGGLE_UI, UIType.DESKTOP_FULLBLEED); + expect(attribution).to.have.class('i-amphtml-story-ad-fullbleed'); + }); + it('does not create attribution when missing icon', async () => { expectAsyncConsoleError( /amp-story-auto-ads attribution icon must be available/ diff --git a/extensions/amp-story/0.1/test/validator-amp-story-cta-layer-error.out b/extensions/amp-story/0.1/test/validator-amp-story-cta-layer-error.out index c956a6800d3c..59f226d14272 100644 --- a/extensions/amp-story/0.1/test/validator-amp-story-cta-layer-error.out +++ b/extensions/amp-story/0.1/test/validator-amp-story-cta-layer-error.out @@ -67,7 +67,7 @@ amp-story/0.1/test/validator-amp-story-cta-layer-error.html:54:6 Tag 'amp-story- >> ^~~~~~~~~ amp-story/0.1/test/validator-amp-story-cta-layer-error.html:61:4 The tag 'amp-story-cta-layer' may only appear as a descendant of tag 'amp-story-page'. >> ^~~~~~~~~ -amp-story/0.1/test/validator-amp-story-cta-layer-error.html:61:4 Tag 'amp-story-cta-layer' is disallowed as child of tag 'amp-story'. Child tag must be one of ['amp-analytics', 'amp-consent', 'amp-geo', 'amp-pixel', 'amp-sidebar', 'amp-story-access', 'amp-story-auto-ads', 'amp-story-bookend', 'amp-story-page']. (see https://amp.dev/documentation/components/amp-story) +amp-story/0.1/test/validator-amp-story-cta-layer-error.html:61:4 Tag 'amp-story-cta-layer' is disallowed as child of tag 'amp-story'. Child tag must be one of ['amp-analytics', 'amp-consent', 'amp-geo', 'amp-pixel', 'amp-sidebar', 'amp-story-auto-ads', 'amp-story-bookend', 'amp-story-page']. (see https://amp.dev/documentation/components/amp-story) | Illegal CTA layer outside of amp-story-page! | | diff --git a/extensions/amp-story/1.0/amp-story-draggable-drawer.js b/extensions/amp-story/1.0/amp-story-draggable-drawer.js index ab9cc4d1348c..133bcd88d50d 100644 --- a/extensions/amp-story/1.0/amp-story-draggable-drawer.js +++ b/extensions/amp-story/1.0/amp-story-draggable-drawer.js @@ -266,8 +266,6 @@ export class DraggableDrawer extends AMP.BaseElement { return; } - event.stopPropagation(); - const coordinates = this.getClientTouchCoordinates_(event); if (!coordinates) { return; @@ -275,6 +273,18 @@ export class DraggableDrawer extends AMP.BaseElement { const {x, y} = coordinates; + this.touchEventState_.swipingUp = y < this.touchEventState_.lastY; + this.touchEventState_.lastY = y; + + if ( + this.state_ === DrawerState.CLOSED && + !this.touchEventState_.swipingUp + ) { + return; + } + + event.stopPropagation(); + if (this.touchEventState_.isSwipeY === null) { this.touchEventState_.isSwipeY = Math.abs(this.touchEventState_.startY - y) > @@ -284,9 +294,6 @@ export class DraggableDrawer extends AMP.BaseElement { } } - this.touchEventState_.swipingUp = y < this.touchEventState_.lastY; - this.touchEventState_.lastY = y; - this.onSwipeY_({ event, data: { diff --git a/extensions/amp-story/1.0/amp-story-page.js b/extensions/amp-story/1.0/amp-story-page.js index 4446b9cbf1b7..cf2843d2f002 100644 --- a/extensions/amp-story/1.0/amp-story-page.js +++ b/extensions/amp-story/1.0/amp-story-page.js @@ -471,7 +471,9 @@ export class AmpStoryPage extends AMP.BaseElement { if (this.isActive()) { this.advancement_.start(); - this.maybeStartAnimations(); + this.prefersReducedMotion_() + ? this.maybeFinishAnimations_() + : this.maybeStartAnimations_(); this.checkPageHasAudio_(); this.renderOpenAttachmentUI_(); this.findAndPrepareEmbeddedComponents_(); @@ -512,13 +514,8 @@ export class AmpStoryPage extends AMP.BaseElement { */ onUIStateUpdate_(uiState) { // On vertical rendering, render all the animations with their final state. - if (uiState === UIType.VERTICAL && this.animationManager_) { - this.signals() - .whenSignal(CommonSignals.LOAD_END) - .then(() => this.maybeApplyFirstAnimationFrame()) - .then(() => { - this.animationManager_.finishAll(); - }); + if (uiState === UIType.VERTICAL) { + this.maybeFinishAnimations_(); } } @@ -1018,14 +1015,41 @@ export class AmpStoryPage extends AMP.BaseElement { /** * Starts playing animations, if the animation manager is available. + * @private */ - maybeStartAnimations() { + maybeStartAnimations_() { if (!this.animationManager_) { return; } this.animationManager_.animateIn(); } + /** + * Finishes playing animations instantly, if the animation manager is + * available. + * @private + */ + maybeFinishAnimations_() { + if (!this.animationManager_) { + return; + } + this.signals() + .whenSignal(CommonSignals.LOAD_END) + .then(() => this.maybeApplyFirstAnimationFrame()) + .then(() => { + this.animationManager_.finishAll(); + }); + } + + /** + * Whether the device opted in prefers-reduced-motion. + * @return {boolean} + * @private + */ + prefersReducedMotion_() { + return this.win.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + /** * @return {!Promise} */ diff --git a/extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js b/extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js new file mode 100644 index 000000000000..693b9248de2f --- /dev/null +++ b/extensions/amp-story/1.0/amp-story-viewer-messaging-handler.js @@ -0,0 +1,127 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Action, + StateProperty, + getStoreService, +} from './amp-story-store-service'; + +/** @typedef {{property: !StateProperty}} */ +let GetStateConfigurationDef; + +// TODO(#26020): implement and allow retrieving PAGE_ATTACHMENT_STATE. +// TODO(gmajoulet): implement and allow retrieving STORY_PROGRESS. +/** @enum {!GetStateConfigurationDef} */ +const GET_STATE_CONFIGURATIONS = { + 'CURRENT_PAGE_ID': { + property: StateProperty.CURRENT_PAGE_ID, + }, + 'MUTED_STATE': { + property: StateProperty.MUTED_STATE, + }, +}; + +/** @typedef {{action: !Action, isValueValid: function(*):boolean}} */ +let SetStateConfigurationDef; + +/** @enum {!SetStateConfigurationDef} */ +const SET_STATE_CONFIGURATIONS = { + 'MUTED_STATE': { + action: Action.TOGGLE_MUTED, + isValueValid: value => typeof value === 'boolean', + }, +}; + +/** + * Viewer messaging handler. + */ +export class AmpStoryViewerMessagingHandler { + /** + * @param {!Window} win + * @param {!../../../src/service/viewer-interface.ViewerInterface} viewer + */ + constructor(win, viewer) { + /** @private @const {!./amp-story-store-service.AmpStoryStoreService} */ + this.storeService_ = getStoreService(win); + + /** @private @const {!../../../src/service/viewer-interface.ViewerInterface} */ + this.viewer_ = viewer; + } + + /** + * @public + */ + startListening() { + this.viewer_.onMessageRespond('getDocumentState', data => + this.onGetDocumentState_(data) + ); + this.viewer_.onMessageRespond('setDocumentState', data => + this.onSetDocumentState_(data) + ); + } + + /** + * @param {string} eventType + * @param {?JsonObject|string|undefined} data + * @param {boolean=} cancelUnsent + */ + send(eventType, data, cancelUnsent = false) { + this.viewer_.sendMessage(eventType, data, cancelUnsent); + } + + /** + * Handles 'getDocumentState' viewer messages. + * @param {!Object=} data + * @return {!Promise} + * @private + */ + onGetDocumentState_(data = {}) { + const {state} = data; + const config = GET_STATE_CONFIGURATIONS[state]; + + if (!config) { + return Promise.reject(`Invalid 'state' parameter`); + } + + const value = this.storeService_.get(config.property); + + return Promise.resolve({state, value}); + } + + /** + * Handles 'setDocumentState' viewer messages. + * @param {!Object=} data + * @return {!Promise} + * @private + */ + onSetDocumentState_(data = {}) { + const {state, value} = data; + const config = SET_STATE_CONFIGURATIONS[state]; + + if (!config) { + return Promise.reject(`Invalid 'state' parameter`); + } + + if (!config.isValueValid(value)) { + return Promise.reject(`Invalid 'value' parameter`); + } + + this.storeService_.dispatch(config.action, value); + + return Promise.resolve({state, value}); + } +} diff --git a/extensions/amp-story/1.0/amp-story.css b/extensions/amp-story/1.0/amp-story.css index 971f236ef37d..6cc998c243df 100644 --- a/extensions/amp-story/1.0/amp-story.css +++ b/extensions/amp-story/1.0/amp-story.css @@ -40,6 +40,15 @@ amp-consent { z-index: initial !important; } +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0s !important; + transition-duration: 0s !important; + } +} + /** * amp-hidden uses visiblity: hidden that does not propagate to shadow trees * because of amp-story-shadow-reset.css. Use display: none instead. diff --git a/extensions/amp-story/1.0/amp-story.js b/extensions/amp-story/1.0/amp-story.js index 831b962afbff..12cd6f01eebf 100644 --- a/extensions/amp-story/1.0/amp-story.js +++ b/extensions/amp-story/1.0/amp-story.js @@ -54,6 +54,7 @@ import {AmpStoryPage, NavigationDirection, PageState} from './amp-story-page'; import {AmpStoryPageAttachment} from './amp-story-page-attachment'; import {AmpStoryQuiz} from './amp-story-quiz'; import {AmpStoryRenderService} from './amp-story-render-service'; +import {AmpStoryViewerMessagingHandler} from './amp-story-viewer-messaging-handler'; import {AnalyticsVariable, getVariableService} from './variable-service'; import {CSS} from '../../../build/amp-story-1.0.css'; import {CommonSignals} from '../../../src/common-signals'; @@ -76,10 +77,7 @@ import {MediaPool, MediaType} from './media-pool'; import {PaginationButtons} from './pagination-buttons'; import {Services} from '../../../src/services'; import {ShareMenu} from './amp-story-share-menu'; -import { - SwipeXYRecognizer, - SwipeYRecognizer, -} from '../../../src/gesture-recognizers'; +import {SwipeXYRecognizer} from '../../../src/gesture-recognizers'; import {SystemLayer} from './amp-story-system-layer'; import {UnsupportedBrowserLayer} from './amp-story-unsupported-browser-layer'; import {ViewportWarningLayer} from './amp-story-viewport-warning-layer'; @@ -336,6 +334,11 @@ export class AmpStory extends AMP.BaseElement { /** @private @const {!../../../src/service/viewer-interface.ViewerInterface} */ this.viewer_ = Services.viewerForDoc(this.element); + /** @private @const {?AmpStoryViewerMessagingHandler} */ + this.viewerMessagingHandler_ = this.viewer_.isEmbedded() + ? new AmpStoryViewerMessagingHandler(this.win, this.viewer_) + : null; + /** * Store the current paused state, to make sure the story does not play on * resume if it was previously paused. @@ -832,23 +835,31 @@ export class AmpStory extends AMP.BaseElement { this.getViewport().onResize(debounce(this.win, () => this.onResize(), 300)); this.installGestureRecognizers_(); + // TODO(gmajoulet): migrate this to amp-story-viewer-messaging-handler once + // there is a way to navigate to pages that does not involve using private + // amp-story methods. this.viewer_.onMessage('selectPage', data => this.onSelectPage_(data)); + + if (this.viewerMessagingHandler_) { + this.viewerMessagingHandler_.startListening(); + } } /** @private */ installGestureRecognizers_() { + // If the story is within a viewer that enabled the swipe capability, this + // disables the navigation education overlay to enable: + // - horizontal swipe events to the next story + // - vertical swipe events to close the viewer, or open a page attachment + if (this.viewer_.hasCapability('swipe')) { + return; + } + const {element} = this; const gestures = Gestures.get(element, /* shouldNotPreventDefault */ true); - // If the story is within a viewer that enabled the swipe capability, this - // disables the navigation education overlay on the X axis to enable the - // swipe to the next story feature. - const swipeRecognizer = this.viewer_.hasCapability('swipe') - ? SwipeYRecognizer - : SwipeXYRecognizer; - // Shows "tap to navigate" hint when swiping. - gestures.onGesture(swipeRecognizer, gesture => { + gestures.onGesture(SwipeXYRecognizer, gesture => { const {deltaX, deltaY} = gesture.data; const embedComponent = /** @type {InteractiveComponentDef} */ (this.storeService_.get( StateProperty.INTERACTIVE_COMPONENT_STATE @@ -1323,8 +1334,8 @@ export class AmpStory extends AMP.BaseElement { * @private */ onNoNextPage_() { - if (this.viewer_.hasCapability('swipe')) { - this.viewer_./*OK*/ sendMessage('selectDocument', dict({'next': true})); + if (this.viewer_.hasCapability('swipe') && this.viewerMessagingHandler_) { + this.viewerMessagingHandler_.send('selectDocument', dict({'next': true})); return; } @@ -1352,8 +1363,8 @@ export class AmpStory extends AMP.BaseElement { * @private */ onNoPreviousPage_() { - if (this.viewer_.hasCapability('swipe')) { - this.viewer_./*OK*/ sendMessage( + if (this.viewer_.hasCapability('swipe') && this.viewerMessagingHandler_) { + this.viewerMessagingHandler_.send( 'selectDocument', dict({'previous': true}) ); diff --git a/extensions/amp-story/1.0/test/test-amp-story-viewer-messaging-handler.js b/extensions/amp-story/1.0/test/test-amp-story-viewer-messaging-handler.js new file mode 100644 index 000000000000..b6cb161331d8 --- /dev/null +++ b/extensions/amp-story/1.0/test/test-amp-story-viewer-messaging-handler.js @@ -0,0 +1,149 @@ +/** + * Copyright 2020 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Action, + StateProperty, + getStoreService, +} from '../amp-story-store-service'; +import {AmpStoryViewerMessagingHandler} from '../amp-story-viewer-messaging-handler'; + +describes.fakeWin('amp-story-viewer-messaging-handler', {}, env => { + let fakeViewerService; + let storeService; + let viewerMessagingHandler; + + beforeEach(() => { + fakeViewerService = { + responderMap: {}, + onMessageRespond(eventType, responder) { + this.responderMap[eventType] = responder; + }, + receiveMessage(eventType, data) { + if (!this.responderMap[eventType]) { + return; + } + return this.responderMap[eventType](data); + }, + }; + viewerMessagingHandler = new AmpStoryViewerMessagingHandler( + env.win, + fakeViewerService + ); + viewerMessagingHandler.startListening(); + storeService = getStoreService(env.win); + }); + + describe('getDocumentState', () => { + it('should throw if no state', async () => { + try { + await fakeViewerService.receiveMessage('getDocumentState', undefined); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'state' parameter`); + } + }); + + it('should throw if invalid state', async () => { + try { + await fakeViewerService.receiveMessage('getDocumentState', { + state: 'UNEXISTING_STATE', + }); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'state' parameter`); + } + }); + + it('should return the MUTED_STATE', async () => { + const response = await fakeViewerService.receiveMessage( + 'getDocumentState', + {state: 'MUTED_STATE'} + ); + expect(response).to.deep.equal({ + state: 'MUTED_STATE', + value: storeService.get(StateProperty.MUTED_STATE), + }); + }); + + it('should return the CURRENT_PAGE_ID', async () => { + storeService.dispatch(Action.CHANGE_PAGE, {id: 'foo', index: 0}); + const response = await fakeViewerService.receiveMessage( + 'getDocumentState', + {state: 'CURRENT_PAGE_ID'} + ); + expect(response).to.deep.equal({ + state: 'CURRENT_PAGE_ID', + value: storeService.get(StateProperty.CURRENT_PAGE_ID), + }); + }); + }); + + describe('setDocumentState', () => { + it('should throw if no state', async () => { + try { + await fakeViewerService.receiveMessage('setDocumentState', undefined); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'state' parameter`); + } + }); + + it('should throw if invalid state', async () => { + try { + await fakeViewerService.receiveMessage('setDocumentState', { + state: 'UNEXISTING_STATE', + value: true, + }); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'state' parameter`); + } + }); + + it('should throw if no value', async () => { + try { + await fakeViewerService.receiveMessage('setDocumentState', { + state: 'MUTED_STATE', + }); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'value' parameter`); + } + }); + + it('should throw if invalid value', async () => { + try { + await fakeViewerService.receiveMessage('setDocumentState', { + state: 'MUTED_STATE', + value: 'true' /** only accepts booleans */, + }); + return Promise.reject('Previous line should throw an error'); + } catch (error) { + expect(error).to.equal(`Invalid 'value' parameter`); + } + }); + + it('should set a state', async () => { + storeService.dispatch(Action.TOGGLE_MUTED, false); + await fakeViewerService.receiveMessage('setDocumentState', { + state: 'MUTED_STATE', + value: true, + }); + expect(storeService.get(StateProperty.MUTED_STATE)).to.be.true; + }); + }); +}); diff --git a/extensions/amp-story/1.0/test/test-amp-story.js b/extensions/amp-story/1.0/test/test-amp-story.js index d3f5b77abd3d..48943ec355b8 100644 --- a/extensions/amp-story/1.0/test/test-amp-story.js +++ b/extensions/amp-story/1.0/test/test-amp-story.js @@ -54,6 +54,7 @@ describes.realWin( let win, ampdoc; let element; let hasSwipeCapability = false; + let isEmbedded = false; let story; let replaceStateStub; @@ -117,6 +118,10 @@ describes.realWin( .stub(viewer, 'hasCapability') .withArgs('swipe') .returns(hasSwipeCapability); + env.sandbox + .stub(viewer, 'isEmbedded') + .withArgs() + .returns(isEmbedded); env.sandbox.stub(Services, 'viewerForDoc').returns(viewer); registerServiceBuilder(win, 'performance', () => ({ @@ -1116,14 +1121,20 @@ describes.realWin( }); describe('with #cap=swipe', () => { - before(() => (hasSwipeCapability = true)); - after(() => (hasSwipeCapability = false)); + before(() => { + hasSwipeCapability = true; + isEmbedded = true; + }); + after(() => { + hasSwipeCapability = false; + isEmbedded = false; + }); it('should send a message when tapping on last page in viewer', async () => { await createStoryWithPages(1, ['cover']); const sendMessageStub = env.sandbox.stub( - story.viewer_, - 'sendMessage' + story.viewerMessagingHandler_, + 'send' ); await story.layoutCallback(); @@ -1163,14 +1174,20 @@ describes.realWin( }); describe('with #cap=swipe', () => { - before(() => (hasSwipeCapability = true)); - after(() => (hasSwipeCapability = false)); + before(() => { + hasSwipeCapability = true; + isEmbedded = true; + }); + after(() => { + hasSwipeCapability = false; + isEmbedded = false; + }); it('should send a message when tapping on last page in viewer', async () => { await createStoryWithPages(1, ['cover']); const sendMessageStub = env.sandbox.stub( - story.viewer_, - 'sendMessage' + story.viewerMessagingHandler_, + 'send' ); await story.layoutCallback(); @@ -1503,7 +1520,7 @@ describes.realWin( }; describe('without #cap=swipe', () => { - it('should handle touch events at the story level', async () => { + it('should handle h touch events at the story level', async () => { await createStoryWithPages(2); const touchmoveSpy = env.sandbox.spy(); story.win.document.addEventListener('touchmove', touchmoveSpy); @@ -1511,6 +1528,14 @@ describes.realWin( expect(touchmoveSpy).to.not.have.been.called; }); + it('should handle v touch events at the story level', async () => { + await createStoryWithPages(2); + const touchmoveSpy = env.sandbox.spy(); + story.win.document.addEventListener('touchmove', touchmoveSpy); + dispatchSwipeEvent(0, 100); + expect(touchmoveSpy).to.not.have.been.called; + }); + it('should trigger the navigation overlay', async () => { await createStoryWithPages(2); dispatchSwipeEvent(100, 0); @@ -1527,7 +1552,7 @@ describes.realWin( before(() => (hasSwipeCapability = true)); after(() => (hasSwipeCapability = false)); - it('should let touch events bubble up to be forwarded', async () => { + it('should let h touch events bubble up to be forwarded', async () => { await createStoryWithPages(2); const touchmoveSpy = env.sandbox.spy(); story.win.document.addEventListener('touchmove', touchmoveSpy); @@ -1535,6 +1560,14 @@ describes.realWin( expect(touchmoveSpy).to.have.been.called; }); + it('should let v touch events bubble up to be forwarded', async () => { + await createStoryWithPages(2); + const touchmoveSpy = env.sandbox.spy(); + story.win.document.addEventListener('touchmove', touchmoveSpy); + dispatchSwipeEvent(0, 100); + expect(touchmoveSpy).to.have.been.called; + }); + it('should not trigger the navigation education overlay', async () => { await createStoryWithPages(2); dispatchSwipeEvent(100, 0); diff --git a/extensions/amp-story/1.0/test/validator-amp-story-access.html b/extensions/amp-story/1.0/test/validator-amp-story-access.html deleted file mode 100644 index a4f293300199..000000000000 --- a/extensions/amp-story/1.0/test/validator-amp-story-access.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - My Story - - - - - - - - - - - - - - - - - - -

Free page!

-
-
- - - - - - -

You're reading premium content!

-
-
-
- - diff --git a/extensions/amp-story/1.0/test/validator-amp-story-access.out b/extensions/amp-story/1.0/test/validator-amp-story-access.out deleted file mode 100644 index b12cc5399291..000000000000 --- a/extensions/amp-story/1.0/test/validator-amp-story-access.out +++ /dev/null @@ -1,65 +0,0 @@ -PASS -| -| -| -| -| -| -| -| -| -| My Story -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -|

Free page!

-|
-|
-| -| -| -| -| -| -|

You're reading premium content!

-|
-|
-|
-| -| diff --git a/extensions/amp-story/1.0/test/validator-amp-story-amp-experiment-error.out b/extensions/amp-story/1.0/test/validator-amp-story-amp-experiment-error.out index 54a97f3ef307..4669f5607cc6 100644 --- a/extensions/amp-story/1.0/test/validator-amp-story-amp-experiment-error.out +++ b/extensions/amp-story/1.0/test/validator-amp-story-amp-experiment-error.out @@ -81,7 +81,7 @@ amp-story/1.0/test/validator-amp-story-amp-experiment-error.html:61:6 Tag 'amp-e | | >> ^~~~~~~~~ -amp-story/1.0/test/validator-amp-story-amp-experiment-error.html:77:4 Tag 'amp-experiment' is disallowed as child of tag 'amp-story'. Child tag must be one of ['amp-analytics', 'amp-consent', 'amp-geo', 'amp-pixel', 'amp-sidebar', 'amp-story-access', 'amp-story-auto-ads', 'amp-story-bookend', 'amp-story-page']. (see https://amp.dev/documentation/components/amp-story) +amp-story/1.0/test/validator-amp-story-amp-experiment-error.html:77:4 Tag 'amp-experiment' is disallowed as child of tag 'amp-story'. Child tag must be one of ['amp-analytics', 'amp-consent', 'amp-geo', 'amp-pixel', 'amp-sidebar', 'amp-story-auto-ads', 'amp-story-bookend', 'amp-story-page']. (see https://amp.dev/documentation/components/amp-story) | + + + AMP Story + + + + + + + + + + + + + + + +

1

+
+
+ + + +

2

+
+
+ + + +

3

+
+
+ + + +

4

+
+
+ + + +

5

+
+
+ + + +

6

+
+
+ + + + +

7

+
+
+ + + +

8

+
+
+ + + +

9

+
+
+ + + +

10

+
+
+ +
+ + + diff --git a/test/manual/amp-next-page/0.2/amp-next-page-v1.element-visibility.html b/test/manual/amp-next-page/1.0/amp-next-page-v1.element-visibility.html similarity index 100% rename from test/manual/amp-next-page/0.2/amp-next-page-v1.element-visibility.html rename to test/manual/amp-next-page/1.0/amp-next-page-v1.element-visibility.html diff --git a/test/manual/amp-next-page/0.2/amp-next-page.amp.html b/test/manual/amp-next-page/1.0/amp-next-page.amp.html similarity index 99% rename from test/manual/amp-next-page/0.2/amp-next-page.amp.html rename to test/manual/amp-next-page/1.0/amp-next-page.amp.html index e84cb820650b..480a530f0f54 100644 --- a/test/manual/amp-next-page/0.2/amp-next-page.amp.html +++ b/test/manual/amp-next-page/1.0/amp-next-page.amp.html @@ -8,7 +8,7 @@ - + - + - - - -