diff --git a/.travis.yml b/.travis.yml index b5ef6c837b81..dbccee0bcfb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ branches: - master - release - canary + - /^amp-release-.*$/ env: global: - NPM_CONFIG_PROGRESS="false" diff --git a/3p/environment.js b/3p/environment.js index b44d07cd4a4c..ca981321eb71 100644 --- a/3p/environment.js +++ b/3p/environment.js @@ -64,6 +64,7 @@ function manageWin_(win) { installObserver(win); // Existing iframes. maybeInstrumentsNodes(win, win.document.querySelectorAll('iframe')); + blockSyncPopups(win); } @@ -237,6 +238,33 @@ function instrumentEntryPoints(win) { } } +/** + * Blackhole the legacy popups since they should never be used for anything. + * @param {!Window} win + */ +function blockSyncPopups(win) { + let count = 0; + function maybeThrow() { + // Prevent deep recursion. + if (count++ > 2) { + throw new Error('security error'); + } + } + try { + win.alert = maybeThrow; + win.prompt = function() { + maybeThrow(); + return ''; + }; + win.confirm = function() { + maybeThrow(); + return false; + }; + } catch (e) { + console./*OK*/error(e.message, e.stack); + } +} + /** * Run when we just became visible again. Runs all the queued up rafs. * @visibleForTesting diff --git a/3p/integration.js b/3p/integration.js index 57c324c72634..0b9d6eb4d922 100644 --- a/3p/integration.js +++ b/3p/integration.js @@ -30,6 +30,7 @@ import {adreactor} from '../ads/adreactor'; import {adsense} from '../ads/adsense'; import {adtech} from '../ads/adtech'; import {plista} from '../ads/plista'; +import {criteo} from '../ads/criteo'; import {doubleclick} from '../ads/doubleclick'; import {dotandads} from '../ads/dotandads'; import {endsWith} from '../src/string'; @@ -47,6 +48,10 @@ import {taboola} from '../ads/taboola'; import {smartadserver} from '../ads/smartadserver'; import {revcontent} from '../ads/revcontent'; import {openadstream} from '../ads/openadstream'; +import {triplelift} from '../ads/triplelift'; +import {teads} from '../ads/teads'; +import {rubicon} from '../ads/rubicon'; +import {imobile} from '../ads/imobile'; /** * Whether the embed type may be used with amp-embed tag. @@ -62,6 +67,7 @@ register('adreactor', adreactor); register('adsense', adsense); register('adtech', adtech); register('plista', plista); +register('criteo', criteo); register('doubleclick', doubleclick); register('flite', flite); register('taboola', taboola); @@ -76,6 +82,10 @@ register('smartadserver', smartadserver); register('mediaimpact', mediaimpact); register('revcontent', revcontent); register('openadstream', openadstream); +register('triplelift', triplelift); +register('teads', teads); +register('rubicon', rubicon); +register('imobile', imobile); // For backward compat, we always allow these types without the iframe // opting in. diff --git a/3p/remote.html b/3p/remote.html index d59e8a1de444..c160c339f13f 100644 --- a/3p/remote.html +++ b/3p/remote.html @@ -16,7 +16,7 @@ - - - - -

Welcome to the mobile web

- - -``` - -## Required mark-up - -AMP HTML documents MUST - -- start with the doctype ``. -- contain a top-level `` tag (`` is accepted as well). -- contain `` and `` tags (They are optional in HTML). -- contain a `` tag inside their head that points to the regular HTML version of the AMP HTML document or to itself if no such HTML version exists. -- contain a `` tag as the first child of their head tag. -- contain a `` tag inside their head tag. It's also recommended to include `initial-scale=1`. -- contain a `` tag as the last element in their head. -- contain the [AMP boilerplate code](../spec/amp-boilerplate.md) in their head tag. - -Most HTML tags can be used unchanged in AMP HTML. -Certain tags have equivalent custom AMP HTML tags; -other HTML tags are outright banned -(see [HTML Tags in the specification](../spec/amp-html-format.md)). - -# Include an image - -Content pages include more features than just the content. -To get you started, -here's the basic AMP HTML page now with an image: - -```html - - - - - Hello, AMPs - - - - - - - -

Welcome to the mobile web

- - - -``` - -Learn more about -[how to include common features](../docs/include_features.md). - -# Add some styles - -AMPs are web pages; add custom styling using common CSS properties. - -Style elements inside ` - - - - -

Welcome to the mobile web

- - - -``` - -Learn more about adding elements, including extended components, -in [How to Include Common Features](../docs/include_features.md). - -# Page layout - -Externally-loaded resources (like images, ads, videos, etc.) must have height -and width attributes. This ensures that sizes of all elements can be -calculated by the browser via CSS automatically and element sizes won't be -recalculated because of external resources, preventing the page from jumping -around as resources load. - -Moreover, use of the style attribute for tags is not permitted, as this -optimizes impact rendering speed in unpredictable ways. - - - -Learn more in the [AMP HTML Components specification](../spec/amp-html-components.md). - -# Test the page - -Test the page by viewing the page in your local server -and validating the page using the -[Chrome DevTools console](https://developers.google.com/web/tools/javascript/console/). - -1. Include your page in your local directory, for example, -`/ampproject/amphtml/examples`. -2. Get your web server up and running locally. -For a quick web server, run `python -m SimpleHTTPServer`. -4. Open your page, for example, go to -[http://localhost:8000/released.amp.html](http://localhost:8000/released.amp.html). -5. Add "#development=1" to the URL, for example, -[http://localhost:8000/released.amp.html#development=1](http://localhost:8000/released.amp.html#development=1). -6. Open the Chrome DevTools console and check for validation errors. - - - -# Final steps before publishing - -Congrats! You've tested your page locally and fixed all validation errors. - -Learn more about tools that can help you get your content production ready in -[Set Up Your Build Tools](https://developers.google.com/web/tools/setup/workspace/setup-buildtools). diff --git a/docs/include_features.md b/docs/include_features.md deleted file mode 100644 index eb95e5151511..000000000000 --- a/docs/include_features.md +++ /dev/null @@ -1,316 +0,0 @@ -**Important:** We're deprecating this guide soon. See the newer version: [Include Third-Party Content](https://www.ampproject.org/docs/guides/third_party_components.html). -For all the latest docs, go to [ampproject.org](https://www.ampproject.org). - -# How to Include Common Features - -AMP HTML components make it simple for you to control your content. -Learn how to include common features in your pages using these elements. - -Make sure to review the documentation for each component individually as a reference: -* [AMP HTML Built-in Components](../builtins/README.md) -* [AMP HTML Extended Components](../extensions/README.md). - -# Display an iframe - -Display an iframe in your page using the -[`amp-iframe`](../extensions/amp-iframe/amp-iframe.md) element. - -`amp-iframe` requirements: - -* Must be at least 600px or 75% of the first viewport away from the top (except for iframes implemented with a placeholder, as described below). -* Can only request resources via HTTPS, and they must not be in the same origin as the container, -unless they do not specify allow-same-origin. - -To include an `amp-iframe` in your page, -first include the following script to the ``, which loads the additional code for the extended component: - -```html - -``` - -An example `amp-iframe` from the -[released.amp example](https://github.com/ampproject/amphtml/blob/master/examples/released.amp.html): - -```html - - -``` - -* It is possible to have an `amp-iframe` appear on the top of a document when the `amp-ifame` has a `placeholder` element as shown in the example below. - -```html - - - -``` -- The `amp-iframe` must contain an element with the `placeholder` attribute, (for instance an `amp-img` element) which would be rendered as a placeholder till the iframe is ready to be displayed. -- Iframe readiness will be inferred by listening to `onload` of the iframe or an `embed-ready` postmesssage which would be sent by the Iframe document, whichever comes first. - -Example of IFrame embed-ready request: -```javascript -window.parent./*OK*/postMessage({ - sentinel: 'amp', - type: 'embed-ready' -}, '*'); -``` - -# Media - -Include images, video, and audio in your page using AMP media elements. - -## Include an image - -Include an image in your page -using the [`amp-img`](../builtins/amp-img.md) element. - -`amp-img` requirements: - -* Must include an explicit width and height. -* Recommended: include a placeholder in case the image resource fails to load. - -Responsive image example: -```html - -``` -Fixed-size image example: -```html - -``` -Hidden image example: -```html - -``` -The AMP HTML runtime can effectively manage image resources, -choosing to delay or prioritize resource loading -based on the viewport position, system resources, connection bandwidth, or other factors. - -If the resource requested by the `amp-img` component fails to load, -the space will be blank. -Set a placeholder background color or other visual -using a CSS selector and style on the element itself: -```css -amp-img { - background-color: grey; -} -``` -## Include an animated image - -Include an animated image in your page -using the [`amp-anim`](../extensions/amp-anim/amp-anim.md) element. - -The `amp-anim` element is very similar to the `amp-img` element, -and provides additional functionality to manage loading and playing -of animated images such as GIFs. - -To include an `amp-anim` in your page, -first include the following script to the ``: - -```html - -``` - -The `amp-anim` component can also have an optional placeholder child -to display while the `src` file is loading. -The placeholder is specified via the `placeholder` attribute: -```html - - - - -``` -## Embed a Tweet - -Embed a Twitter Tweet in your page -using the [`amp-twitter`](../extensions/amp-twitter/amp-twitter.md) element. - -To include a tweet in your page, -first include the following script to the ``: - -```html - -``` - -Currently tweets are automatically proportionally scaled -to fit the provided size, -but this may yield less than ideal appearance. -Manually tweak the provided width and height or use the media attribute -to select the aspect ratio based on screen width. - -Example `amp-twitter` from the -[twitter.amp example](../examples/twitter.amp.html): -```html - - -``` - - - -## Include a video - -Include a video in your page -using the [`amp-video`](../builtins/amp-video.md) element. - -Only use this element for direct HTML5 video file embeds. -The element loads the video resource specified by the `src` attribute lazily, -at a time determined by the AMP HTML runtime. - -Include a placeholder before the video starts, and a fallback, -if the browser doesn't support HTML5 video, for example: -```html - -
-

Your browser doesn’t support HTML5 video

-
-
-``` -## Include a youtube video - -Include a youtube video in your page -using the [`amp-youtube`](../extensions/amp-youtube/amp-youtube.md) element. - -You must include the following script in the ``: - -```html - -``` - -The Youtube `data-videoid` can be found in every Youtube video page URL. -For example, in https://www.youtube.com/watch?v=Z1q71gFeRqM, -Z1q71gFeRqM is the video id. - -Use `layout="responsive"` to yield correct layouts for 16:9 aspect ratio videos: -```html - - -``` -## Include an audio resource - -Include an audio resource in your page, -using the [`amp-audio`](../extensions/amp-audio/amp-audio.md) element. - -You must include the following script in the ``: - -```html - -``` - -Only use this element for direct HTML5 audio file embeds. -Like all embedded external resources in an AMP page, -the element loads the audio resource specified by the `src` attribute lazily, -at a time determined by the AMP HTML runtime. - -Include a placeholder before the audio starts, and a fallback, -if the browser doesn't support HTML5 audio, for example: -```html - -
-

Your browser doesn’t support HTML5 audio

-
- - -
-``` -# Add Border Box Sizing - -Included in the base amp css is a class of `amp-border-box` that will set `box-sizing: border-box` on all elements -nested under that class. You can set this on your `html` tag to provide your page with default `border-box` sizing. -Individual elements can override this by beating or matching the CSS specificity of `.amp-border-box`. -# Count user page views - -Count user page views -using the [`amp-pixel`](../builtins/amp-pixel.md) element. - -The `amp-pixel` element takes a simple URL to send a GET request -to when the tracking pixel is loaded. - -Use the special string `$RANDOM` to add a random number -to the URL if required. - -For example, `` -makes a request to something like `https://www.my-analytics.com/?rand=8390278471201`, -where the $RANDOM value is randomly generated upon each impression. - -An example `amp-pixel` from the -[everything.amp example](https://github.com/ampproject/amphtml/blob/master/examples/everything.amp.html): -```html - -``` -# Monetization through ads - -The following ad networks are supported in AMP HTML pages: - -- [A9](../ads/a9.md) -- [Adform](../ads/adform.md) -- [AdReactor](../ads/adreactor.md) -- [AdSense](../ads/adsense.md) -- [AdTech](../ads/adtech.md) -- [Doubleclick](../ads/doubleclick.md) - -## Display an ad - -Display an ad in your page -using the [`amp-ad`](../builtins/amp-ad.md) element. -Only ads served via HTTPS are supported. - -No ad network provided JavaScript is allowed to run inside the AMP document. -Instead the AMP runtime loads an iframe from a -different origin (via iframe sandbox) -and executes the ad network’s JS inside that iframe sandbox. - -You must specify the ad width and height, and the ad network type. -The `type` identifies the ad network's template. -Different ad types require different `data-*` attributes. -```html - - -``` -If supported by the ad network, -include a `placeholder` -to be shown if no ad is available: -```html - -
Have a great day!
-
-``` diff --git a/examples/ads.amp.html b/examples/ads.amp.html index 82890c2e28cb..944a34bc108a 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -70,6 +70,15 @@

AdTech

src="https://adserver.adtechus.com/addyn/3.0/5280.1/2274008/0/-1/ADTECH;size=300x250;key=plumber;alias=careerbear-ros-middle1;loc=300;;target=_blank;grp=27980912;misc=3767074"> +

Teads

+ + +
Teads fallback - Discover inRead by Teads !
+
+

Doubleclick

Challenging ad. data-slot="/35096353/amptesting/badvideoad"> +

Criteo

+ + +

Flite

Revcontent Widget with placeholder and fallback
200 Billion Content recommendations!
+

TripleLift

+ + + +

Rubicon Project Smart Tag

+ +
Ad Loading...
+
Ad Load Failed.
+
+ +

Rubicon Project FastLane Single Slot

+ +
Ad Loading...
+
Ad Load Failed.
+
+ +

I-Mobile 320x50 banner

+ + diff --git a/examples/analytics.amp.html b/examples/analytics.amp.html index 37e6319ad628..e7fc1096524b 100644 --- a/examples/analytics.amp.html +++ b/examples/analytics.amp.html @@ -175,6 +175,25 @@ + + + + + + diff --git a/examples/instagram.amp.html b/examples/instagram.amp.html index a78fe03f38fb..bd1740109a9d 100644 --- a/examples/instagram.amp.html +++ b/examples/instagram.amp.html @@ -35,5 +35,18 @@

Instagram

layout="responsive"> + + + + + + diff --git a/examples/social-share.amp.html b/examples/social-share.amp.html new file mode 100644 index 000000000000..e9433317eafe --- /dev/null +++ b/examples/social-share.amp.html @@ -0,0 +1,51 @@ + + + + + Hello, AMPs + + + + + + + + +

Social Share

+ + + + + + + + +
+ +
+
+ + + diff --git a/examples/twitter.amp.html b/examples/twitter.amp.html index 0d17ff94ebeb..78a644adc66b 100644 --- a/examples/twitter.amp.html +++ b/examples/twitter.amp.html @@ -19,6 +19,7 @@

Twitter

+ Twitter data-cards="hidden"> +

Intentionally non-existing-tweet

+ + + + diff --git a/examples/viewer-integr-messaging.js b/examples/viewer-integr-messaging.js index 9445e6efefc5..dbe009a33cb7 100644 --- a/examples/viewer-integr-messaging.js +++ b/examples/viewer-integr-messaging.js @@ -21,9 +21,10 @@ * @param {string} targetOrigin * @param {function(string, *, boolean):(!Promise<*>|undefined)} * requestProcessor + * @param {string=} opt_targetId * @constructor */ -function ViewerMessaging(target, targetOrigin, requestProcessor) { +function ViewerMessaging(target, targetOrigin, requestProcessor, opt_targetId) { this.sentinel_ = '__AMP__'; this.requestSentinel_ = this.sentinel_ + 'REQUEST'; this.responseSentinel_ = this.sentinel_ + 'RESPONSE'; @@ -33,6 +34,8 @@ function ViewerMessaging(target, targetOrigin, requestProcessor) { /** @const @private {!Widnow} */ this.target_ = target; + /** @const @private {string|undefined} */ + this.targetId_ = opt_targetId; /** @const @private {string} */ this.targetOrigin_ = targetOrigin; /** @const @private {function(string, *, boolean):(!Promise<*>|undefined)} */ @@ -74,7 +77,7 @@ ViewerMessaging.prototype.sendRequest = function(eventType, payload, * @private */ ViewerMessaging.prototype.onMessage_ = function(event) { - if (event.source != this.target_ && event.origin != this.targetOrigin_) { + if (event.source != this.target_ || event.origin != this.targetOrigin_) { return; } var message = event.data; diff --git a/examples/viewer-integr.js b/examples/viewer-integr.js index b40bf16ccaca..edb900478ba1 100644 --- a/examples/viewer-integr.js +++ b/examples/viewer-integr.js @@ -67,7 +67,7 @@ function whenMessagingLoaded(callback) { var messaging = new ViewerMessaging(window.parent, viewerOrigin, function(type, payload, awaitResponse) { return viewer.receiveMessage(type, payload, awaitResponse); - }); + }, window.location.href); viewer.setMessageDeliverer(function(type, payload, awaitResponse) { return messaging.sendRequest(type, payload, awaitResponse); }, viewerOrigin); diff --git a/examples/viewer.html b/examples/viewer.html index b5baebbe6a64..16900f491309 100644 --- a/examples/viewer.html +++ b/examples/viewer.html @@ -140,8 +140,9 @@ var allViewers = []; - function Viewer(containerId, id, visible) { + function Viewer(containerId, id, url, visible) { this.id = id; + this.url = url; this.alreadyLoaded_ = false; this.stackIndex_ = 0; this.viewportType_ = 'natural'; // "natural" or "virtual" @@ -154,6 +155,7 @@ this.header = document.querySelector('header'); this.container = document.getElementById(containerId); this.iframe = document.createElement('iframe'); + this.iframe.setAttribute('id', 'AMP_DOC_' + this.id); // TODO(dvoytenko): Set to the final value when crbug/577330 is fixed: // this.iframe.setAttribute('sandbox', // 'allow-popups allow-scripts allow-forms allow-pointer-lock' + @@ -199,7 +201,7 @@ }; log('Params:' + JSON.stringify(params)); - var inputUrl = './article-access.amp.max.html#' + paramsStr(params); + var inputUrl = this.url + '#' + paramsStr(params); if (window.location.hash && window.location.hash.length > 1) { inputUrl += '&' + window.location.hash.substring(1); } @@ -228,6 +230,7 @@ Viewer.prototype.awaitHandshake_ = function() { + var targetId = this.iframe.id; var target = this.iframe.contentWindow; var targetOrigin = this.frameOrigin_; var listener = function(event) { @@ -241,7 +244,7 @@ target./*OK*/postMessage('amp-handshake-response', targetOrigin); this.messaging_ = new ViewerMessaging(target, targetOrigin, - this.processRequest_.bind(this)); + this.processRequest_.bind(this), targetId); this.sendRequest_('visibilitychange', { state: this.visibilityState_, prerenderSize: this.prerenderSize @@ -279,6 +282,12 @@ this.iframe.style.display = ''; this.iframe.style.visibility = ''; + + this.iframe.contentWindow.onbeforeunload = (function() { + this.container.style.paddingTop = '120px'; + this.container.textContent = + 'Unload of the AMP iframe is not allowed!'; + }).bind(this); }; @@ -342,6 +351,7 @@ } this.stackIndex_ = stackIndex; window.history.pushState({}, ''); + return Promise.resolve(); }; @@ -353,6 +363,7 @@ } this.stackIndex_ = stackIndex; window.history.go(-1); + return Promise.resolve(); }; @@ -405,6 +416,9 @@ if (type == 'broadcast') { return this.broadcast_(data); } + if (type == 'setFlushParams') { + return; + } return Promise.reject('request not supported: ' + type); }; @@ -458,13 +472,25 @@ function loadAmpDoc() { - new Viewer('container1', '1', true).start(); + new Viewer( + 'container1', + '1', + './everything.amp.max.html', + true).start(); addShowContainer('1'); - new Viewer('container2', '2', false).start(); + new Viewer( + 'container2', + '2', + './article-access.amp.max.html', + false).start(); addShowContainer('2'); - new Viewer('container3', '3', false).start(); + new Viewer( + 'container3', + '3', + './article-access.amp.max.html', + false).start(); addShowContainer('3'); showContainer('1'); diff --git a/examples/youtube.amp.html b/examples/youtube.amp.html index 0cb9323f73e6..e4433e0f5701 100644 --- a/examples/youtube.amp.html +++ b/examples/youtube.amp.html @@ -15,6 +15,7 @@ diff --git a/extensions/amp-access/0.1/amp-access.js b/extensions/amp-access/0.1/amp-access.js index fc2eb62a354b..e58971c7ef8d 100644 --- a/extensions/amp-access/0.1/amp-access.js +++ b/extensions/amp-access/0.1/amp-access.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {CSS} from '../../../build/amp-access-0.1.css'; import {actionServiceFor} from '../../../src/action'; import {analyticsFor} from '../../../src/analytics'; import {assert, assertEnumValue} from '../../../src/asserts'; @@ -24,7 +25,6 @@ import {evaluateAccessExpr} from './access-expr'; import {getService} from '../../../src/service'; import {getValueForExpr} from '../../../src/json'; import {installStyles} from '../../../src/styles'; -import {isExperimentOn} from '../../../src/experiments'; import {isObject} from '../../../src/types'; import {listenOnce} from '../../../src/event-helper'; import {log} from '../../../src/log'; @@ -91,11 +91,7 @@ export class AccessService { constructor(win) { /** @const {!Window} */ this.win = win; - installStyles(this.win.document, $CSS$, () => {}, false, 'amp-access'); - - /** @const @private {boolean} */ - this.isAnalyticsExperimentOn_ = isExperimentOn( - this.win, 'amp-access-analytics'); + installStyles(this.win.document, CSS, () => {}, false, 'amp-access'); const accessElement = document.getElementById('amp-access'); @@ -150,12 +146,15 @@ export class AccessService { /** @private {?JSONObject} */ this.authResponse_ = null; - /** @private {!Promise} */ + /** @const @private {!Promise} */ this.firstAuthorizationPromise_ = new Promise(resolve => { /** @private {!Promise} */ this.firstAuthorizationResolver_ = resolve; }); + /** @private {!Promise} */ + this.lastAuthorizationPromise_ = this.firstAuthorizationPromise_; + /** @private {?Promise} */ this.reportViewPromise_ = null; @@ -254,11 +253,9 @@ export class AccessService { * @private */ analyticsEvent_(eventType) { - if (this.isAnalyticsExperimentOn_) { - this.analyticsPromise_.then(analytics => { - analytics.triggerEvent(eventType); - }); - } + this.analyticsPromise_.then(analytics => { + analytics.triggerEvent(eventType); + }); } /** @@ -288,7 +285,7 @@ export class AccessService { this.runAuthorization_(); // Wait for the "view" signal. - this.scheduleView_(); + this.scheduleView_(VIEW_TIMEOUT); // Listen to amp-access broadcasts from other pages. this.listenToBroadcasts_(); @@ -353,10 +350,14 @@ export class AccessService { } /** + * Returns the promise that resolves when all authorization work has + * completed, including authorization endpoint call and UI update. + * Note that this promise never fails. + * @param {boolean=} opt_disableFallback * @return {!Promise} * @private */ - runAuthorization_() { + runAuthorization_(opt_disableFallback) { if (this.config_.type == AccessType.OTHER) { log.fine(TAG, 'Ignore authorization due to type=other'); this.firstAuthorizationResolver_(); @@ -365,9 +366,9 @@ export class AccessService { log.fine(TAG, 'Start authorization via ', this.config_.authorization); this.toggleTopClass_('amp-access-loading', true); - const promise = this.buildUrl_( + const urlPromise = this.buildUrl_( this.config_.authorization, /* useAuthData */ false); - return promise.then(url => { + const promise = urlPromise.then(url => { log.fine(TAG, 'Authorization URL: ', url); return this.timer_.timeoutPromise( AUTHORIZATION_TIMEOUT, @@ -377,7 +378,7 @@ export class AccessService { })); }).catch(error => { this.analyticsEvent_('access-authorization-failed'); - if (this.config_.authorizationFallbackResponse) { + if (this.config_.authorizationFallbackResponse && !opt_disableFallback) { // Use fallback. setTimeout(() => {throw error;}); return this.config_.authorizationFallbackResponse; @@ -401,6 +402,10 @@ export class AccessService { this.toggleTopClass_('amp-access-loading', false); this.toggleTopClass_('amp-access-error', true); }); + // The "first" promise must always succeed first. + this.lastAuthorizationPromise_ = Promise.all( + [this.firstAuthorizationPromise_, promise]); + return promise; } /** @@ -420,9 +425,6 @@ export class AccessService { * @return {?Promise} */ getAccessReaderId() { - if (!this.isAnalyticsExperimentOn_) { - return null; - } if (!this.enabled_) { return null; } @@ -430,25 +432,27 @@ export class AccessService { } /** - * Returns the field from the authorization response. If the authorization - * response have not been received yet, the result will be `null`. + * Returns the promise that will yield the value of the specified field from + * the authorization response. This method will wait for the most recent + * authorization request to complete. * * This is a restricted API. * * @param {string} field - * @return {*|null} + * @return {?Promise<*|null>} */ getAuthdataField(field) { - if (!this.isAnalyticsExperimentOn_) { - return null; - } - if (!this.enabled_ || !this.authResponse_) { + if (!this.enabled_) { return null; } - return getValueForExpr(this.authResponse_, field) || null; + return this.lastAuthorizationPromise_.then(() => { + if (!this.authResponse_) { + return null; + } + return getValueForExpr(this.authResponse_, field) || null; + }); } - /** * @return {!Promise} Returns a promise for the initial authorization. */ @@ -567,44 +571,48 @@ export class AccessService { } /** + * @param {time} timeToView * @private */ - scheduleView_() { + scheduleView_(timeToView) { onDocumentReady(this.win.document, () => { if (this.viewer_.isVisible()) { - this.reportWhenViewed_(); + this.reportWhenViewed_(timeToView); } this.viewer_.onVisibilityChanged(() => { if (this.viewer_.isVisible()) { - this.reportWhenViewed_(); + this.reportWhenViewed_(timeToView); } }); }); } /** + * @param {time} timeToView * @return {!Promise} * @private */ - reportWhenViewed_() { + reportWhenViewed_(timeToView) { if (this.reportViewPromise_) { return this.reportViewPromise_; } log.fine(TAG, 'start view monitoring'); - this.reportViewPromise_ = this.whenViewed_() + this.reportViewPromise_ = this.whenViewed_(timeToView) + .then(() => { + // Wait for the most recent authorization flow to complete. + return this.lastAuthorizationPromise_; + }) .then(() => { + // Report the analytics event. this.analyticsEvent_('access-viewed'); - // Wait for the first authorization flow to complete. - return this.firstAuthorizationPromise_; + return this.reportViewToServer_(); }) - .then( - this.reportViewToServer_.bind(this), - reason => { - // Ignore - view has been canceled. - log.fine(TAG, 'view cancelled:', reason); - this.reportViewPromise_ = null; - throw reason; - }); + .catch(reason => { + // Ignore - view has been canceled. + log.fine(TAG, 'view cancelled:', reason); + this.reportViewPromise_ = null; + throw reason; + }); this.reportViewPromise_.then(this.broadcastReauthorize_.bind(this)); return this.reportViewPromise_; } @@ -612,10 +620,18 @@ export class AccessService { /** * The promise will be resolved when a view of this document has occurred. It * will be rejected if the current impression should not be counted as a view. + * @param {time} timeToView Pass the value of 0 when this method is called + * as the result of the user action. * @return {!Promise} * @private */ - whenViewed_() { + whenViewed_(timeToView) { + if (timeToView == 0) { + // Immediate view has been registered. This will happen when this method + // is called as the result of the user action. + return Promise.resolve(); + } + // Viewing kick off: document is visible. const unlistenSet = []; return new Promise((resolve, reject) => { @@ -627,7 +643,7 @@ export class AccessService { })); // 2. After a few seconds: register a view. - const timeoutId = this.timer_.delay(resolve, VIEW_TIMEOUT); + const timeoutId = this.timer_.delay(resolve, timeToView); unlistenSet.push(() => this.timer_.cancel(timeoutId)); // 3. If scrolled: register a view. @@ -744,8 +760,12 @@ export class AccessService { if (success) { this.loginAnalyticsEvent_(type, 'success'); this.broadcastReauthorize_(); - // Repeat the authorization flow. - return this.runAuthorization_(); + // Repeat the authorization and pingback flows. Pingback is repeated + // in this case since this is now a new "view" with a different access + // profile. + return this.runAuthorization_(/* disableFallback */ true).then(() => { + this.scheduleView_(/* timeToView */ 0); + }); } else { this.loginAnalyticsEvent_(type, 'rejected'); } diff --git a/extensions/amp-access/0.1/amp-login-done-dialog.js b/extensions/amp-access/0.1/amp-login-done-dialog.js index 34c878c3ad0e..8fae933f53de 100644 --- a/extensions/amp-access/0.1/amp-login-done-dialog.js +++ b/extensions/amp-access/0.1/amp-login-done-dialog.js @@ -208,9 +208,21 @@ export class LoginDoneDialog { } const doc = this.win.document; - doc.documentElement.classList.toggle('amp-postback-error', true); + doc.documentElement.classList.toggle('amp-error', true); + doc.documentElement.setAttribute('data-error', 'postback'); doc.getElementById('closeButton').onclick = () => { - this.win.close(); + try { + this.win.close(); + } catch (e) { + // Ignore. + } + // Give a little time to actually close. If it didn't work, set the flag + // for closing failure. + this.win.setTimeout(() => { + if (!this.win.closed) { + doc.documentElement.setAttribute('data-error', 'close'); + } + }, 1000); }; } } diff --git a/extensions/amp-access/0.1/amp-login-done.html b/extensions/amp-access/0.1/amp-login-done.html index bebe97e4fd77..33f9fdb4ffed 100644 --- a/extensions/amp-access/0.1/amp-login-done.html +++ b/extensions/amp-access/0.1/amp-login-done.html @@ -12,6 +12,7 @@ } header { + margin-bottom: 32px; padding: 16px; display: -webkit-box; display: -webkit-flex; @@ -58,18 +59,28 @@ margin: 16px; } + .subsection { + margin: 8px 0; + } + [lang] { display: none; } - .button-section { + .error-section { + display: none; + } + + .amp-error .progress-section { display: none; } - .amp-postback-error .button-section { + + .amp-error[data-error="postback"] .postback-failed-section { display: block; } - .amp-postback-error .progress-section { - display: none; + + .amp-error[data-error="close"] .close-failed-section { + display: block; } #closeButton { @@ -78,6 +89,7 @@ font-weight: 300; padding: 8px 0; text-decoration: none; + text-transform: uppercase; } @@ -99,23 +111,404 @@
-
- Please wait... +
Opening page...
+
Maak tans bladsy oop …
+
ገጽ በመክፈት ላይ...
+
جارٍ فتح الصفحة...
+
Старонка адкрываецца...
+
Страницата се отваря...
+
পৃষ্ঠা খোলা হচ্ছে…
+
Stranica se otvara...
+
S'està obrint la pàgina…
+
Pe ouver paz...
+
Otevírání stránky...
+
Åbner siden...
+
Seite wird geöffnet...
+
Άνοιγμα σελίδας...
+
Opening page...
+
Abriendo la página...
+
Abriendo página…
+
Lehe avamine …
+
Orria irekitzen…
+
در حال باز کردن صفحه...
+
Avataan sivua…
+
Binubuksan ang page...
+
Ouverture de la page...
+
Abrindo páxina...
+
पृष्ठ खोला जा रहा है...
+
Otvaranje stranice...
+
Oldal megnyitása...
+
Membuka laman...
+
Opnar síðu...
+
Apertura pagina...
+
פותח את הדף...
+
ページを開いています...
+
მიმდინარეობს გვერდის გახსნა...
+
កំពុងបើកទំព័រ...
+
ಪುಟ ತೆರೆಯಲಾಗುತ್ತಿದೆ...
+
페이지 여는 중...
+
ກຳລັງເປີດໜ້າ...
+
Atidaromas puslapis...
+
Notiek lapas atvēršana...
+
Manokatra pejy…
+
Се отвора страницата...
+
പേജ് തുറക്കുന്നു...
+
Хуудсыг нээж байна...
+
पृष्ठ उघडत आहे...
+
Membuka halaman…
+
စာမျက်နှာကို ဖွင့်နေသည်...
+
पृष्ठ खोल्दै...
+
De pagina wordt geopend...
+
Åpner siden ...
+
ਪੰਨਾ ਖੋਲ੍ਹਿਆ ਜਾ ਰਿਹਾ ਹੈ...
+
Otwieram stronę...
+
A abrir a página…
+
Abrindo página…
+
Avrel pagina...
+
Se deschide pagina...
+
Страница открывается...
+
පිටුව විවෘත කරමින්...
+
Otvára sa stránka...
+
Odpiranje strani ...
+
Bogga furitaanka ...
+
Faqja po hapet...
+
Отварање странице...
+
E bula leqephe...
+
Öppnar sida ...
+
Inafungua ukurasa...
+
பக்கத்தைத் திறக்கிறது...
+
పేజీని తెరుస్తోంది...
+
กำลังเปิดหน้าเว็บ...
+
E bula tsebe...
+
Sayfa açılıyor...
+
Відкривається сторінка…
+
صفحہ کھل رہا ہے…
+
Đang mở trang...
+
正在打开网页…
+
正在開啟網頁...
+
Ivula ikhasi...
+
+ +
+
+
Couldn't open page
+
Kon nie bladsy oopmaak nie
+
ገጽ መከፈት አልተቻለም
+
تعذَّر فتح الصفحة
+
Не атрымалася адкрыць старонку
+
Страницата не можа да се отвори
+
পৃষ্ঠা খোলা যায়নি
+
Nije moguće otvoriti stranicu
+
No s'ha pogut obrir la pàgina
+
Pa'n kapab ouver paz
+
Stránku nelze otevřít
+
Siden kunne ikke åbnes
+
Seite konnte nicht geöffnet werden
+
Δεν ήταν δυνατό το άνοιγμα της σελίδας
+
Couldn't open page
+
No se ha podido abrir la página
+
No se pudo abrir la página
+
Lehte ei saanud avada
+
Ezin da ireki orria
+
صفحه باز نشد
+
Sivun avaaminen epäonnistui.
+
Hindi mabuksan ang page
+
La page n'a pas pu être ouverte.
+
Non se puido abrir a páxina
+
पृष्ठ खोला नहीं जा सका
+
Stranica se ne može otvoriti
+
Nem sikerült megnyitni az oldalt
+
Tidak dapat membuka laman
+
Ekki tókst að opna síðuna
+
Impossibile aprire la pagina
+
לא ניתן היה לפתוח את הדף
+
ページを開けませんでした
+
გვერდის გახსნა ვერ მოხერხდა
+
មិនអាចបើកទំព័របានទេ
+
ಪುಟ ತೆರೆಯಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ
+
페이지를 열 수 없습니다.
+
ບໍ່ສາມາດເປີດໜ້າໄດ້
+
Nepavyko atidaryti puslapio
+
Nevarēja atvērt lapu
+
Tsy afa-nanokatra pejy
+
Не можеше да се отвори страницата
+
പേജ് തുറക്കാനായില്ല
+
Хуудсыг нээж чадсангүй
+
पृष्ठ उघडणे शक्य झाले नाही
+
Tidak dapat membuka halaman
+
စာမျက်နှာကို ဖွင့်၍မရခဲ့ပါ
+
पृष्ठ खोल्न सकिएन
+
Kan de pagina niet openen
+
Kunne ikke åpne siden
+
ਪੰਨਾ ਖੋਲ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ
+
Nie udało się otworzyć strony
+
Não foi possível abrir a página
+
Não foi possível abrir a página.
+
Na poss betg avrir pagina
+
Pagina nu s-a putut deschide
+
Не удалось открыть страницу
+
පිටුව විවෘත කළ නොහැකි විය
+
Stránku nie je možné otvoriť
+
Strani ni bilo mogoče odpreti
+
Wuu furi kari waayay bogga
+
Faqja nuk mund të hapej
+
Отварање странице није успело
+
E hlolehile ho bula leqephe
+
Det gick inte att öppna sidan
+
Haikuweza kufungua ukurasa
+
பக்கத்தைத் திறக்க முடியவில்லை
+
పేజీని తెరవడం సాధ్యపడలేదు
+
ไม่สามารถเปิดหน้าเว็บได้
+
Ga e a kgona go bula tsebe
+
Sayfa açılamadı
+
Не вдалося відкрити сторінку
+
صفحہ نہیں کھل سکا
+
Không thể mở trang
+
无法打开网页
+
無法開啟網頁
+
Ayikwazanga ukuvula ikhasi
-
- Attendez, s'il vous plaît... + +
-
- -
- Click here to continue. -
-
- Clique ici pour continuer -
-
+
+
+
An error occurred
+
'n Fout het voorgekom
+
ስህተት ተከስቷል
+
حدث خطأ
+
Адбылася памылка
+
Възникна грешка
+
একটি ত্রুটি ঘটেছে
+
Došlo je do greške
+
S'ha produït un error
+
En lerer n'arive
+
Došlo k chybě
+
Der opstod en fejl
+
Ein Fehler ist aufgetreten
+
Παρουσιάστηκε σφάλμα
+
An error occurred
+
Se ha producido un error
+
Se produjo un error.
+
Ilmnes viga
+
Errore bat gertatu da
+
خطایی روی داد
+
Tapahtui virhe
+
Nagkaroon ng error
+
Une erreur s'est produite.
+
Produciuse un erro
+
कोई त्रुटि आई
+
Došlo je do pogreške
+
Hiba történt
+
Terjadi kesalahan
+
Villa kom upp
+
Si è verificato un errore
+
אירעה שגיאה
+
エラーが発生しました
+
წარმოიშვა შეცდომა
+
កំហុសបានកើតឡើង
+
ದೋಷವೊಂದು ಕಾಣಿಸಿಕೊಂಡಿದೆ
+
오류가 발생했습니다.
+
ມີຄວາມຜິດພາດເກີດຂຶ້ນ
+
Įvyko klaida
+
Radās kļūda
+
Nisy tsy fetezana
+
Се појави грешка
+
ഒരു പിശക് സംഭവിച്ചു
+
Алдаа гарлаа
+
एक त्रुटी आली आहे
+
Berlaku ralat
+
ချွတ်ယွင်းမှုတစ်ခု ဖြစ်ပေါ်ခဲ့သည်
+
एउटा त्रुटि भयो।
+
Er is een fout opgetreden
+
Det oppsto en feil
+
ਇੱਕ ਗੜਬੜ ਹੋ ਗਈ
+
Wystąpił błąd
+
Ocorreu um erro
+
Ocorreu um erro.
+
Igl ha dà in errur
+
A apărut o eroare
+
Произошла ошибка
+
දෝෂයක් සිදු විය
+
Vyskytla sa chyba
+
Prišlo je do napake
+
Qalad ayaa dhacay
+
Ndodhi një gabim
+
Дошло је до грешке
+
Ho hlahile bothata
+
Ett fel uppstod
+
Hitilafu fulani imetokea
+
பிழை ஏற்பட்டது
+
లోపం సంభవించింది
+
เกิดข้อผิดพลาด
+
Go diragetse phoso
+
Bir hata oluştu
+
Сталася помилка
+
ایک خرابی پیش آگئی
+
Đã xảy ra lỗi
+
发生了错误
+
發生錯誤
+
Kuvele iphutha
+
+ +
+
Please close this page
+
Maak hierdie bladsy asseblief toe
+
እባክዎ ይህንን ገጽ ይዝጉት
+
يُرجى إغلاق هذه الصفحة
+
Калі ласка, закрыйце гэту старонку
+
Моля, затворете тази страница
+
অনুগ্রহ করে পৃষ্ঠাটি বন্ধ করুন
+
Zatvorite ovu stranicu
+
Tanqueu aquesta pàgina
+
Silvouple ferm sa paz
+
Zavřete stránku
+
Luk denne side
+
Bitte schließe die Seite
+
Κλείστε αυτήν τη σελίδα
+
Please close this page
+
Cierra esta página
+
Cierra esta página
+
Sulgege see leht
+
Itxi orri hau
+
لطفاً این صفحه را ببندید
+
Sulje tämä sivu
+
Pakisara ang page na ito
+
Veuillez fermer cette page.
+
Pecha esta páxina
+
कृपया यह पृष्ठ बंद करें
+
Zatvorite stranicu
+
Zárja be az oldalt
+
Tutup laman ini
+
Lokaðu þessari síðu
+
Chiudi questa pagina
+
עליך לסגור את הדף
+
このページを閉じてください
+
გთხოვთ, დახუროთ ეს გვერდი
+
សូមបិទទំព័រនេះ
+
ದಯವಿಟ್ಟು ಈ ಪುಟ ಮುಚ್ಚಿ
+
이 페이지를 닫아주세요.
+
ກະລຸນາປິດໜ້ານີ້
+
Uždarykite šį puslapį
+
Lūdzu, aizveriet šo lapu
+
Hidio ity pejy ity azafady
+
Затворете ја страницата
+
ഈ പേജ് അടയ്‌ക്കുക
+
Энэ хуудсыг хаана уу
+
कृपया हे पृष्ठ बंद करा
+
Sila tutup halaman ini
+
ဤစာမျက်နှာကို ပိတ်ပါ
+
कृपया यो पृष्ठ बन्द गर्नुहोस्
+
Sluit deze pagina
+
Lukk denne siden
+
ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਪੰਨੇ ਨੂੰ ਬੰਦ ਕਰੋ
+
Zamknij tę stronę
+
Feche esta página
+
Feche esta página.
+
Per plaschair serrar questa pagina
+
Închideți pagina
+
Закройте страницу
+
මෙම පිටුව වසන්න
+
Zatvorte túto stránku
+
Zaprite to stran
+
Fadlan xidh boggan
+
Mbylle këtë faqe
+
Затворите ову страницу
+
Hle kwala leqephe lena
+
Stäng den här sidan
+
Tafadhali funga ukurasa huu
+
இந்தப் பக்கத்தை மூடவும்
+
దయచేసి ఈ పేజీని మూసివేయండి
+
โปรดปิดหน้าเว็บนี้
+
Tsweetswee tswala tsebe eno
+
Lütfen bu sayfayı kapatın
+
Закрийте цю сторінку
+
براہ کرم اس صفحہ کو بند کر دیں
+
Hãy đóng trang này
+
请关闭此网页
+
請關閉這個網頁
+
Sicela uvale leli khasi
+
diff --git a/extensions/amp-access/0.1/login-dialog.js b/extensions/amp-access/0.1/login-dialog.js index 99bfd87d2e22..94bce7abadea 100644 --- a/extensions/amp-access/0.1/login-dialog.js +++ b/extensions/amp-access/0.1/login-dialog.js @@ -18,6 +18,7 @@ import {getMode} from '../../../src/mode'; import {listen} from '../../../src/event-helper'; import {log} from '../../../src/log'; import {parseUrl, removeFragment} from '../../../src/url'; +import {viewerFor} from '../../../src/viewer'; /** @const */ const TAG = 'AmpAccessLogin'; @@ -38,11 +39,59 @@ const RETURN_URL_REGEX = new RegExp('RETURN_URL'); * @return {!Promise} */ export function openLoginDialog(win, urlOrPromise) { - return new LoginDialog(win, urlOrPromise).open(); + const viewer = viewerFor(win); + const overrideDialog = parseInt(viewer.getParam('dialog'), 10); + if (overrideDialog) { + return new ViewerLoginDialog(viewer, urlOrPromise).open(); + } + return new WebLoginDialog(win, urlOrPromise).open(); } -class LoginDialog { +/** + * The implementation of the Login Dialog delegated via Viewer. + */ +class ViewerLoginDialog { + /** + * @param {!Viewer} viewer + * @param {string|!Promise} urlOrPromise + */ + constructor(viewer, urlOrPromise) { + /** @const {!Viewer} */ + this.viewer = viewer; + + /** @const {string|!Promise} */ + this.urlOrPromise = urlOrPromise; + } + + /** + * Opens the dialog. Returns the promise that will yield with the dialog's + * result or will be rejected if dialog fails. The dialog's result is + * typically a hash string from the return URL. + * @return {!Promise} + */ + open() { + let urlPromise; + if (typeof this.urlOrPromise == 'string') { + urlPromise = Promise.resolve(this.urlOrPromise); + } else { + urlPromise = this.urlOrPromise; + } + return urlPromise.then(url => { + const loginUrl = buildLoginUrl(url, 'RETURN_URL'); + log.fine(TAG, 'Open viewer dialog: ', loginUrl); + return this.viewer.sendMessage('openDialog', { + 'url': loginUrl, + }, true); + }); + } +} + + +/** + * Web-based implementation of the Login Dialog. + */ +class WebLoginDialog { /** * @param {!Window} win * @param {string|!Promise} urlOrPromise @@ -51,7 +100,7 @@ class LoginDialog { /** @const {!Window} */ this.win = win; - /** @const {string} */ + /** @const {string|!Promise} */ this.urlOrPromise = urlOrPromise; /** @private {?function(string)} */ @@ -128,7 +177,7 @@ class LoginDialog { let dialogReadyPromise = null; if (typeof this.urlOrPromise == 'string') { - const loginUrl = this.buildLoginUrl_(this.urlOrPromise, returnUrl); + const loginUrl = buildLoginUrl(this.urlOrPromise, returnUrl); log.fine(TAG, 'Open dialog: ', loginUrl, returnUrl, w, h, x, y); this.dialog_ = this.win.open(loginUrl, '_blank', options); if (this.dialog_) { @@ -139,7 +188,7 @@ class LoginDialog { this.dialog_ = this.win.open('', '_blank', options); if (this.dialog_) { dialogReadyPromise = this.urlOrPromise.then(url => { - const loginUrl = this.buildLoginUrl_(url, returnUrl); + const loginUrl = buildLoginUrl(url, returnUrl); log.fine(TAG, 'Set dialog url: ', loginUrl); this.dialog_.location.replace(loginUrl); }, error => { @@ -217,24 +266,6 @@ class LoginDialog { this.cleanup_(); } - /** - * @param {string} url - * @param {string} returnUrl - * @return {string} - * @private - */ - buildLoginUrl_(url, returnUrl) { - // RETURN_URL has to arrive here unreplaced by UrlReplacements for two - // reasons: (1) sync replacement and (2) if we need to propagate this - // replacement to the viewer. - if (RETURN_URL_REGEX.test(url)) { - return url.replace(RETURN_URL_REGEX, encodeURIComponent(returnUrl)); - } - return url + - (url.indexOf('?') == -1 ? '?' : '&') + - 'return=' + encodeURIComponent(returnUrl); - } - /** * @return {string} * @private @@ -252,3 +283,22 @@ class LoginDialog { return returnUrl + '?url=' + encodeURIComponent(currentUrl); } } + + +/** + * @param {string} url + * @param {string} returnUrl + * @return {string} + * @private + */ +function buildLoginUrl(url, returnUrl) { + // RETURN_URL has to arrive here unreplaced by UrlReplacements for two + // reasons: (1) sync replacement and (2) if we need to propagate this + // replacement to the viewer. + if (RETURN_URL_REGEX.test(url)) { + return url.replace(RETURN_URL_REGEX, encodeURIComponent(returnUrl)); + } + return url + + (url.indexOf('?') == -1 ? '?' : '&') + + 'return=' + encodeURIComponent(returnUrl); +} diff --git a/extensions/amp-access/0.1/test/test-amp-access.js b/extensions/amp-access/0.1/test/test-amp-access.js index 69a65fd4ccb0..c236f1f720a5 100644 --- a/extensions/amp-access/0.1/test/test-amp-access.js +++ b/extensions/amp-access/0.1/test/test-amp-access.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {AccessService} from '../../../../build/all/v0/amp-access-0.1.max'; +import {AccessService} from '../amp-access'; import {Observable} from '../../../../src/observable'; import {installCidService} from '../../../../src/service/cid-impl'; import {markElementScheduledForTesting} from '../../../../src/custom-element'; @@ -214,6 +214,7 @@ describe('AccessService', () => { expect(service.buildLoginUrls_.callCount).to.equal(1); expect(service.runAuthorization_.callCount).to.equal(1); expect(service.scheduleView_.callCount).to.equal(1); + expect(service.scheduleView_.firstCall.args[0]).to.equal(2000); expect(service.listenToBroadcasts_.callCount).to.equal(1); }); @@ -239,18 +240,6 @@ describe('AccessService', () => { expect(service.config_.authorizationFallbackResponse).to.deep.equal( {'error': true}); }); - - it('should NOT send events by default', () => { - element.textContent = JSON.stringify({ - 'authorization': 'https://acme.com/a', - 'pingback': 'https://acme.com/p', - 'login': 'https://acme.com/l', - }); - const service = new AccessService(window); - service.analyticsPromise_ = {then: sandbox.spy()}; - service.analyticsEvent_('an-event'); - expect(service.analyticsPromise_.then.callCount).to.equal(0); - }); }); @@ -323,7 +312,6 @@ describe('AccessService authorization', () => { }; analyticsMock = sandbox.mock(analytics); service.analyticsPromise_ = {then: callback => callback(analytics)}; - service.isAnalyticsExperimentOn_ = true; }); afterEach(() => { @@ -363,7 +351,11 @@ describe('AccessService authorization', () => { .returns(Promise.resolve({access: true})) .once(); service.buildLoginUrls_ = sandbox.spy(); + expect(service.lastAuthorizationPromise_).to.equal( + service.firstAuthorizationPromise_); const promise = service.runAuthorization_(); + const lastPromise = service.lastAuthorizationPromise_; + expect(lastPromise).to.not.equal(service.firstAuthorizationPromise_); expect(document.documentElement).to.have.class('amp-access-loading'); expect(document.documentElement).not.to.have.class('amp-access-error'); expect(service.buildLoginUrls_.callCount).to.equal(0); @@ -375,6 +367,8 @@ describe('AccessService authorization', () => { expect(service.authResponse_).to.exist; expect(service.authResponse_.access).to.be.true; expect(service.buildLoginUrls_.callCount).to.equal(1); + // Last authorization promise stays unchanged. + expect(service.lastAuthorizationPromise_).to.equal(lastPromise); }); }); @@ -398,6 +392,38 @@ describe('AccessService authorization', () => { }); }); + it('should NOT resolve last promise until first success', () => { + expectGetReaderId('reader1'); + xhrMock.expects('fetchJson') + .withExactArgs('https://acme.com/a?rid=reader1', { + credentials: 'include', + requireAmpResponseSourceOrigin: true, + }) + .returns(Promise.reject('intentional')) + .once(); + const promise = service.runAuthorization_(); + let lastResolved = false; + service.lastAuthorizationPromise_.then(() => { + lastResolved = true; + }); + expect(service.lastAuthorizationPromise_).to.not.equal(promise); + expect(service.lastAuthorizationPromise_).to.not.equal( + service.firstAuthorizationPromise_); + return promise.then(() => { + // Skip microtask. + }).then(() => { + // The authorization promise succeeded, but not the last promise. + expect(lastResolved).to.be.false; + // Resolve the first promise. + service.firstAuthorizationResolver_(); + return service.lastAuthorizationPromise_; + }).then(() => { + // After first promise has been resolved, the last promised is resolved + // as well. + expect(lastResolved).to.be.true; + }); + }); + it('should time out authorization flow', () => { expectGetReaderId('reader1'); xhrMock.expects('fetchJson') @@ -446,6 +472,24 @@ describe('AccessService authorization', () => { }); }); + it('should NOT fallback on authorization failure when disabled', () => { + expectGetReaderId('reader1'); + xhrMock.expects('fetchJson') + .withExactArgs('https://acme.com/a?rid=reader1', { + credentials: 'include', + requireAmpResponseSourceOrigin: true, + }) + .returns(Promise.reject('intentional')) + .once(); + service.config_.authorizationFallbackResponse = {'error': true}; + const promise = service.runAuthorization_(/* disableFallback */ true); + expect(document.documentElement).to.have.class('amp-access-loading'); + expect(document.documentElement).not.to.have.class('amp-access-error'); + return promise.then(() => { + expect(document.documentElement).to.have.class('amp-access-error'); + }); + }); + it('should resolve first-authorization promise after success', () => { expectGetReaderId('reader1'); xhrMock.expects('fetchJson') @@ -664,6 +708,7 @@ describe('AccessService pingback', () => { let configElement; let xhrMock; let cidMock; + let analytics; let analyticsMock; let visibilityChanged; let scrolled; @@ -696,12 +741,11 @@ describe('AccessService pingback', () => { cidMock = sandbox.mock(cid); service.cid_ = Promise.resolve(cid); - const analytics = { + analytics = { triggerEvent: () => {}, }; analyticsMock = sandbox.mock(analytics); service.analyticsPromise_ = {then: callback => callback(analytics)}; - service.isAnalyticsExperimentOn_ = true; this.docState_ = { onReady: callback => callback(), @@ -747,7 +791,7 @@ describe('AccessService pingback', () => { analyticsMock.expects('triggerEvent') .withExactArgs('access-viewed') .once(); - const p = service.reportWhenViewed_(); + const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { clock.tick(2001); return p; @@ -763,7 +807,7 @@ describe('AccessService pingback', () => { analyticsMock.expects('triggerEvent') .withExactArgs('access-viewed') .once(); - const p = service.reportWhenViewed_(); + const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { scrolled.fire(); return p; @@ -779,7 +823,7 @@ describe('AccessService pingback', () => { analyticsMock.expects('triggerEvent') .withExactArgs('access-viewed') .once(); - const p = service.reportWhenViewed_(); + const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { let clickEvent; if (document.createEvent) { @@ -798,32 +842,61 @@ describe('AccessService pingback', () => { }); }); - it('should wait for authorization completion', () => { + it('should wait for first authorization completion', () => { expect(service.firstAuthorizationPromise_).to.exist; let firstAuthorizationResolver; service.firstAuthorizationPromise_ = new Promise(resolve => { firstAuthorizationResolver = resolve; }); - analyticsMock.expects('triggerEvent') - .withExactArgs('access-viewed') - .once(); + const triggerEventStub = sandbox.stub(analytics, 'triggerEvent'); + const triggerStart = 1; // First event is "access-authorization-received". service.reportViewToServer_ = sandbox.spy(); - service.reportWhenViewed_(); + service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { clock.tick(2001); return Promise.resolve(); - }).then(() => {}, () => {}).then(() => { + }).then(() => { expect(service.reportViewToServer_.callCount).to.equal(0); + expect(triggerEventStub.callCount).to.equal(triggerStart); firstAuthorizationResolver(); return service.firstAuthorizationPromise_; - }).then(() => {}, () => {}).then(() => { + }).then(() => { expect(service.reportViewToServer_.callCount).to.equal(1); + expect(triggerEventStub.callCount).to.equal(triggerStart + 1); + expect(triggerEventStub.getCall(triggerStart).args[0]) + .to.equal('access-viewed'); + }); + }); + + it('should wait for last authorization completion', () => { + expect(service.lastAuthorizationPromise_).to.exist; + let lastAuthorizationResolver; + service.lastAuthorizationPromise_ = new Promise(resolve => { + lastAuthorizationResolver = resolve; + }); + const triggerEventStub = sandbox.stub(analytics, 'triggerEvent'); + const triggerStart = 1; // First event is "access-authorization-received". + service.reportViewToServer_ = sandbox.spy(); + service.reportWhenViewed_(/* timeToView */ 2000); + return Promise.resolve().then(() => { + clock.tick(2001); + return Promise.resolve(); + }).then(() => { + expect(service.reportViewToServer_.callCount).to.equal(0); + expect(triggerEventStub.callCount).to.equal(triggerStart); + lastAuthorizationResolver(); + return service.lastAuthorizationPromise_; + }).then(() => { + expect(service.reportViewToServer_.callCount).to.equal(1); + expect(triggerEventStub.callCount).to.equal(triggerStart + 1); + expect(triggerEventStub.getCall(triggerStart).args[0]) + .to.equal('access-viewed'); }); }); it('should cancel "viewed" signal after click', () => { service.reportViewToServer_ = sandbox.spy(); - const p = service.reportWhenViewed_(); + const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { service.viewer_.isVisible = () => false; visibilityChanged.fire(); @@ -836,13 +909,17 @@ describe('AccessService pingback', () => { }); it('should schedule "viewed" monitoring only once', () => { - service.whenViewed_ = () => Promise.resolve(); + const timeToView = 2000; + service.whenViewed_ = ttv => { + expect(ttv).to.equal(timeToView); + return Promise.resolve(); + }; service.reportViewToServer_ = sandbox.spy(); - const p1 = service.reportWhenViewed_(); - const p2 = service.reportWhenViewed_(); + const p1 = service.reportWhenViewed_(timeToView); + const p2 = service.reportWhenViewed_(timeToView); expect(p2).to.equal(p1); return p1.then(() => { - const p3 = service.reportWhenViewed_(); + const p3 = service.reportWhenViewed_(timeToView); expect(p3).to.equal(p1); return p3; }).then(() => { @@ -853,7 +930,7 @@ describe('AccessService pingback', () => { it('should re-schedule "viewed" monitoring after visibility change', () => { service.reportViewToServer_ = sandbox.spy(); - service.scheduleView_(); + service.scheduleView_(/* timeToView */ 2000); // 1. First attempt fails due to document becoming invisible. const p1 = service.reportViewPromise_; @@ -959,7 +1036,7 @@ describe('AccessService pingback', () => { it('should broadcast "viewed" signal to other documents', () => { service.reportViewToServer_ = sandbox.stub().returns(Promise.resolve()); const broadcastStub = sandbox.stub(service.viewer_, 'broadcast'); - const p = service.reportWhenViewed_(); + const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { clock.tick(2001); return p; @@ -1015,7 +1092,6 @@ describe('AccessService login', () => { }; analyticsMock = sandbox.mock(analytics); service.analyticsPromise_ = {then: callback => callback(analytics)}; - service.isAnalyticsExperimentOn_ = true; service.openLoginDialog_ = () => {}; serviceMock = sandbox.mock(service); @@ -1133,7 +1209,9 @@ describe('AccessService login', () => { }); it('should succeed login with success=true', () => { - service.runAuthorization_ = sandbox.spy(); + const authorizationStub = sandbox.stub(service, 'runAuthorization_', + () => Promise.resolve()); + const viewStub = sandbox.stub(service, 'scheduleView_'); const broadcastStub = sandbox.stub(service.viewer_, 'broadcast'); serviceMock.expects('openLoginDialog_') .withExactArgs('https://acme.com/l?rid=R') @@ -1147,7 +1225,11 @@ describe('AccessService login', () => { .once(); return service.login('').then(() => { expect(service.loginPromise_).to.not.exist; - expect(service.runAuthorization_.callCount).to.equal(1); + expect(authorizationStub.callCount).to.equal(1); + expect(authorizationStub.calledWithExactly( + /* disableFallback */ true)).to.be.true; + expect(viewStub.callCount).to.equal(1); + expect(viewStub.calledWithExactly(/* timeToView */ 0)).to.be.true; expect(broadcastStub.callCount).to.equal(1); expect(broadcastStub.firstCall.args[0]).to.deep.equal({ 'type': 'amp-access-reauthorize', @@ -1214,7 +1296,8 @@ describe('AccessService login', () => { 'login1': 'https://acme.com/l1?rid=R', 'login2': 'https://acme.com/l2?rid=R', }; - service.runAuthorization_ = sandbox.spy(); + const authorizationStub = sandbox.stub(service, 'runAuthorization_', + () => Promise.resolve()); const broadcastStub = sandbox.stub(service.viewer_, 'broadcast'); serviceMock.expects('openLoginDialog_') .withExactArgs('https://acme.com/l2?rid=R') @@ -1234,7 +1317,7 @@ describe('AccessService login', () => { .once(); return service.login('login2').then(() => { expect(service.loginPromise_).to.not.exist; - expect(service.runAuthorization_.callCount).to.equal(1); + expect(authorizationStub.callCount).to.equal(1); expect(broadcastStub.callCount).to.equal(1); expect(broadcastStub.firstCall.args[0]).to.deep.equal({ 'type': 'amp-access-reauthorize', @@ -1300,7 +1383,6 @@ describe('AccessService analytics', () => { service = new AccessService(window); service.enabled_ = true; - service.isAnalyticsExperimentOn_ = true; service.getReaderId_ = () => { return Promise.resolve('reader1'); }; @@ -1315,12 +1397,6 @@ describe('AccessService analytics', () => { sandbox = null; }); - it('should return null without experiment', () => { - service.isAnalyticsExperimentOn_ = false; - expect(service.getAccessReaderId()).to.be.null; - expect(service.getAuthdataField('views')).to.be.null; - }); - it('should return null when not enabled', () => { service.enabled_ = false; expect(service.getAccessReaderId()).to.be.null; @@ -1334,15 +1410,56 @@ describe('AccessService analytics', () => { }); it('should return authdata', () => { - expect(service.getAuthdataField('views')).to.equal(3); - expect(service.getAuthdataField('child.type')).to.equal('premium'); - expect(service.getAuthdataField('other')).to.be.null; - expect(service.getAuthdataField('child.other')).to.be.null; + service.firstAuthorizationResolver_(); + return Promise.all([ + service.getAuthdataField('views'), + service.getAuthdataField('child.type'), + service.getAuthdataField('other'), + service.getAuthdataField('child.other'), + ]).then(res => { + expect(res[0]).to.equal(3); + expect(res[1]).to.equal('premium'); + expect(res[2]).to.be.null; + expect(res[3]).to.be.null; + }); }); - it('should return null before authdata initialized', () => { - service.setAuthResponse_(null); - expect(service.getAuthdataField('views')).to.be.null; + it('should wait the first authorization for authdata', () => { + let viewsValue; + const promise = service.getAuthdataField('views').then(res => { + viewsValue = res; + }); + return Promise.resolve().then(() => { + expect(viewsValue).to.be.undefined; + // Resolve the authorization. + service.firstAuthorizationResolver_(); + return promise; + }).then(() => { + expect(viewsValue).to.equal(3); + }); + }); + + it('should wait the latest authorization for authdata if started', () => { + let resolver; + service.lastAuthorizationPromise_ = new Promise(resolve => { + resolver = resolve; + }); + let viewsValue; + const promise = service.getAuthdataField('views').then(res => { + viewsValue = res; + }); + return Promise.resolve().then(() => { + expect(viewsValue).to.be.undefined; + // Resolve the first authorization. + service.firstAuthorizationResolver_(); + }).then(() => { + expect(viewsValue).to.be.undefined; + // Resolve the second authorization. + resolver(); + return promise; + }).then(() => { + expect(viewsValue).to.equal(3); + }); }); }); @@ -1405,6 +1522,9 @@ describe('AccessService type=other', () => { expect(document.documentElement).not.to.have.class('amp-access-error'); expect(service.firstAuthorizationPromise_).to.exist; return service.firstAuthorizationPromise_; + }).then(() => { + expect(service.lastAuthorizationPromise_).to.equal( + service.firstAuthorizationPromise_); }); }); diff --git a/extensions/amp-access/0.1/test/test-amp-login-done-dialog.js b/extensions/amp-access/0.1/test/test-amp-login-done-dialog.js index 2de7d33612b8..772c7fe4641e 100644 --- a/extensions/amp-access/0.1/test/test-amp-login-done-dialog.js +++ b/extensions/amp-access/0.1/test/test-amp-login-done-dialog.js @@ -268,15 +268,34 @@ describe('LoginDoneDialog', () => { expect(dialog.postbackError_.callCount).to.equal(1); }); - it('should configure error mode', () => { + it('should configure error mode for "postback"', () => { dialog.postbackError_(new Error()); - expect(windowApi.document.documentElement).to.have.class( - 'amp-postback-error'); + expect(windowApi.document.documentElement) + .to.have.class('amp-error'); + expect(windowApi.document.documentElement.getAttribute('data-error')) + .to.equal('postback'); expect(closeButton.onclick).to.exist; windowMock.expects('close').once(); closeButton.onclick(); }); + + it('should configure error mode for "close"', () => { + dialog.postbackError_(new Error()); + + expect(windowApi.document.documentElement) + .to.have.class('amp-error'); + expect(windowApi.document.documentElement.getAttribute('data-error')) + .to.equal('postback'); + windowMock.expects('close').once(); + closeButton.onclick(); + + clock.tick(3000); + expect(windowApi.document.documentElement) + .to.have.class('amp-error'); + expect(windowApi.document.documentElement.getAttribute('data-error')) + .to.equal('close'); + }); }); }); diff --git a/extensions/amp-access/0.1/test/test-login-dialog.js b/extensions/amp-access/0.1/test/test-login-dialog.js index ea63d655676e..49b732313de9 100644 --- a/extensions/amp-access/0.1/test/test-login-dialog.js +++ b/extensions/amp-access/0.1/test/test-login-dialog.js @@ -22,10 +22,130 @@ const RETURN_URL_ESC = encodeURIComponent('http://localhost:8000/extensions' + encodeURIComponent('http://localhost:8000/test-login-dialog')); -describe('LoginDialog', () => { +describe('ViewerLoginDialog', () => { + + let sandbox; + let viewer; + let windowApi; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + + viewer = { + getParam: param => { + if (param == 'dialog') { + return '1'; + } + return null; + }, + sendMessage: () => {}, + }; + + windowApi = { + services: { + 'viewer': {obj: viewer}, + }, + screen: {width: 1000, height: 1000}, + open: () => { + throw new Error('Not allowed'); + }, + addEventListener: () => { + throw new Error('Not allowed'); + }, + setTimeout: () => { + throw new Error('Not allowed'); + }, + setInterval: () => { + throw new Error('Not allowed'); + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + sandbox = null; + }); + + it('should delegate to viewer with url', () => { + const stub = sandbox.stub(viewer, 'sendMessage', + () => Promise.resolve('#success=yes')); + return openLoginDialog(windowApi, 'http://acme.com/login').then(res => { + expect(res).to.equal('#success=yes'); + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args[0]).to.equal('openDialog'); + expect(stub.firstCall.args[1]).to.deep.equal({ + 'url': 'http://acme.com/login?return=RETURN_URL', + }); + expect(stub.firstCall.args[2]).to.be.true; + }); + }); + + it('should delegate to viewer with url promise', () => { + const stub = sandbox.stub(viewer, 'sendMessage', + () => Promise.resolve('#success=yes')); + const urlPromise = Promise.resolve('http://acme.com/login'); + return openLoginDialog(windowApi, urlPromise).then(res => { + expect(res).to.equal('#success=yes'); + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args[0]).to.equal('openDialog'); + expect(stub.firstCall.args[1]).to.deep.equal({ + 'url': 'http://acme.com/login?return=RETURN_URL', + }); + expect(stub.firstCall.args[2]).to.be.true; + }); + }); + + it('should fail when url promise fails', () => { + sandbox.stub(viewer, 'sendMessage', + () => Promise.resolve('#success=yes')); + const urlPromise = Promise.reject('expected'); + return openLoginDialog(windowApi, urlPromise).then(() => { + throw new Error('must not be here'); + }, reason => { + expect(reason).to.equal('expected'); + }); + }); + + it('should fail when viewer fails', () => { + sandbox.stub(viewer, 'sendMessage', + () => Promise.reject('expected')); + return openLoginDialog(windowApi, 'http://acme.com/login').then(() => { + throw new Error('must not be here'); + }, reason => { + expect(reason).to.equal('expected'); + }); + }); + + it('should have correct URL with other parameters', () => { + const stub = sandbox.stub(viewer, 'sendMessage', + () => Promise.resolve('#success=yes')); + const url = 'http://acme.com/login?a=b'; + return openLoginDialog(windowApi, url).then(() => { + expect(stub.firstCall.args[1]).to.deep.equal({ + 'url': 'http://acme.com/login?a=b&return=RETURN_URL', + }); + }); + }); + + it('should allow alternative form of return URL', () => { + const stub = sandbox.stub(viewer, 'sendMessage', + () => Promise.resolve('#success=yes')); + const url = 'http://acme.com/login?a=b&ret1=RETURN_URL'; + return openLoginDialog(windowApi, url).then(() => { + expect(stub.firstCall.args[1]).to.deep.equal({ + 'url': 'http://acme.com/login?a=b&ret1=RETURN_URL', + }); + }); + }); +}); + + +describe('WebLoginDialog', () => { let sandbox; let clock; + let viewer; let windowApi; let windowMock; let dialog; @@ -38,7 +158,13 @@ describe('LoginDialog', () => { clock = sandbox.useFakeTimers(); messageListener = undefined; + viewer = { + getParam: () => null, + }; windowApi = { + services: { + 'viewer': {obj: viewer}, + }, open: () => {}, location: { protocol: 'http:', diff --git a/extensions/amp-access/amp-access-analytics.md b/extensions/amp-access/amp-access-analytics.md index 3b8030754e8b..ae55a6799b03 100644 --- a/extensions/amp-access/amp-access-analytics.md +++ b/extensions/amp-access/amp-access-analytics.md @@ -16,11 +16,6 @@ limitations under the License. # AMP Access and Analytics -Experiment: "amp-access-analytics" should be enabled via https://cdn.ampproject.org/experiments.html or -`AMP.toggleExperiment('amp-access-analytics')`. See [Experiments Guide](../../tools/experiments/README.md). - -An integration with *amp-analytics* is under development and can be tracked on [Issue #1556](https://github.com/ampproject/amphtml/issues/1556). This document will be updated when more details on the integration are available. - ## Access analytics triggers Access service issues events for major states in the access flow. These events can be reported via an analytics package using triggers. diff --git a/extensions/amp-access/amp-access.md b/extensions/amp-access/amp-access.md index 73b4fe6aca07..72120890736b 100644 --- a/extensions/amp-access/amp-access.md +++ b/extensions/amp-access/amp-access.md @@ -37,28 +37,6 @@ limitations under the License. -The following lists validation errors specific to the `amp-access` tag -(see also `amp-access` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - -
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-access extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when the amp-access extension .json script is missing the mandatory attribute, type.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the attribute, type, is any value other than the required value: application/json. Error also thrown when the src attribute for the script tag is invalid. The value must be "https://cdn.ampproject.org/v0/amp-access-0.1.js".
- ## Solution The proposed solution gives control to the Publisher over the following decisions and flows: @@ -111,7 +89,7 @@ Authorization is an endpoint provided by the publisher and called by AMP Runtime ### Pingback Endpoint -Pingback is an endpoint provided by the publisher and called by AMP Runtime or Google AMP Cache. It is a credentialed CORS endpoint. AMP Runtime calls this endpoint automatically when the Reader has started viewing the document. On of the main goals of the Pingback is for the Publisher to update metering information. +Pingback is an endpoint provided by the publisher and called by AMP Runtime or Google AMP Cache. It is a credentialed CORS endpoint. AMP Runtime calls this endpoint automatically when the Reader has started viewing the document. This endpoint is also called after the Reader has successfully completed the Login Flow. On of the main goals of the Pingback is for the Publisher to update metering information. ### Login Page and Login Link @@ -119,7 +97,7 @@ Login Page is implemented and served by the Publisher and called by the AMP Runt Login Page is triggered when the Reader taps on the Login Link which can be placed by the Publisher anywhere in the document. -## Specification v0.4 +## Specification v0.5 ### Configuration @@ -322,6 +300,8 @@ Pingback URL can take any parameters as defined in the [Access URL Variables][7] Pingback does not produce a response - any response is ignored by AMP runtime. +Pingback endpoint is called when the reader has started viewing the document and after the Rser has successfully completed the Login Flow. + The publisher may choose to use the pingback as: - One of the main purposes for pingback is to count down meter when it is used. - As a credentialed CORS endpoint it may contain publisher cookies. Thus it can be used to map AMP Reader ID to the Publisher’s identity. @@ -374,6 +354,8 @@ RETURN_URL#success=true|false ``` Notice the use of a URL hash parameter “success”. The value is either “true” or “false” depending on whether the login succeeds or is abandoned. Ideally the Login Page, when possible, will send the signal in cases of both success or failure. +If the `success=true` signal is returned, the AMP runtime will repeat calls to Authorization and Pingback endpoints to update the document's state and report the "view" with the new access profile. + #### Login Link The Publisher may choose to place the Login Link anywhere in the content of the document. @@ -437,6 +419,7 @@ Both steps are covered by the AMP Access spec. The referrer can be injected into - Feb 11: Authorization request timeout in [Authorization Endpoint][4]. - Feb 15: [Configuration][8] and [Authorization Endpoint][4] now allow "authorizationFallbackResponse" property that can be used when authorization fails. - Feb 19: Corrected samples to remove `{}` from URL var substitutions. +- Mar 3: Resend pingback after login (v0.5). ## Appendix A: “amp-access” expression grammar @@ -490,3 +473,27 @@ This section will cover a detailed explanation of the design underlying the amp- [11]: #amp-access-and-cookies [12]: #metering [13]: #first-click-free + +## Validation errors + +The following lists validation errors specific to the `amp-access` tag +(see also `amp-access` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-access extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when the amp-access extension .json script is missing the mandatory attribute, type.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the attribute, type, is any value other than the required value: application/json. Error also thrown when the src attribute for the script tag is invalid. The value must be "https://cdn.ampproject.org/v0/amp-access-0.1.js".
diff --git a/extensions/amp-accordion/0.1/amp-accordion.js b/extensions/amp-accordion/0.1/amp-accordion.js index 25d58fcd79e5..e624aca9ca01 100644 --- a/extensions/amp-accordion/0.1/amp-accordion.js +++ b/extensions/amp-accordion/0.1/amp-accordion.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {CSS} from '../../../build/amp-accordion-0.1.css'; import {Layout} from '../../../src/layout'; import {assert} from '../../../src/asserts'; import {isExperimentOn} from '../../../src/experiments'; @@ -86,4 +87,4 @@ class AmpAccordion extends AMP.BaseElement { } } -AMP.registerElement('amp-accordion', AmpAccordion, $CSS$); +AMP.registerElement('amp-accordion', AmpAccordion, CSS); diff --git a/extensions/amp-accordion/0.1/test/test-amp-accordion.js b/extensions/amp-accordion/0.1/test/test-amp-accordion.js index 0dc3d4c08c67..4b334fd1c8a6 100644 --- a/extensions/amp-accordion/0.1/test/test-amp-accordion.js +++ b/extensions/amp-accordion/0.1/test/test-amp-accordion.js @@ -18,7 +18,7 @@ import {Timer} from '../../../../src/timer'; import {adopt} from '../../../../src/runtime'; import {createIframePromise} from '../../../../testing/iframe'; import {toggleExperiment} from '../../../../src/experiments'; -require('../../../../build/all/v0/amp-accordion-0.1.max'); +require('../amp-accordion'); adopt(window); diff --git a/extensions/amp-accordion/amp-accordion.md b/extensions/amp-accordion/amp-accordion.md index 76685d376186..608aa8dadb83 100644 --- a/extensions/amp-accordion/amp-accordion.md +++ b/extensions/amp-accordion/amp-accordion.md @@ -31,7 +31,7 @@ limitations under the License. Examples - None + amp-accordion.html diff --git a/extensions/amp-analytics/0.1/test/test-amp-analytics.js b/extensions/amp-analytics/0.1/test/test-amp-analytics.js index 214430348073..57dc98fd3b4a 100644 --- a/extensions/amp-analytics/0.1/test/test-amp-analytics.js +++ b/extensions/amp-analytics/0.1/test/test-amp-analytics.js @@ -15,10 +15,10 @@ */ import {ANALYTICS_CONFIG} from '../vendors'; -import {AmpAnalytics} from '../../../../build/all/v0/amp-analytics-0.1.max'; +import {AmpAnalytics} from '../amp-analytics'; import { installUserNotificationManager, -} from '../../../../build/all/v0/amp-user-notification-0.1.max'; +} from '../../../amp-user-notification/0.1/amp-user-notification'; import {adopt} from '../../../../src/runtime'; import {createIframePromise} from '../../../../testing/iframe'; import {getService} from '../../../../src/service'; diff --git a/extensions/amp-analytics/0.1/test/vendor-requests.json b/extensions/amp-analytics/0.1/test/vendor-requests.json index 45562725ca17..f1e5e3ce6db5 100644 --- a/extensions/amp-analytics/0.1/test/vendor-requests.json +++ b/extensions/amp-analytics/0.1/test/vendor-requests.json @@ -33,6 +33,10 @@ "pageview": "https://beacon.krxd.net/pixel.gif?source=amp&confid=$confid&_kpid=$pubid&_kcp_s=$site&_kcp_sc=$section&_kcp_ssc=$subsection&_kcp_d=_canonical_host_&_kpref_=_document_referrer_&_kua_kx_amp_client_id=_client_id_&_kua_kx_lang=_browser_language_&_kua_kx_tech_browser_language=_browser_language_&_kua_kx_tz=_timezone_&t_navigation_type=0&t_dns=_domain_lookup_time_&t_tcp=_tcp_connect_time_&t_http_request=_server_response_time_&t_http_response=_page_download_time_&t_content_ready=_content_load_time_&t_window_load=_page_load_time_&t_redirect=_redirect_time_", "event": "https://beacon.krxd.net/event.gif?source=amp&confid=$confid&_kpid=$pubid&_kcp_s=$site&_kcp_sc=$section&_kcp_ssc=$subsection&_kcp_d=_canonical_host_&_kpref_=_document_referrer_&_kua_kx_amp_client_id=_client_id_&_kua_kx_lang=_browser_language_&_kua_kx_tech_browser_language=_browser_language_&_kua_kx_tz=_timezone_&t_navigation_type=0&t_dns=_domain_lookup_time_&t_tcp=_tcp_connect_time_&t_http_request=_server_response_time_&t_http_response=_page_download_time_&t_content_ready=_content_load_time_&t_window_load=_page_load_time_&t_redirect=_redirect_time_&pageview=false" }, + "mediametrie": { + "host": "https://prof.estat.com/m/web", + "pageview": "https://prof.estat.com/m/web/$serial?c=$level1&dom=_ampdoc_url_&enc=_document_charset_&l3=$level3&l4=$level4&n=_random_&p=$level2&r=_document_referrer_&sch=_screen_height_&scw=_screen_width_&tn=amp&v=1&vh=_available_screen_height_&vw=_available_screen_width_" + }, "parsely": { "host": "https://srv.pixel.parsely.com", "basePrefix": "https://srv.pixel.parsely.com/plogger/?rand=_timestamp_&idsite=$apikey&url=_ampdoc_url_&urlref=_document_referrer_&screen=_screen_width_x_screen_height_%7C_available_screen_width_x_available_screen_height_%7C_screen_color_depth_&title=_title_&date=_timestamp_&id=_client_id_", diff --git a/extensions/amp-analytics/0.1/vendors.js b/extensions/amp-analytics/0.1/vendors.js index f68c47b77daf..8e88932ea4b3 100644 --- a/extensions/amp-analytics/0.1/vendors.js +++ b/extensions/amp-analytics/0.1/vendors.js @@ -61,6 +61,8 @@ export const ANALYTICS_CONFIG = { 'title': 'TITLE', 'totalEngagedTime': 'TOTAL_ENGAGED_TIME', 'viewer': 'VIEWER', + 'viewportHeight': 'VIEWPORT_HEIGHT', + 'viewportWidth': 'VIEWPORT_WIDTH', }, }, @@ -230,6 +232,33 @@ export const ANALYTICS_CONFIG = { }, }, + 'mediametrie': { + 'requests': { + 'host': 'https://prof.estat.com/m/web', + 'pageview': '${host}/${serial}?' + + 'c=${level1}' + + '&dom=${ampdocUrl}' + + '&enc=${documentCharset}' + + '&l3=${level3}' + + '&l4=${level4}' + + '&n=${random}' + + '&p=${level2}' + + '&r=${documentReferrer}' + + '&sch=${screenHeight}' + + '&scw=${screenWidth}' + + '&tn=amp' + + '&v=1' + + '&vh=${availableScreenHeight}' + + '&vw=${availableScreenWidth}', + }, + 'triggers': { + 'trackPageview': { + 'on': 'visible', + 'request': 'pageview', + }, + }, + }, + 'parsely': { 'requests': { 'host': 'https://srv.pixel.parsely.com', diff --git a/extensions/amp-analytics/amp-analytics.md b/extensions/amp-analytics/amp-analytics.md index 88547ba87f03..d3b52d7746d7 100644 --- a/extensions/amp-analytics/amp-analytics.md +++ b/extensions/amp-analytics/amp-analytics.md @@ -35,24 +35,6 @@ limitations under the License. -The following lists validation errors specific to the `amp-analytics` tag -(see also `amp-analytics` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - -
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-analytics extension .js script tag is missing or incorrect.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the src attribute for the script tag is invalid. The value must be "https://cdn.ampproject.org/v0/amp-analytics-0.1.js".
- ## Behavior The `` element is used to measure activity on an AMP document. The details concerning what is measured and how that data is sent to an analytics server is specified in a JSON configuration object. It comes pre-configured to support many [analytics vendors](#analytics-vendors) out of the box. @@ -117,7 +99,7 @@ Adds support for AT Internet. More details for adding AT Internet support can be Type attribute value: `chartbeat` -Adds support for Chartbeat. More details for adding Chartbeat support can be found at [support.chartbeat.com](http://support.chartbeat.com/docs/). +Adds support for Chartbeat. More details for adding Chartbeat support can be found at [support.chartbeat.com](http://support.chartbeat.com/docs/integrations.html#amp). ### comScore @@ -152,6 +134,12 @@ Type attribute value: `krux` Adds support for Krux. Configuration details can be found at [help.krux.com](https://konsole.zendesk.com/hc/en-us/articles/216596608). +### Médiamétrie + +Type attribute value: `mediametrie` + +Adds support for Médiamétrie tracking pages. Requires defining *var* `serial`. Vars `level1` to `level4` are optional. + ### Parsely Type attribute value: `parsely` @@ -170,7 +158,7 @@ Type attribute value: `simplereach` Adds support for SimpleReach. Configuration details can be found at [simplereach.com/docs](http://docs.simplereach.com/dev-guide/implementation/google-amp-implementation) -##### Webtrekk +### Webtrekk Type attribute value: `webtrekk` @@ -346,16 +334,48 @@ If more than one of the above transport methods are enabled, the precedence is ` In the example below, `beacon` and `xhrpost` are set to `false`, so they will not be used even though they have higher precedence than `image`. `image` would be set `true` by default, but it is explicitly declared here. If the client's user agent supports the `image` method, then it will be used; otherwise, no request would be sent. ```javascript -'transport': { - 'beacon': false, - 'xhrpost': false, - 'image': true +"transport": { + "beacon": false, + "xhrpost": false, + "image": true +} +``` + + +### Extra URL Params + +The `extraUrlParams` attribute specifies additional parameters to append to the query string of a request URL via the usual "&foo=baz" convention. + +Here's an example that would append `&a=1&b=2&c=3` to a request: + +```javascript +"extraUrlParams": { + "a": "1", + "b": "2", + "c": "3" } ``` +The `extraUrlParamsReplaceMap` attribute specifies a map of keys and values that act as parameters to String.replace() to preprocess keys in the extraUrlParams configuration. For example, if an `extraUrlParams` configuration defines `"page.title": "The title of my page"` and the `extraUrlParamsReplaceMap` defines `"page.": "_p_"`, then `&_p_title=The%20title%20of%20my%20page%20` will be appended to the request. -# Extra URL Params +`extraUrlParamsReplaceMap` is not required to use `extraUrlParams`. If `extraUrlParamsReplaceMap` is not defined, then no string substitution will happens and the strings defined in `extraUrlParams` are used as-is. -The `extraUrlParams` attribute specifies additional parameters to append to the query string of the url via the usual "&foo=baz" convention. +## Validation errors -The `extraUrlParamsReplaceMap` attribute specifies a map of keys and values that act as parameters to String.replace() to preprocess keys in the extraUrlParams map. +The following lists validation errors specific to the `amp-analytics` tag +(see also `amp-analytics` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-analytics extension .js script tag is missing or incorrect.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the src attribute for the script tag is invalid. The value must be "https://cdn.ampproject.org/v0/amp-analytics-0.1.js".
diff --git a/extensions/amp-analytics/analytics-vars.md b/extensions/amp-analytics/analytics-vars.md index d5d5450c89e4..b740b6ad00e7 100644 --- a/extensions/amp-analytics/analytics-vars.md +++ b/extensions/amp-analytics/analytics-vars.md @@ -271,7 +271,7 @@ Example usage: `${queryParam(foo)}` - if foo is available its associated value w ## requestCount -Provides the number of requests sent out from a particular `amp-analytics` tag. This value can be used to reconstruct the sequence in which requests were sent from a tag. The value starts from 0 and increases monotonically. Note that there may be a gap in requestCount numbers if the request sending fails due to network issues. +Provides the number of requests sent out from a particular `amp-analytics` tag. This value can be used to reconstruct the sequence in which requests were sent from a tag. The value starts from 1 and increases monotonically. Note that there may be a gap in requestCount numbers if the request sending fails due to network issues. Example value: `6` diff --git a/extensions/amp-anim/amp-anim.md b/extensions/amp-anim/amp-anim.md index d3aec4a532e2..cc90bda156a5 100644 --- a/extensions/amp-anim/amp-anim.md +++ b/extensions/amp-anim/amp-anim.md @@ -31,37 +31,7 @@ limitations under the License. Examples - amp-anim.html
everything.amp.html - - - -The following lists validation errors specific to the `amp-anim` tag -(see also `amp-anim` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-anim extension .js script tag is missing or incorrect.
The tag 'example1' is missing a mandatory attribute - pick one of example2.Error thrown when neither src or srcset is included. One of these attributes is mandatory.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.amp-anim.html
everything.amp.html
@@ -106,3 +76,34 @@ amp-anim { background-color: grey; } ``` +## Validation errors + +The following lists validation errors specific to the `amp-anim` tag +(see also `amp-anim` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-anim extension .js script tag is missing or incorrect.
The tag 'example1' is missing a mandatory attribute - pick one of example2.Error thrown when neither src or srcset is included. One of these attributes is mandatory.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-audio/amp-audio.md b/extensions/amp-audio/amp-audio.md index ff51138f810d..8cb6c9f6e940 100644 --- a/extensions/amp-audio/amp-audio.md +++ b/extensions/amp-audio/amp-audio.md @@ -31,10 +31,52 @@ limitations under the License. Examples - everything.amp.html + amp-audio.html
everything.amp.html +## Behavior + +The `amp-audio` component loads the audio resource specified by its `src` attribute at a time determined by the runtime. It can be controlled in much the same way as a standard HTML5 `audio` tag. +Like all embedded external resources in an AMP file, the audio is "lazily" loaded, only when the `amp-audio` element is in or near the viewport. + +The `amp-audio` component HTML accepts up to three unique types of HTML nodes as children - `source` tags, a placeholder for before the audio starts, and a fallback if the browser doesn’t support HTML5 audio. + +`source` tag children can be used in the same way as the standard `audio` tag, to specify different source files to play. + +One or zero immediate child nodes can have the `placeholder` attribute. If present, this node and its children form a placeholder that will display instead of the audio. A click or tap anywhere inside of the `amp-audio` container will replace the placeholder with the audio itself. + +One or zero immediate child nodes can have the `fallback` attribute. If present, this node and its children form the content that will be displayed if HTML5 audio is not supported on the user’s browser. + +For example: +```html + +
+

Your browser doesn’t support HTML5 audio

+
+ + +
+``` + +## Attributes + +**autoplay** + +The `autoplay` attribute allows the author to specify when - if ever - the animated image will autoplay. + +The presence of the attribute alone implies that the animated image will always autoplay. The author may specify values to limit when the animations will autoplay. Allowable values are `desktop`, `tablet`, or `mobile`, with multiple values separated by a space. The runtime makes a best-guess approximation to the device type to apply this value. + +**loop** + +If present, will automatically loop the audio back to the start upon reaching the end. + +**muted** + +If present, will mute the audio by default. + +## Validation errors + The following lists validation errors specific to the `amp-audio` tag (see also `amp-audio` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -77,43 +119,3 @@ May need to add something to this table based on technical review. Error thrown when specified layout is set to RESPONSIVE, FILL, or CONTAINER; these layout types aren't supported. - -## Behavior - -The `amp-audio` component loads the audio resource specified by its `src` attribute at a time determined by the runtime. It can be controlled in much the same way as a standard HTML5 `audio` tag. -Like all embedded external resources in an AMP file, the audio is "lazily" loaded, only when the `amp-audio` element is in or near the viewport. - -The `amp-audio` component HTML accepts up to three unique types of HTML nodes as children - `source` tags, a placeholder for before the audio starts, and a fallback if the browser doesn’t support HTML5 audio. - -`source` tag children can be used in the same way as the standard `audio` tag, to specify different source files to play. - -One or zero immediate child nodes can have the `placeholder` attribute. If present, this node and its children form a placeholder that will display instead of the audio. A click or tap anywhere inside of the `amp-audio` container will replace the placeholder with the audio itself. - -One or zero immediate child nodes can have the `fallback` attribute. If present, this node and its children form the content that will be displayed if HTML5 audio is not supported on the user’s browser. - -For example: -```html - -
-

Your browser doesn’t support HTML5 audio

-
- - -
-``` - -## Attributes - -**autoplay** - -The `autoplay` attribute allows the author to specify when - if ever - the animated image will autoplay. - -The presence of the attribute alone implies that the animated image will always autoplay. The author may specify values to limit when the animations will autoplay. Allowable values are `desktop`, `tablet`, or `mobile`, with multiple values separated by a space. The runtime makes a best-guess approximation to the device type to apply this value. - -**loop** - -If present, will automatically loop the audio back to the start upon reaching the end. - -**muted** - -If present, will mute the audio by default. diff --git a/extensions/amp-brightcove/amp-brightcove.md b/extensions/amp-brightcove/amp-brightcove.md index 87e9d350db0e..14c648e3e4b6 100644 --- a/extensions/amp-brightcove/amp-brightcove.md +++ b/extensions/amp-brightcove/amp-brightcove.md @@ -31,37 +31,7 @@ limitations under the License. Examples - amp-brightcove.html
brightcove.amp.html - - - -The following lists validation errors specific to the `amp-brightcove` tag -(see also `amp-brightcove` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-brightcove extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-account attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.amp-brightcove.html
brightcove.amp.html
@@ -124,3 +94,35 @@ Keys and values will be URI encoded. Keys will be camel cased. This script should be added to the configuration of Brightcove Players used with this component. This allows the AMP document to pause the player. Only the script need be added, no plugin name or JSON are needed. * http://players.brightcove.net/906043040001/plugins/postmessage_pause.js + +## Validation errors + +The following lists validation errors specific to the `amp-brightcove` tag +(see also `amp-brightcove` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-brightcove extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-account attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-carousel/0.1/amp-carousel.js b/extensions/amp-carousel/0.1/amp-carousel.js index dbef9b47ac05..51c55267bbfa 100644 --- a/extensions/amp-carousel/0.1/amp-carousel.js +++ b/extensions/amp-carousel/0.1/amp-carousel.js @@ -16,7 +16,7 @@ import {AmpSlides} from './slides'; import {AmpCarousel} from './carousel'; - +import {CSS} from '../../../build/amp-carousel-0.1.css'; class CarouselSelector { @@ -29,4 +29,4 @@ class CarouselSelector { } } -AMP.registerElement('amp-carousel', CarouselSelector, $CSS$); +AMP.registerElement('amp-carousel', CarouselSelector, CSS); diff --git a/extensions/amp-carousel/0.1/base-carousel.js b/extensions/amp-carousel/0.1/base-carousel.js index de45d91db2dc..0f48a532d852 100644 --- a/extensions/amp-carousel/0.1/base-carousel.js +++ b/extensions/amp-carousel/0.1/base-carousel.js @@ -47,9 +47,7 @@ export class BaseCarousel extends AMP.BaseElement { // a way to be overridden. this.prevButton_.setAttribute('aria-label', 'previous'); this.prevButton_.onclick = () => { - if (!this.prevButton_.classList.contains('amp-disabled')) { - this.go(-1, true); - } + this.interactionPrev(); }; this.element.appendChild(this.prevButton_); @@ -59,9 +57,7 @@ export class BaseCarousel extends AMP.BaseElement { this.nextButton_.setAttribute('role', 'button'); this.nextButton_.setAttribute('aria-label', 'next'); this.nextButton_.onclick = () => { - if (!this.nextButton_.classList.contains('amp-disabled')) { - this.go(1, true); - } + this.interactionNext(); }; this.element.appendChild(this.nextButton_); } @@ -91,7 +87,8 @@ export class BaseCarousel extends AMP.BaseElement { } /** - * Calls `goCallback` and `setControlsState` for transition behavior. + * Calls `goCallback` and any additional work needed to proceed to next + * desired direction. * @param {number} dir -1 or 1 * @param {boolean} animate */ @@ -162,4 +159,22 @@ export class BaseCarousel extends AMP.BaseElement { hasNext() { // Subclasses may override. } + + /** + * Called on user interaction to proceed to the next item/position. + */ + interactionNext() { + if (!this.nextButton_.classList.contains('amp-disabled')) { + this.go(1, true); + } + } + + /** + * Called on user interaction to proceed to the previous item/position. + */ + interactionPrev() { + if (!this.prevButton_.classList.contains('amp-disabled')) { + this.go(-1, true); + } + } } diff --git a/extensions/amp-carousel/0.1/slides.js b/extensions/amp-carousel/0.1/slides.js index 2364610341a4..fdc4d830f1ac 100644 --- a/extensions/amp-carousel/0.1/slides.js +++ b/extensions/amp-carousel/0.1/slides.js @@ -37,7 +37,7 @@ export class AmpSlides extends BaseCarousel { /** @private @const {boolean} */ this.isLooping_ = this.element.hasAttribute('loop'); - /** @private @const {boolean} */ + /** @private {boolean} */ this.isAutoplayRequested_ = this.element.hasAttribute('autoplay'); /** @private @const {number} */ @@ -293,6 +293,10 @@ export class AmpSlides extends BaseCarousel { * @private */ onSwipeStart_(unusedSwipe) { + // cancel any current and future autoplay request + this.tryCancelAutoplayTimeout_(); + this.isAutoplayRequested_ = false; + const currentSlide = this.curSlide_(); const containerWidth = this.element./*OK*/offsetWidth; let minDelta = 0; @@ -385,15 +389,15 @@ export class AmpSlides extends BaseCarousel { if (s.currentIndex != this.currentIndex_) { return; } - const oldSlide = this.slides_[this.currentIndex_]; + const oldSlide = this.curSlide_(); if (newPos > 0.5) { s.nextTr(1); this.currentIndex_ = s.nextIndex; - this.commitSwitch_(oldSlide, this.slides_[this.currentIndex_]); + this.commitSwitch_(oldSlide, this.curSlide_()); } else if (newPos < -0.5) { s.prevTr(1); this.currentIndex_ = s.prevIndex; - this.commitSwitch_(oldSlide, this.slides_[this.currentIndex_]); + this.commitSwitch_(oldSlide, this.curSlide_()); } else { s.nextTr(0); s.prevTr(0); @@ -417,6 +421,22 @@ export class AmpSlides extends BaseCarousel { return this.currentIndex_ < this.slides_.length - 1; } + /** @override */ + interactionNext() { + if (!this.nextButton_.classList.contains('amp-disabled')) { + this.isAutoplayRequested_ = false; + this.go(1, true); + } + } + + /** @override */ + interactionPrev() { + if (!this.prevButton_.classList.contains('amp-disabled')) { + this.isAutoplayRequested_ = false; + this.go(-1, true); + } + } + /** * Gets the relative index using a step value that loops around even if the * step goes out of bounds of the current length. (less than zero, greater diff --git a/extensions/amp-carousel/0.1/test/test-slides.js b/extensions/amp-carousel/0.1/test/test-slides.js index 0cd1754432c8..775f514dc9d9 100644 --- a/extensions/amp-carousel/0.1/test/test-slides.js +++ b/extensions/amp-carousel/0.1/test/test-slides.js @@ -492,6 +492,7 @@ describe('Slides functional', () => { let setupAutoplaySpy; let goSpy; let isInViewportStub; + let tryCancelAutoplayTimeoutSpy; function autoplaySetup(delay = '', inViewport = true) { clock = sandbox.useFakeTimers(); @@ -504,6 +505,8 @@ describe('Slides functional', () => { setupAutoplaySpy = sandbox.spy(AmpSlides.prototype, 'setupAutoplay_'); tryAutoplaySpy = sandbox.spy(AmpSlides.prototype, 'tryAutoplay_'); goSpy = sandbox.spy(AmpSlides.prototype, 'go'); + tryCancelAutoplayTimeoutSpy = sandbox + .spy(AmpSlides.prototype, 'tryCancelAutoplayTimeout_'); setupSlides(); setupSpies(); setupInViewport(inViewport); @@ -562,11 +565,17 @@ describe('Slides functional', () => { expect(tryAutoplaySpy.callCount).to.equal(2); expect(goSpy.callCount).to.equal(1); + + clock.tick(5000); + + expect(tryAutoplaySpy.callCount).to.equal(3); + expect(goSpy.callCount).to.equal(2); }); it('should call `go` after 2000ms (set by user)', () => { autoplaySetup(2000); + expect(slides.isAutoplayRequested_).to.be.true; expect(tryAutoplaySpy.callCount).to.equal(0); expect(goSpy.callCount).to.equal(0); @@ -579,6 +588,71 @@ describe('Slides functional', () => { expect(tryAutoplaySpy.callCount).to.equal(2); expect(goSpy.callCount).to.equal(1); + expect(slides.isAutoplayRequested_).to.be.true; + }); + + it('should cancel autoplay on swipe start', () => { + autoplaySetup(2000); + + expect(slides.isAutoplayRequested_).to.be.true; + expect(tryAutoplaySpy.callCount).to.equal(0); + expect(goSpy.callCount).to.equal(0); + expect(tryCancelAutoplayTimeoutSpy.callCount).to.equal(0); + + slides.viewportCallback(true); + + expect(tryCancelAutoplayTimeoutSpy.callCount).to.equal(1); + expect(tryAutoplaySpy.callCount).to.equal(1); + expect(goSpy.callCount).to.equal(0); + + // user interaction + slides.onSwipeStart_({}); + + expect(slides.isAutoplayRequested_).to.be.false; + expect(tryCancelAutoplayTimeoutSpy.callCount).to.equal(2); + + clock.tick(2000); + + expect(tryAutoplaySpy.callCount).to.equal(1); + expect(goSpy.callCount).to.equal(0); + }); + + it('should cancel autoplay on user interaction', () => { + autoplaySetup(2000); + + expect(slides.isAutoplayRequested_).to.be.true; + expect(tryAutoplaySpy.callCount).to.equal(0); + expect(goSpy.callCount).to.equal(0); + + slides.viewportCallback(true); + + expect(tryAutoplaySpy.callCount).to.equal(1); + expect(goSpy.callCount).to.equal(0); + + clock.tick(2000); + + // autoplay call + expect(tryAutoplaySpy.callCount).to.equal(2); + expect(goSpy.callCount).to.equal(1); + + clock.tick(2000); + + expect(tryAutoplaySpy.callCount).to.equal(3); + expect(goSpy.callCount).to.equal(2); + + // user interaction + slides.interactionNext(1, false); + expect(slides.isAutoplayRequested_).to.be.false; + + clock.tick(2000); + + expect(tryAutoplaySpy.callCount).to.equal(4); + expect(goSpy.callCount).to.equal(3); + + clock.tick(2000); + + expect(tryAutoplaySpy.callCount).to.equal(4); + expect(goSpy.callCount).to.equal(3); }); }); diff --git a/extensions/amp-carousel/amp-carousel.md b/extensions/amp-carousel/amp-carousel.md index a526671dfc06..cfed096f95d8 100644 --- a/extensions/amp-carousel/amp-carousel.md +++ b/extensions/amp-carousel/amp-carousel.md @@ -31,33 +31,7 @@ limitations under the License. Examples - amp-carousel.html
everything.amp.html - - - -The following lists validation errors specific to the `amp-carousel` tag -(see also `amp-carousel` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-carousel extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
Layout not supported for: responsiveError thrown when layout set to RESPONSIVE and the type attribute is set to (or defaults to) carousel.amp-carousel.html
image_galleries_with_amp-carousel.html
everything.amp.html
@@ -142,3 +116,30 @@ You may override this with your own svg or image like so: background-color: rgba(255, 0, 0, .5); } ``` +## Validation errors + +The following lists validation errors specific to the `amp-carousel` tag +(see also `amp-carousel` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-carousel extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
Layout not supported for: responsiveError thrown when layout set to RESPONSIVE and the type attribute is set to (or defaults to) carousel.
diff --git a/extensions/amp-dailymotion/amp-dailymotion.md b/extensions/amp-dailymotion/amp-dailymotion.md index f09db4efc9c0..8dbf2f659ce5 100644 --- a/extensions/amp-dailymotion/amp-dailymotion.md +++ b/extensions/amp-dailymotion/amp-dailymotion.md @@ -31,41 +31,7 @@ limitations under the License. Examples - dailymotion.amp.html - - - -The following lists validation errors specific to the `amp-dailymotion` tag -(see also `amp-dailymotion` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-dailymotion extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-videoid attribute missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the data-videoid attribute is invalid.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types.amp-dailymotion.html
dailymotion.amp.html
@@ -143,3 +109,39 @@ Whether to show video information (title and owner) on the start screen. Value: `"true"` or `"false"` Default value: `"true"` + +## Validation errors + +The following lists validation errors specific to the `amp-dailymotion` tag +(see also `amp-dailymotion` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-dailymotion extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-videoid attribute missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the data-videoid attribute is invalid.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types.
diff --git a/extensions/amp-dynamic-css-classes/0.1/test/test-runtime-classes.js b/extensions/amp-dynamic-css-classes/0.1/test/test-runtime-classes.js index 6ea10d4c8891..035cc21f393b 100644 --- a/extensions/amp-dynamic-css-classes/0.1/test/test-runtime-classes.js +++ b/extensions/amp-dynamic-css-classes/0.1/test/test-runtime-classes.js @@ -65,7 +65,8 @@ describe('dynamic classes are inserted at runtime', () => { } describe('when experiment is disabled', () => { - beforeEach(() => { + beforeEach(function() { + this.timeout(5000); return setup(false); }); @@ -79,7 +80,8 @@ describe('dynamic classes are inserted at runtime', () => { }); describe('when experiment is enabled', () => { - beforeEach(() => { + beforeEach(function() { + this.timeout(5000); return setup(true); }); @@ -93,14 +95,16 @@ describe('dynamic classes are inserted at runtime', () => { }); describe('Normalizing Referrers', () => { - it('should normalize twitter shortlinks to twitter', () => { + it('should normalize twitter shortlinks to twitter', function() { + this.timeout(5000); return setup(true, '', tcoReferrer).then(() => { expect(documentElement).to.have.class('amp-referrer-com'); expect(documentElement).to.have.class('amp-referrer-twitter-com'); }); }); - it('should normalize pinterest on android', () => { + it('should normalize pinterest on android', function() { + this.timeout(5000); return setup(true, PinterestUA, '').then(() => { expect(documentElement).to.have.class('amp-referrer-com'); expect(documentElement).to.have.class('amp-referrer-pinterest-com'); @@ -109,7 +113,8 @@ describe('dynamic classes are inserted at runtime', () => { }); }); - it('should delay unhiding the body', () => { + it('should delay unhiding the body', function() { + this.timeout(5000); return createServedIframe(iframeSrc).then(fixture => { expect(fixture.doc.body).to.be.hidden; diff --git a/extensions/amp-facebook/0.1/amp-facebook.js b/extensions/amp-facebook/0.1/amp-facebook.js index c74dbcaba995..723826b0e7c0 100644 --- a/extensions/amp-facebook/0.1/amp-facebook.js +++ b/extensions/amp-facebook/0.1/amp-facebook.js @@ -26,7 +26,8 @@ class AmpFacebook extends AMP.BaseElement { preconnectCallback(onLayout) { this.preconnect.url('https://facebook.com', onLayout); // Hosts the facebook SDK. - this.preconnect.prefetch('https://connect.facebook.net/en_US/sdk.js'); + this.preconnect.prefetch( + 'https://connect.facebook.net/en_US/sdk.js', 'script'); prefetchBootstrap(this.getWin()); } diff --git a/extensions/amp-facebook/amp-facebook.md b/extensions/amp-facebook/amp-facebook.md index cac1671e5398..e82f19d4ee33 100644 --- a/extensions/amp-facebook/amp-facebook.md +++ b/extensions/amp-facebook/amp-facebook.md @@ -31,37 +31,7 @@ limitations under the License. Examples - amp-facebook.html
facebook.amp.html - - - -The following lists validation errors specific to the `amp-facebook` tag -(see also `amp-facebook` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-facebook extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-href attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.amp-facebook.html
facebook.amp.html
@@ -97,3 +67,35 @@ Either `post` or `video` (default: `post`). Both posts and videos can be embedded as a post. Setting `data-embed-as="video"` for Facebook videos only embed the player of the video ignoring the accompanying post card with it. This is recommended if you'd like a better aspect ratio management for the video to be responsive. Checkout the documentation for differences between [post embeds](https://developers.facebook.com/docs/plugins/embedded-posts) and [video embeds](https://developers.facebook.com/docs/plugins/embedded-video-player). + +## Validation errors + +The following lists validation errors specific to the `amp-facebook` tag +(see also `amp-facebook` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-facebook extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-href attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-fit-text/0.1/amp-fit-text.js b/extensions/amp-fit-text/0.1/amp-fit-text.js index e299928faff7..d8aa7dbb303d 100644 --- a/extensions/amp-fit-text/0.1/amp-fit-text.js +++ b/extensions/amp-fit-text/0.1/amp-fit-text.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {CSS} from '../../../build/amp-fit-text-0.1.css'; import {getLengthNumeral, isLayoutSizeDefined} from '../../../src/layout'; import * as st from '../../../src/style'; @@ -151,4 +152,4 @@ export function updateOverflow_(content, measurer, maxHeight, fontSize) { }; -AMP.registerElement('amp-fit-text', AmpFitText, $CSS$); +AMP.registerElement('amp-fit-text', AmpFitText, CSS); diff --git a/extensions/amp-fit-text/0.1/test/test-amp-fit-text.js b/extensions/amp-fit-text/0.1/test/test-amp-fit-text.js index 82711f3af69e..dd740dfeda95 100644 --- a/extensions/amp-fit-text/0.1/test/test-amp-fit-text.js +++ b/extensions/amp-fit-text/0.1/test/test-amp-fit-text.js @@ -16,11 +16,11 @@ import {Timer} from '../../../../src/timer'; import {createIframePromise} from '../../../../testing/iframe'; -require('../../../../build/all/v0/amp-fit-text-0.1.max'); +require('../amp-fit-text'); import { calculateFontSize_, updateOverflow_, -} from '../../../../build/all/v0/amp-fit-text-0.1.max'; +} from '../amp-fit-text'; import {adopt} from '../../../../src/runtime'; adopt(window); diff --git a/extensions/amp-fit-text/amp-fit-text.md b/extensions/amp-fit-text/amp-fit-text.md index db81fed10871..9126bb7f4ba6 100644 --- a/extensions/amp-fit-text/amp-fit-text.md +++ b/extensions/amp-fit-text/amp-fit-text.md @@ -31,33 +31,7 @@ limitations under the License. Examples - everything.amp.html - - - -The following lists validation errors specific to the `amp-fit-text` tag -(see also `amp-fit-text` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-fit-text extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.image_galleries_with_amp-carousel.html
everything.amp.html
@@ -101,3 +75,31 @@ The maximum font size as an integer that the `amp-fit-text` can use. The `amp-fit-text` component can be styled with standard CSS. In particular, it's possible to use `text-align`, `font-weight`, `color` and many other CSS properties with the main exception of `font-size`. + +## Validation errors + +The following lists validation errors specific to the `amp-fit-text` tag +(see also `amp-fit-text` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-fit-text extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-font/amp-font.md b/extensions/amp-font/amp-font.md index f213ffc682e0..a83c4e83f38d 100644 --- a/extensions/amp-font/amp-font.md +++ b/extensions/amp-font/amp-font.md @@ -35,36 +35,6 @@ limitations under the License. -The following lists validation errors specific to the `amp-font` tag -(see also `amp-font` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - - -
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-font extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when font-family attribute is missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the timeout attribute is not a positive integer.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.
- ## Behavior The `amp-font` extension should be used for controlling timeouts on font loading. @@ -127,3 +97,35 @@ CSS class that would be removed from the `document.documentElement` and `documen **font-weight, font-style, font-variant** The attributes above should all behave like they do on standard elements. + +## Validation errors + +The following lists validation errors specific to the `amp-font` tag +(see also `amp-font` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-font extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when font-family attribute is missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the timeout attribute is not a positive integer.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.
diff --git a/extensions/amp-iframe/0.1/amp-iframe.js b/extensions/amp-iframe/0.1/amp-iframe.js index 09d5cf9859f1..9a2b518528d8 100644 --- a/extensions/amp-iframe/0.1/amp-iframe.js +++ b/extensions/amp-iframe/0.1/amp-iframe.js @@ -204,7 +204,7 @@ export class AmpIframe extends AMP.BaseElement { /** * @override */ - getInsersectionElementLayoutBox() { + getIntersectionElementLayoutBox() { if (!this.iframeLayoutBox_) { this.measureIframeLayoutBox_(); } diff --git a/extensions/amp-iframe/amp-iframe.md b/extensions/amp-iframe/amp-iframe.md index a6a07e474e77..d5d451e25a84 100644 --- a/extensions/amp-iframe/amp-iframe.md +++ b/extensions/amp-iframe/amp-iframe.md @@ -31,53 +31,7 @@ limitations under the License. Examples - amp-iframe.html
everything.amp.html - - - -The following lists validation errors specific to the `amp-iframe` tag -(see also `amp-iframe` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-iframe extension .js script tag is missing or incorrect.
The tag 'example1' is missing a mandatory attribute - pick one of example2.Error thrown when neither src or srcdoc is included. One of these attributes is mandatory.
Missing URL for attribute 'example1' in tag 'example2'.Error thrown when src or srcdoc is missing it's URL.
Malformed URL 'example3' for attribute 'example1' in tag 'example2'.Error thrown when src or srcdoc URL is invalid.
Invalid URL protocol 'example3:' for attribute 'example1' in tag 'example2'.Error thrown src or srcdoc URL is http; https protocol required.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when scrolling attribute not auto, yes, or no. Error also thrown when frameborder not 0 or 1. Attribute value must be "" for allowfullscreen, allowtransparency, resizable.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.amp-iframe.html
everything.amp.html
@@ -221,3 +175,51 @@ We strongly recommend using [`amp-analytics`](../amp-analytics/amp-analytics.md) AMP only allows a single iframe, that is used for analytics and tracking purposes, per page. To conserve resources these iframes will be removed from the DOM 5 seconds after they loaded, which should be sufficient time to complete whatever work is needed to be done. Iframes are identified as tracking/analytics iframes if they appear to serve no direct user purpose such as being invisible or small. + +## Validation errors + +The following lists validation errors specific to the `amp-iframe` tag +(see also `amp-iframe` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-iframe extension .js script tag is missing or incorrect.
The tag 'example1' is missing a mandatory attribute - pick one of example2.Error thrown when neither src or srcdoc is included. One of these attributes is mandatory.
Missing URL for attribute 'example1' in tag 'example2'.Error thrown when src or srcdoc is missing it's URL.
Malformed URL 'example3' for attribute 'example1' in tag 'example2'.Error thrown when src or srcdoc URL is invalid.
Invalid URL protocol 'example3:' for attribute 'example1' in tag 'example2'.Error thrown src or srcdoc URL is http; https protocol required.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when scrolling attribute not auto, yes, or no. Error also thrown when frameborder not 0 or 1. Attribute value must be "" for allowfullscreen, allowtransparency, resizable.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-image-lightbox/0.1/amp-image-lightbox.js b/extensions/amp-image-lightbox/0.1/amp-image-lightbox.js index 37bfbab03a16..d4a55e64df61 100644 --- a/extensions/amp-image-lightbox/0.1/amp-image-lightbox.js +++ b/extensions/amp-image-lightbox/0.1/amp-image-lightbox.js @@ -15,6 +15,7 @@ */ import {Animation} from '../../../src/animation'; +import {CSS} from '../../../build/amp-image-lightbox-0.1.css'; import {Gestures} from '../../../src/gesture'; import { DoubletapRecognizer, @@ -999,4 +1000,4 @@ class AmpImageLightbox extends AMP.BaseElement { } } -AMP.registerElement('amp-image-lightbox', AmpImageLightbox, $CSS$); +AMP.registerElement('amp-image-lightbox', AmpImageLightbox, CSS); diff --git a/extensions/amp-image-lightbox/0.1/test/test-amp-image-lightbox.js b/extensions/amp-image-lightbox/0.1/test/test-amp-image-lightbox.js index f3b7bde44830..55eed6066c6f 100644 --- a/extensions/amp-image-lightbox/0.1/test/test-amp-image-lightbox.js +++ b/extensions/amp-image-lightbox/0.1/test/test-amp-image-lightbox.js @@ -16,10 +16,10 @@ import {Timer} from '../../../../src/timer'; import {createIframePromise} from '../../../../testing/iframe'; -require('../../../../build/all/v0/amp-image-lightbox-0.1.max'); +require('../amp-image-lightbox'); import { ImageViewer, -} from '../../../../build/all/v0/amp-image-lightbox-0.1.max'; +} from '../amp-image-lightbox'; import {adopt} from '../../../../src/runtime'; import {parseSrcset} from '../../../../src/srcset'; import * as sinon from 'sinon'; diff --git a/extensions/amp-image-lightbox/amp-image-lightbox.md b/extensions/amp-image-lightbox/amp-image-lightbox.md index 29de438d6ce8..53a3028aeb93 100644 --- a/extensions/amp-image-lightbox/amp-image-lightbox.md +++ b/extensions/amp-image-lightbox/amp-image-lightbox.md @@ -31,29 +31,7 @@ limitations under the License. Examples - amp-image-lightbox.html
everything.amp.html - - - -The following lists validation errors specific to the `amp-image-lightbox` tag -(see also `amp-image-lightbox` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-image-lightbox extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.amp-image-lightbox.html
everything.amp.html
@@ -92,3 +70,27 @@ properties that can be styled are `background` and `color`. The `amp-image-lightbox-caption` class is also available to style the caption section. + +## Validation errors + +The following lists validation errors specific to the `amp-image-lightbox` tag +(see also `amp-image-lightbox` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-image-lightbox extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.
diff --git a/extensions/amp-instagram/0.1/amp-instagram.js b/extensions/amp-instagram/0.1/amp-instagram.js index beb2edb126f3..3769d9620f4d 100644 --- a/extensions/amp-instagram/0.1/amp-instagram.js +++ b/extensions/amp-instagram/0.1/amp-instagram.js @@ -36,11 +36,15 @@ import {isLayoutSizeDefined} from '../../../src/layout'; import {loadPromise} from '../../../src/event-helper'; +import {setStyles} from '../../../src/style'; +import {removeElement} from '../../../src/dom'; class AmpInstagram extends AMP.BaseElement { /** @override */ preconnectCallback(onLayout) { + // See + // https://instagram.com/developer/embedding/?hl=en this.preconnect.url('https://www.instagram.com', onLayout); // Host instagram used for image serving. While the host name is // funky this appears to be stable in the post-domain sharding era. @@ -48,31 +52,111 @@ class AmpInstagram extends AMP.BaseElement { } /** @override */ - isLayoutSupported(layout) { - return isLayoutSizeDefined(layout); - } + buildCallback() { + /** + * @private {?Element} + */ + this.iframe_ = null; + /** + * @private {?Promise} + */ + this.iframePromise_ = null; - /** @override */ - layoutCallback() { - const width = this.element.getAttribute('width'); - const height = this.element.getAttribute('height'); - const shortcode = AMP.assert( + /** + * @private @const + */ + this.shortcode_ = AMP.assert( (this.element.getAttribute('data-shortcode') || this.element.getAttribute('shortcode')), 'The data-shortcode attribute is required for %s', this.element); - // See - // https://instagram.com/developer/embedding/?hl=en + } + + /** @override */ + prerenderAllowed() { + return true; + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } + + maybeRenderIframe_() { + if (this.iframePromise_) { + return this.iframePromise_; + } const iframe = document.createElement('iframe'); + this.iframe_ = iframe; iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allowtransparency', 'true'); iframe.src = 'https://www.instagram.com/p/' + - encodeURIComponent(shortcode) + '/embed/?v=4'; + encodeURIComponent(this.shortcode_) + '/embed/?v=4'; this.applyFillContent(iframe); - iframe.width = width; - iframe.height = height; + iframe.width = this.element.getAttribute('width'); + iframe.height = this.element.getAttribute('height'); this.element.appendChild(iframe); - return loadPromise(iframe); + setStyles(iframe, { + 'opacity': 0, + }); + return this.iframePromise_ = loadPromise(iframe).then(() => { + this.getVsync().mutate(() => { + setStyles(iframe, { + 'opacity': 1, + }); + }); + }); + } + + /** @override */ + layoutCallback() { + const image = new Image(); + // This will redirect to the image URL. By experimentation this is + // always the same URL that is actually used inside of the embed. + image.src = 'https://www.instagram.com/p/' + + encodeURIComponent(this.shortcode_) + '/media/?size=l'; + image.width = this.element.getAttribute('width'); + image.height = this.element.getAttribute('height'); + setStyles(image, { + 'object-fit': 'cover', + }); + const wrapper = document.createElement('wrapper'); + // This makes the non-iframe image appear in the exact same spot + // where it will be inside of the iframe. + setStyles(wrapper, { + 'position': 'absolute', + 'top': '48px', + 'bottom': '48px', + 'left': '8px', + 'right': '8px', + }); + wrapper.appendChild(image); + this.applyFillContent(image); + this.element.appendChild(wrapper); + // The iframe takes up a lot of resources. We only render it of we are in + // in the viewport. + if (this.isInViewport()) { + return this.maybeRenderIframe_(); + } + return loadPromise(image); + } + + /** @override */ + viewportCallback(inViewport) { + // We might not have been rendered this yet. Lets do it now. + if (inViewport) { + this.maybeRenderIframe_(); + } + } + + /** @override */ + documentInactiveCallback() { + if (this.iframe_) { + removeElement(this.iframe_); + this.iframe_ = null; + this.iframePromise_ = null; + } + return true; // Call layoutCallback again. } }; diff --git a/extensions/amp-instagram/0.1/test/test-amp-instagram.js b/extensions/amp-instagram/0.1/test/test-amp-instagram.js index 3d2a5c00f149..8b47cfa1735c 100644 --- a/extensions/amp-instagram/0.1/test/test-amp-instagram.js +++ b/extensions/amp-instagram/0.1/test/test-amp-instagram.js @@ -17,10 +17,22 @@ import {createIframePromise} from '../../../../testing/iframe'; require('../amp-instagram'); import {adopt} from '../../../../src/runtime'; +import * as sinon from 'sinon'; adopt(window); describe('amp-instagram', () => { + let sandbox; + let inViewport; + + beforeEach(() => { + inViewport = true; + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); function getIns(shortcode, opt_responsive) { return createIframePromise().then(iframe => { @@ -31,18 +43,59 @@ describe('amp-instagram', () => { if (opt_responsive) { ins.setAttribute('layout', 'responsive'); } + sandbox.stub(ins.implementation_, 'isInViewport', () => { + return inViewport; + }); return iframe.addElement(ins); }); } - it('renders', () => { + function testImage(image) { + expect(image).to.not.be.null; + expect(image.src).to.equal('https://www.instagram.com/p/fBwFP/media/?size=l'); + expect(image.getAttribute('width')).to.equal('111'); + expect(image.getAttribute('height')).to.equal('222'); + } + + function testIframe(iframe) { + expect(iframe).to.not.be.null; + expect(iframe.src).to.equal('https://www.instagram.com/p/fBwFP/embed/?v=4'); + expect(iframe.getAttribute('width')).to.equal('111'); + expect(iframe.getAttribute('height')).to.equal('222'); + } + + it('renders in viewport', () => { + return getIns('fBwFP').then(ins => { + testIframe(ins.querySelector('iframe')); + testImage(ins.querySelector('img')); + }); + }); + + it('renders outside viewport', () => { + inViewport = false; + return getIns('fBwFP').then(ins => { + let iframe = ins.querySelector('iframe'); + expect(iframe).to.be.null; + // Still not in viewport + ins.implementation_.viewportCallback(false); + iframe = ins.querySelector('iframe'); + expect(iframe).to.be.null; + // In viewport + ins.implementation_.viewportCallback(true); + iframe = ins.querySelector('iframe'); + testIframe(iframe); + testImage(ins.querySelector('img')); + }); + }); + + it('removes iframe after documentInactiveCallback', () => { return getIns('fBwFP').then(ins => { - const iframe = ins.firstChild; - expect(iframe).to.not.be.null; - expect(iframe.tagName).to.equal('IFRAME'); - expect(iframe.src).to.equal('https://www.instagram.com/p/fBwFP/embed/?v=4'); - expect(iframe.getAttribute('width')).to.equal('111'); - expect(iframe.getAttribute('height')).to.equal('222'); + testIframe(ins.querySelector('iframe')); + const obj = ins.implementation_; + obj.documentInactiveCallback(); + expect(ins.querySelector('iframe')).to.be.null; + expect(obj.iframe_).to.be.null; + expect(obj.iframePromise_).to.be.null; }); }); diff --git a/extensions/amp-instagram/amp-instagram.md b/extensions/amp-instagram/amp-instagram.md index 4e771e7c3f54..a5f30add8b39 100644 --- a/extensions/amp-instagram/amp-instagram.md +++ b/extensions/amp-instagram/amp-instagram.md @@ -31,10 +31,47 @@ limitations under the License. Examples - amp-instagram.html
instagram.amp.html + amp-instagram.html
instagram.amp.html +## Behavior + +The `width` and `height` attributes are special for the instagram embed. +These should be the actual width and height of the instagram image. +The system automatically adds space for the "chrome" that instagram adds around the image. + +Many instagrams are square. When you set `layout="responsive"` any value where `width` and `height` are the same will work. + +Example: +```html + + +``` + +If the instagram is not square you will need to enter the actual dimensions of the image. + +When using non-responsive layout you will need to account for the extra space added for the "instagram chrome" around the image. This is currently 48px above and below the image and 8px on the sides. + +## Attributes + + + +**data-shortcode** + +The instagram data-shortcode found in every instagram photo URL. + +E.g. in https://instagram.com/p/fBwFP fBwFP is the data-shortcode. + +## Validation errors + The following lists validation errors specific to the `amp-instagram` tag (see also `amp-instagram` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -81,38 +118,3 @@ The following lists validation errors specific to the `amp-instagram` tag The attribute shortcode is deprecated - use data-shortcode instead. - -## Behavior - -The `width` and `height` attributes are special for the instagram embed. -These should be the actual width and height of the instagram image. -The system automatically adds space for the "chrome" that instagram adds around the image. - -Many instagrams are square. When you set `layout="responsive"` any value where `width` and `height` are the same will work. - -Example: -```html - - -``` - -If the instagram is not square you will need to enter the actual dimensions of the image. - -When using non-responsive layout you will need to account for the extra space added for the "instagram chrome" around the image. This is currently 48px above and below the image and 8px on the sides. - -## Attributes - - - -**data-shortcode** - -The instagram data-shortcode found in every instagram photo URL. - -E.g. in https://instagram.com/p/fBwFP fBwFP is the data-shortcode. diff --git a/extensions/amp-install-serviceworker/0.1/test/test-amp-install-serviceworker.js b/extensions/amp-install-serviceworker/0.1/test/test-amp-install-serviceworker.js index f3eff0d7b83b..b8fdf1988aa5 100644 --- a/extensions/amp-install-serviceworker/0.1/test/test-amp-install-serviceworker.js +++ b/extensions/amp-install-serviceworker/0.1/test/test-amp-install-serviceworker.js @@ -14,7 +14,7 @@ * limitations under the License. */ -require('../../../../build/all/v0/amp-install-serviceworker-0.1.max'); +require('../amp-install-serviceworker'); import {adopt} from '../../../../src/runtime'; adopt(window); diff --git a/extensions/amp-install-serviceworker/amp-install-serviceworker.md b/extensions/amp-install-serviceworker/amp-install-serviceworker.md index 87fe245e49d8..b4beb3c66d5a 100644 --- a/extensions/amp-install-serviceworker/amp-install-serviceworker.md +++ b/extensions/amp-install-serviceworker/amp-install-serviceworker.md @@ -35,6 +35,37 @@ limitations under the License. +## Behavior + +Registers the ServiceWorker given by the `src` attribute. If the current origin is different from the origin of the ServiceWorker, this custom element does nothing (emits warning in development mode). + +This ServiceWorker runs whenever the AMP file is served from the origin where you publish the AMP file. The ServiceWorker will not be loaded when the document is loaded from an AMP cache. + +See [this article](https://medium.com/@cramforce/amps-and-websites-in-the-age-of-the-service-worker-8369841dc962) for how ServiceWorkers can help with making the AMP experience awesome with ServiceWorkers. + +Example + +```html + + + + +``` + +## Attributes + +### `src` + +URL of the ServiceWorker to register. + +### `layout` + +Must have the value `nodisplay`. + +## Validation errors + The following lists validation errors specific to the `amp-install-serviceworker` tag (see also `amp-install-serviceworker` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -68,32 +99,3 @@ The following lists validation errors specific to the `amp-install-serviceworker The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value. - -## Behavior - -Registers the ServiceWorker given by the `src` attribute. If the current origin is different from the origin of the ServiceWorker, this custom element does nothing (emits warning in development mode). - -This ServiceWorker runs whenever the AMP file is served from the origin where you publish the AMP file. The ServiceWorker will not be loaded when the document is loaded from an AMP cache. - -See [this article](https://medium.com/@cramforce/amps-and-websites-in-the-age-of-the-service-worker-8369841dc962) for how ServiceWorkers can help with making the AMP experience awesome with ServiceWorkers. - -Example - -```html - - - - -``` - -## Attributes - -### `src` - -URL of the ServiceWorker to register. - -### `layout` - -Must have the value `nodisplay`. diff --git a/extensions/amp-lightbox/amp-lightbox.md b/extensions/amp-lightbox/amp-lightbox.md index c89a6c744503..d5f70947af05 100644 --- a/extensions/amp-lightbox/amp-lightbox.md +++ b/extensions/amp-lightbox/amp-lightbox.md @@ -31,29 +31,7 @@ limitations under the License. Examples - amp-lightbox.html
everything.amp.html - - - -The following lists validation errors specific to the `amp-lightbox` tag -(see also `amp-lightbox` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-lightbox extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.amp-lightbox.html
everything.amp.html
@@ -79,3 +57,27 @@ Example: ## Styling The `amp-lightbox` component can be styled with standard CSS. + +## Validation errors + +The following lists validation errors specific to the `amp-lightbox` tag +(see also `amp-lightbox` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-lightbox extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.
diff --git a/extensions/amp-list/amp-list.md b/extensions/amp-list/amp-list.md index 17b56582b074..0b4515658da2 100644 --- a/extensions/amp-list/amp-list.md +++ b/extensions/amp-list/amp-list.md @@ -36,48 +36,6 @@ using a supplied template. -The following lists validation errors specific to the `amp-list` tag -(see also `amp-list` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-list extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when src attribute is missing.
Missing URL for attribute 'example1' in tag 'example2'.Error thrown when src attribute is missing it's URL.
Malformed URL 'example3' for attribute 'example1' in tag 'example2'.Error thrown when src attribute's URL is invalid.
Invalid URL protocol 'example3:' for attribute 'example1' in tag 'example2'.Error thrown src attribute's URL is http; https protocol required.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
- ## Usage The `amp-list` defines data source using the following attributes: @@ -155,3 +113,47 @@ that AMP Runtime can resize it. By default, `amp-list` adds `list` ARIA role to the list element and `listitem` role to item elements rendered via the template. + +## Validation errors + +The following lists validation errors specific to the `amp-list` tag +(see also `amp-list` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-list extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when src attribute is missing.
Missing URL for attribute 'example1' in tag 'example2'.Error thrown when src attribute is missing it's URL.
Malformed URL 'example3' for attribute 'example1' in tag 'example2'.Error thrown when src attribute's URL is invalid.
Invalid URL protocol 'example3:' for attribute 'example1' in tag 'example2'.Error thrown src attribute's URL is http; https protocol required.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-mustache/0.1/test/test-amp-mustache.js b/extensions/amp-mustache/0.1/test/test-amp-mustache.js index 127eddf5f69d..d21ff06e2b5e 100644 --- a/extensions/amp-mustache/0.1/test/test-amp-mustache.js +++ b/extensions/amp-mustache/0.1/test/test-amp-mustache.js @@ -16,7 +16,7 @@ import { AmpMustache, -} from '../../../../build/all/v0/amp-mustache-0.1.max'; +} from '../amp-mustache'; describe('amp-mustache template', () => { diff --git a/extensions/amp-mustache/amp-mustache.md b/extensions/amp-mustache/amp-mustache.md index 3d2a40081382..e111c7784c3c 100644 --- a/extensions/amp-mustache/amp-mustache.md +++ b/extensions/amp-mustache/amp-mustache.md @@ -31,28 +31,6 @@ limitations under the License. -The following lists validation errors specific to `amp-mustache` -(see also `template` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - -
Validation ErrorDescription
Mustache template syntax in attribute name 'example1' in tag 'example2'.Mustache templates must be within the amp-mustache tag. Error thrown when templating syntax found in an attribute in another tag.
The attribute 'example1' in tag 'example2' is set to 'example3', which contains unescaped Mustache template syntax.Mustache templates must be within the amp-mustache tag. Error thrown when unescaped templating syntax found in an attribute in another tag.
The attribute 'example1' in tag 'example2' is set to 'example3', which contains a Mustache template partial.Mustache templates must be within the amp-mustache tag. Error thrown when template partial found in an attribute in another tag.
- ## Syntax Mustache is a logic-less template syntax. See [Mustache.js docs](https://github.com/janl/mustache.js/) @@ -98,3 +76,27 @@ formatting tags such as ``, ``, and so on. Notice also that because the body of the template has to be specified within the `template` element, it is impossible to specify `{{&var}}` expressions - they will always be escaped as `{{&var}}`. The triple-mustache `{{{var}}}` has to be used for these cases. + +## Validation errors + +The following lists validation errors specific to `amp-mustache` +(see also `template` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
Mustache template syntax in attribute name 'example1' in tag 'example2'.Mustache templates must be within the amp-mustache tag. Error thrown when templating syntax found in an attribute in another tag.
The attribute 'example1' in tag 'example2' is set to 'example3', which contains unescaped Mustache template syntax.Mustache templates must be within the amp-mustache tag. Error thrown when unescaped templating syntax found in an attribute in another tag.
The attribute 'example1' in tag 'example2' is set to 'example3', which contains a Mustache template partial.Mustache templates must be within the amp-mustache tag. Error thrown when template partial found in an attribute in another tag.
diff --git a/extensions/amp-pinterest/0.1/amp-pinterest.js b/extensions/amp-pinterest/0.1/amp-pinterest.js index 4ae3d817d09e..1a007e838155 100644 --- a/extensions/amp-pinterest/0.1/amp-pinterest.js +++ b/extensions/amp-pinterest/0.1/amp-pinterest.js @@ -35,6 +35,7 @@ * */ +import {CSS} from '../../../build/amp-pinterest-0.1.css'; import {isLayoutSizeDefined} from '../../../src/layout'; import {FollowButton} from './follow-button'; @@ -85,4 +86,4 @@ class AmpPinterest extends AMP.BaseElement { }; -AMP.registerElement('amp-pinterest', AmpPinterest, $CSS$); +AMP.registerElement('amp-pinterest', AmpPinterest, CSS); diff --git a/extensions/amp-pinterest/0.1/test/test-amp-pinterest.js b/extensions/amp-pinterest/0.1/test/test-amp-pinterest.js index 78c9f97f938f..d6bd2fe84952 100644 --- a/extensions/amp-pinterest/0.1/test/test-amp-pinterest.js +++ b/extensions/amp-pinterest/0.1/test/test-amp-pinterest.js @@ -14,7 +14,7 @@ * limitations under the License. */ - require('../../../../build/all/v0/amp-pinterest-0.1.max'); + require('../amp-pinterest'); import {adopt} from '../../../../src/runtime'; import {Timer} from '../../../../src/timer'; diff --git a/extensions/amp-pinterest/amp-pinterest.md b/extensions/amp-pinterest/amp-pinterest.md index 0fb6fcde1f00..c443eff98b7e 100644 --- a/extensions/amp-pinterest/amp-pinterest.md +++ b/extensions/amp-pinterest/amp-pinterest.md @@ -35,35 +35,6 @@ limitations under the License. -The following lists validation errors specific to the `amp-pinterest` tag -(see also `amp-pinterest` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - -
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-pinterest extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-do attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
- ## Examples: Pin It button: `data-do="buttonPin"` @@ -149,3 +120,33 @@ When building the Embedded Pin widget, `data-url` is required and must contain t data-do="embedPin" data-url="https://www.pinterest.com/pin/99360735500167749/" +## Validation errors + +The following lists validation errors specific to the `amp-pinterest` tag +(see also `amp-pinterest` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-pinterest extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-do attribute is missing.
The implied layout 'example1' is not supported by tag 'example2'.Error thrown when implied layout is set to CONTAINER; this layout type isn't supported.
The specified layout 'example1' is not supported by tag 'example2'.Error thrown when specified layout is set to CONTAINER; this layout type isn't supported.
The property 'example1' in attribute 'example2' in tag 'example3' is set to 'example4', which is invalid.Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
diff --git a/extensions/amp-social-share/0.1/amp-social-share-config.js b/extensions/amp-social-share/0.1/amp-social-share-config.js new file mode 100644 index 000000000000..8867ca4dfecf --- /dev/null +++ b/extensions/amp-social-share/0.1/amp-social-share-config.js @@ -0,0 +1,158 @@ +/** + * Copyright 2016 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. + */ + +/** + * Get social share configurations by supported type. + * @param {!string} + * @return {!Object} + */ +export function getSocialConfig(type) { + AMP.assert(type in BUILTINS, + 'Unknown social share type ' + type); + return BUILTINS[type]; +} + +/** + * @type {Object} + */ +const BUILTINS = { + 'twitter': { + 'url': 'https://twitter.com/intent/tweet', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'url': { + 'param': 'url', + 'required': false, + 'type': 'url', + 'maxlength': 1024, + }, + 'text': { + 'param': 'text', + 'required': false, + 'type': 'text', + 'maxlength': 140, + }, + 'attribution': { + 'param': 'via', + 'required': false, + 'type': 'text', + 'maxlength': 20, + }, + }, + }, + 'facebook': { + 'url': 'https://www.facebook.com/dialog/share', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'url': { + 'param': 'href', + 'required': false, + 'type': 'url', + 'maxlength': 1024, + }, + 'attribution': { + 'param': 'app_id', + 'required': true, + 'type': 'text', + 'maxlength': 128, + }, + }, + }, + 'pinterest': { + 'url': 'https://www.pinterest.com/pin/create/button/', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'url': { + 'param': 'url', + 'required': false, + 'type': 'url', + 'maxlength': 1024, + }, + 'text': { + 'param': 'description', + 'required': false, + 'type': 'text', + 'maxlength': 140, + }, + 'image': { + 'param': 'media', + 'required': false, + 'type': 'url', + 'maxlength': 1024, + }, + }, + }, + 'linkedin': { + 'url': 'https://www.linkedin.com/shareArticle', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'url': { + 'param': 'url', + 'required': true, + 'type': 'url', + 'maxlength': 1024, + }, + 'text': { + 'param': 'title', + 'required': false, + 'type': 'text', + 'maxlength': 200, + }, + 'attribution': { + 'param': 'source', + 'required': false, + 'type': 'text', + 'maxlength': 200, + }, + 'mini': { + 'param': 'mini', + 'required': false, + 'type': 'fixed', + 'value': 'true', + }, + }, + }, + 'gplus': { + 'url': 'https://plus.google.com/share', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'url': { + 'param': 'url', + 'required': true, + 'type': 'url', + 'maxlength': 1024, + }, + }, + }, + 'email': { + 'url': 'mailto:', + 'text': '\u00A0', // Use a nbsp to ensure the anchor isn't collapsed + 'params': { + 'text': { + 'param': 'subject', + 'required': true, + 'type': 'text', + 'maxlength': 1024, + }, + 'url': { + 'param': 'body', + 'required': true, + 'type': 'url', + 'maxlength': 1024, + }, + }, + }, +}; diff --git a/extensions/amp-social-share/0.1/amp-social-share.css b/extensions/amp-social-share/0.1/amp-social-share.css new file mode 100644 index 000000000000..d92314290866 --- /dev/null +++ b/extensions/amp-social-share/0.1/amp-social-share.css @@ -0,0 +1,71 @@ +/** + * Copyright 2016 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 "../../../third_party/optimized-svg-icons/amp-social-share-svgs.css"; + +amp-social-share { + height: 44px; + width: 60px; +} + +amp-social-share span { + display: block; + border-radius: 5px; + height: 44px; + width: 60px; +} + +amp-social-share a { + display: block; + padding: 12px; + background-repeat: no-repeat; + background-position: center; + text-decoration: none; +} + +/* Twitter Styling */ +amp-social-share .twitter { + background-color: #55acee; +} + +amp-social-share .twitter a { + background-image: url('https://g.twimg.com/dev/documentation/image/Twitter_logo_white_32.png'); +} + +/* Facebook Styling */ +amp-social-share .facebook { + background-color: #3b5998; +} + +/* Pinterest Styling */ +amp-social-share .pinterest { + background-color: #bd081c; +} + +/* LinkedIn Styling */ +amp-social-share .linkedin { + background-color: #0077b5; +} + +/* Google+ Styling */ +amp-social-share .gplus { + background-color: #dc4e41; +} + +/* Email Styling */ +amp-social-share .email { + background-color: #000000; +} diff --git a/extensions/amp-social-share/0.1/amp-social-share.js b/extensions/amp-social-share/0.1/amp-social-share.js new file mode 100644 index 000000000000..27c60a505964 --- /dev/null +++ b/extensions/amp-social-share/0.1/amp-social-share.js @@ -0,0 +1,157 @@ +/** + * Copyright 2016 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 {addParamsToUrl} from '../../../src/url'; +import {documentInfoFor} from '../../../src/document-info'; +import {elementByTag} from '../../../src/dom'; +import {getSocialConfig} from './amp-social-share-config'; +import {isLayoutSizeDefined, getLengthNumeral, + Layout} from '../../../src/layout'; +import {CSS} from '../../../build/amp-social-share-0.1.css'; + +/** @const {number} */ +const DEFAULT_WIDTH = 60; + +class AmpSocialShare extends AMP.BaseElement { + + /** @override */ + isLayoutSupported(layout) { + return layout == Layout.FIXED; + } + + /** @override */ + buildCallback() { + /** @private @const {!Element} */ + this.type_ = AMP.assert(this.element.getAttribute('type'), + 'The type attribute is required. %s', + this.element); + + /** @private @const {!Object} */ + this.typeConfig_ = getSocialConfig(this.type_); + + /** @private @const {!Object} */ + this.config_ = this.getElementConfig_(); + + /** @private @const {number} */ + this.width_ = getLengthNumeral(this.element.getAttribute('width')) + || DEFAULT_WIDTH; + + /** @private @const {number} */ + this.height_ = getLengthNumeral(this.element.getAttribute('height')); + + this.renderShare_(); + } + + /** + * Renders the share based on the element config. + * @return {!Element} + */ + renderShare_() { + const urlParams = {}; + + for (const param in this.typeConfig_['params']) { + const paramConf = this.typeConfig_['params'][param]; + let paramValue = this.config_[param] || this.getDefaultValue_( + param, this.config_); + AMP.assert(!paramConf['required'] || paramValue !== null, + param + ' is a required attribute for ' + this.type_ + '. %s', + this.element); + if (paramValue == null && paramConf['type'] == 'fixed') { + paramValue = paramConf['value']; + } + if ('maxlength' in paramConf) { + const maxlength = paramConf['maxlength']; + AMP.assert(!paramValue || paramValue.length < maxlength, + param + ' cannot exceed ' + maxlength + '. %s', this.element); + } + if (paramValue != null) { + urlParams[paramConf['param']] = paramValue; + } + } + + // Get the anchor or create one. + let link = elementByTag(this.element, 'a'); + if (link == null) { + link = this.getWin().document.createElement('a'); + link.textContent = this.typeConfig_['text']; + link.setAttribute('target', '_blank'); + + // Get the container or create one. + let container = elementByTag(this.element, 'span'); + if (container == null) { + container = this.getWin().document.createElement('span'); + container.classList.add(this.type_); + + // Only add the container to the element if it didn't exist + this.element.appendChild(container); + } + container.appendChild(link); + } + + // Set share url. + link.setAttribute('href', + addParamsToUrl(this.typeConfig_['url'], urlParams)); + } + + /** + * Gets the configuration for the current social element + * @param {!element} element + * @return {?Object} + */ + getElementConfig_() { + const script = elementByTag(this.element, 'script'); + let config = {}; + if (script) { + // Get config from script + try { + config = JSON.parse(script.textContent); + } catch (e) { + AMP.assert(false, 'Malformed JSON configuration. %s', this.element); + } + } else { + // Get config from attributes + for (const param in this.typeConfig_['params']) { + if (this.element.hasAttribute('data-' + param)) { + config[param] = this.element.getAttribute('data-' + param); + } + } + } + return config; + } + + /** + * Gets the default value for a given param, if there is one. + * Otherwise it returns null + * @param {!string} param + * @param {!Object} config + * @return {*} + */ + getDefaultValue_(param, config) { + if (param in config) { + return config[param]; + } + switch (param) { + case 'url': + const info = documentInfoFor(this.getWin()); + return info.canonicalUrl; + case 'text': + return ''; + } + return null; + } +}; + +AMP.registerElement('amp-social-share', AmpSocialShare, CSS); diff --git a/extensions/amp-social-share/0.1/test/test-amp-social-share.js b/extensions/amp-social-share/0.1/test/test-amp-social-share.js new file mode 100644 index 000000000000..11aec50cd9e7 --- /dev/null +++ b/extensions/amp-social-share/0.1/test/test-amp-social-share.js @@ -0,0 +1,242 @@ +/** + * Copyright 2016 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 {adopt} from '../../../../src/runtime'; +import {createIframePromise} from '../../../../testing/iframe'; +import '../amp-social-share'; + +adopt(window); + +const STRINGS = { + 'text': 'Hello world', + 'url': 'https://example.com/', + 'attribution': 'AMPhtml', + 'text-too-long': 'Hello world, Hello world, Hello world, Hello world, Hello' + + 'world, Hello world, Hello world, Hello world, Hello world, Hello world, ' + + 'Hello world', +}; + +describe('amp-social-share', () => { + + function getShare(type, config) { + return getCustomShare(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + const script = iframe.doc.createElement('script'); + + script.setAttribute('type', 'application/json'); + script.textContent = JSON.stringify(config);; + + share.setAttribute('type', type); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + share.appendChild(script); + return share; + }); + } + + function getCustomShare(modifier) { + return createIframePromise().then(iframe => { + const canonical = iframe.doc.createElement('link'); + + canonical.setAttribute('rel', 'canonical'); + canonical.setAttribute('href', STRINGS['url']); + + iframe.addElement(canonical); + + return iframe.addElement(modifier(iframe)); + }); + } + + it('renders twitter', () => { + const conf = { + 'text': STRINGS['text'], + 'url': STRINGS['url'], + 'attribution': STRINGS['attribution'], + }; + return getShare('twitter', conf).then(ins => { + const tShare = ins.getElementsByTagName('span')[0]; + expect(tShare).to.not.be.null; + expect(tShare.firstChild).to.not.be.null; + const shareAnchor = tShare.firstChild; + expect(shareAnchor.tagName).to.equal('A'); + + const shareHref = shareAnchor.getAttribute('href'); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['text'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['url'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['attribution'])); + }); + }); + + it('renders a custom element', () => { + return getCustomShare(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + const script = iframe.doc.createElement('script'); + const container = iframe.doc.createElement('span'); + const link = iframe.doc.createElement('a'); + + script.setAttribute('type', 'application/json'); + script.textContent = JSON.stringify({ + 'text': STRINGS['text'], + 'url': STRINGS['url'], + 'attribution': STRINGS['attribution'], + });; + + share.setAttribute('type', 'twitter'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + share.appendChild(script); + + container.classList.add('amp-social-share-test'); + container.appendChild(link); + + link.classList.add('amp-social-share-test'); + + return share; + }).then(ins => { + const tShare = ins.getElementsByTagName('span')[0]; + expect(tShare).to.not.be.null; + expect(tShare.firstChild).to.not.be.null; + const shareAnchor = tShare.firstChild; + expect(shareAnchor.tagName).to.equal('A'); + + const shareHref = shareAnchor.getAttribute('href'); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['text'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['url'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['attribution'])); + }); + }); + + it('renders a custom element with attribute config', () => { + return getCustomShare(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + const container = iframe.doc.createElement('span'); + const link = iframe.doc.createElement('a'); + + share.setAttribute('type', 'twitter'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + + // Set data + share.setAttribute('data-text', STRINGS['text']); + share.setAttribute('data-url', STRINGS['url']); + share.setAttribute('data-attribution', STRINGS['attribution']); + + container.classList.add('amp-social-share-test'); + container.appendChild(link); + + link.classList.add('amp-social-share-test'); + + return share; + }).then(ins => { + const tShare = ins.getElementsByTagName('span')[0]; + expect(tShare).to.not.be.null; + expect(tShare.firstChild).to.not.be.null; + const shareAnchor = tShare.firstChild; + expect(shareAnchor.tagName).to.equal('A'); + + const shareHref = shareAnchor.getAttribute('href'); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['text'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['url'])); + expect(shareHref).to.contain(encodeURIComponent(STRINGS['attribution'])); + }); + }); + + it('adds a default value for url', () => { + return getCustomShare(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + + share.setAttribute('type', 'twitter'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + + return share; + }).then(ins => { + const tShare = ins.getElementsByTagName('span')[0]; + expect(tShare).to.not.be.null; + expect(tShare.firstChild).to.not.be.null; + const shareAnchor = tShare.firstChild; + expect(shareAnchor.tagName).to.equal('A'); + + const shareHref = shareAnchor.getAttribute('href'); + expect(shareHref).to.contain(encodeURIComponent('url')); + }); + }); + + it('adds a default value for text', () => { + return getCustomShare(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + + share.setAttribute('type', 'twitter'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + + return share; + }).then(ins => { + const tShare = ins.getElementsByTagName('span')[0]; + expect(tShare).to.not.be.null; + expect(tShare.firstChild).to.not.be.null; + const shareAnchor = tShare.firstChild; + expect(shareAnchor.tagName).to.equal('A'); + + const shareHref = shareAnchor.getAttribute('href'); + expect(shareHref).to.contain(encodeURIComponent('text')); + }); + }); + + it('throws error with too long text', () => { + return createIframePromise().then(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + const script = iframe.doc.createElement('script'); + + script.setAttribute('type', 'application/json'); + script.textContent = JSON.stringify({ + 'text': STRINGS['text-too-long'], + 'url': STRINGS['url'], + 'attribution': STRINGS['attribution'], + });; + + share.setAttribute('type', 'twitter'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + share.appendChild(script); + + expect(() => { + share.build(true); + }).to.throw('text cannot exceed'); + }); + }); + + it('throws error with missing required field', () => { + return createIframePromise().then(iframe => { + const share = iframe.doc.createElement('amp-social-share'); + const script = iframe.doc.createElement('script'); + + script.setAttribute('type', 'application/json'); + script.textContent = JSON.stringify({ + 'url': STRINGS['url'], + });; + + share.setAttribute('type', 'facebook'); + share.setAttribute('width', 60); + share.setAttribute('height', 44); + share.appendChild(script); + + expect(() => { + share.build(true); + }).to.throw('attribution is a required attribute for facebook'); + }); + }); +}); diff --git a/extensions/amp-social-share/amp-social-share.md b/extensions/amp-social-share/amp-social-share.md new file mode 100644 index 000000000000..78658ef19370 --- /dev/null +++ b/extensions/amp-social-share/amp-social-share.md @@ -0,0 +1,107 @@ + + +### `amp-social-share` + +Displays a social share button. + +#### The simplest example: +The share button guesses some defaults for you. It assumes that the current window location is the URL you want to share and the page title is the text you want to share. +```html + + +``` + +#### Simple Examples: +When you want to configure the share content, you can specify ```data-``` configuration. +```html + + +``` +*or* + +You can embed a ```script``` tag with JSON configuration. +```html + + + +``` + +#### Customized views: +Sometimes you want to provide your own style. In this instance, you can embed an anchor without a ```href``` and it will be populated. This provides you the flexibility to build your own UI for the share button. +```html + + + +``` +*or* + +You can include any additional document structure around the anchor, so long as you don't specify the ```href```. +```html + + +
+ +
+
+``` + +### Structure + +Required attributes are `type`, `width` and `height`. Some [types (social providers)](#user-content-types) require specific fields for their integration. For instance Facebook requires you include your ```app_id``` (as ```attribution```), failure to this attribute for ```type="facebook"``` will result in an error. + +You can embed an `anchor` tag _without a_ ```href``` into the element to for the extension to provide the href for you. This enables customization of the social share element. You can also specify an arbitrary amount of AMP compatible HTML within the element to provide any hooks for styling. + +### Types + +The builtin supported types are configured in [AMP Social Share Config](v.0/amp-amp-social-share-config.js). Below are the possible types and their configuration options: +- twitter + - url `optional` (defaults: `rel=canonical` URL) + - text + - attribution +- facebook + - attribution `required` (Your `app_id`) + - url `optional` (defaults: `rel=canonical` URL) +- pinterest + - url `optional` (defaults: `rel=canonical` URL) + - text + - image +- linkedin + - url `optional` (defaults: `rel=canonical` URL) + - title + - attribution +- gplus + - url `optional` (defaults: `rel=canonical` URL) +- email + - text (email subject)`optional` (defaults: `''`) + - url (email body) `optional` (defaults: `rel=canonical` URL) + +As you can see, they use a common set of attribute names which are translated into specifics for the service. Note the required elements for each of the types - the most common is the URL which you'll want to include for the share to be of use. diff --git a/extensions/amp-soundcloud/amp-soundcloud.md b/extensions/amp-soundcloud/amp-soundcloud.md index 8775251fea05..9e914f7537b6 100644 --- a/extensions/amp-soundcloud/amp-soundcloud.md +++ b/extensions/amp-soundcloud/amp-soundcloud.md @@ -31,37 +31,7 @@ limitations under the License. Examples - soundcloud.amp.html - - - -The following lists validation errors specific to the `amp-soundcloud` tag -(see also `amp-soundcloud` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-soundcloud extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-trackid attribute missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the data-trackid attribute is invalid. Only integers allowed.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is FIXED_HEIGHT. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is FIXED_HEIGHT. Error thrown if specified layout is any other value.amp-soundcloud.html
soundcloud.amp.html
@@ -109,3 +79,35 @@ Custom color override for the "Classic" mode. Ignored in "Visual" mode. **width and height** Layout is `fixed-height` and will fill all the available horizontal space. This is ideal for "Classic" mode, but for "Visual", height is recommended to be 300px, 450px or 600px, as per Soundcloud embed code. This will allow the clip's internal elements to resize properly on mobile. + +## Validation errors + +The following lists validation errors specific to the `amp-soundcloud` tag +(see also `amp-soundcloud` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-soundcloud extension .js script tag is missing or incorrect.
The mandatory attribute 'example1' is missing in tag 'example2'.Error thrown when data-trackid attribute missing.
The attribute 'example1' in tag 'example2' is set to the invalid value 'example3'.Error thrown when the data-trackid attribute is invalid. Only integers allowed.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is FIXED_HEIGHT. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is FIXED_HEIGHT. Error thrown if specified layout is any other value.
diff --git a/extensions/amp-twitter/0.1/amp-twitter.js b/extensions/amp-twitter/0.1/amp-twitter.js index bb46b054f3e7..6d3b4c71e165 100644 --- a/extensions/amp-twitter/0.1/amp-twitter.js +++ b/extensions/amp-twitter/0.1/amp-twitter.js @@ -29,7 +29,8 @@ class AmpTwitter extends AMP.BaseElement { // All images this.preconnect.url('https://pbs.twimg.com', onLayout); // Hosts the script that renders tweets. - this.preconnect.prefetch('https://platform.twitter.com/widgets.js'); + this.preconnect.prefetch( + 'https://platform.twitter.com/widgets.js', 'script'); prefetchBootstrap(this.getWin()); } @@ -38,6 +39,11 @@ class AmpTwitter extends AMP.BaseElement { return isLayoutSizeDefined(layout); } + /** @override */ + firstLayoutCompleted() { + // Do not hide placeholder + } + /** @override */ layoutCallback() { // TODO(malteubl): Preconnect to twitter. @@ -47,6 +53,9 @@ class AmpTwitter extends AMP.BaseElement { this.element.appendChild(iframe); // Triggered by context.updateDimensions() inside the iframe. listen(iframe, 'embed-size', data => { + // We only get the message if and when there is a tweet to display, + // so hide the placeholder. + this.togglePlaceholder(false); iframe.height = data.height; iframe.width = data.width; const amp = iframe.parentElement; diff --git a/extensions/amp-twitter/amp-twitter.md b/extensions/amp-twitter/amp-twitter.md index 33dd7ca22098..8f2aa1e472ea 100644 --- a/extensions/amp-twitter/amp-twitter.md +++ b/extensions/amp-twitter/amp-twitter.md @@ -31,10 +31,41 @@ limitations under the License. Examples - amp-twitter.html
twitter.amp.html + amp-twitter.html
twitter.amp.html +## Behavior + +**CAVEAT** + +Twitter does not currently provide an API that yields fixed aspect ratio Tweet embeds. We currently automatically proportionally scale the Tweet to fit the provided size, but this may yield less than ideal appearance. Authors may need to manually tweak the provided width and height. You may also use the `media` attribute to select the aspect ratio based on screen width. We are looking for feedback how feasible this approach is in practice. + +Example: + +```html + + + +``` + +Copy the placeholder from Twitter's embed dialog, but remove the `script`. Then add the `placeholder` attribute to the `blockquote` tag. + +## Attributes + +**data-tweetid** + +The ID of the tweet. In a URL like https://twitter.com/joemccann/status/640300967154597888 `640300967154597888` is the tweetID. + +**data-nameofoption** + +Options for the Tweet appearance can be set using `data-` attributes. E.g. `data-cards="hidden"` deactivates Twitter cards. For documentation of the available options, see [Twitter's docs](https://dev.twitter.com/web/javascript/creating-widgets#create-tweet). + +## Validation errors + The following lists validation errors specific to the `amp-twitter` tag (see also `amp-twitter` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -81,29 +112,3 @@ May need to add something to this table based on technical review. Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY. - -## Behavior - -**CAVEAT** - -Twitter does not currently provide an API that yields fixed aspect ratio Tweet embeds. We currently automatically proportionally scale the Tweet to fit the provided size, but this may yield less than ideal appearance. Authors may need to manually tweak the provided width and height. You may also use the `media` attribute to select the aspect ratio based on screen width. We are looking for feedback how feasible this approach is in practice. - -Example: - -```html - - -``` - -## Attributes - -**data-tweetid** - -The ID of the tweet. In a URL like https://twitter.com/joemccann/status/640300967154597888 `640300967154597888` is the tweetID. - -**data-nameofoption** - -Options for the Tweet appearance can be set using `data-` attributes. E.g. `data-cards="hidden"` deactivates Twitter cards. For documentation of the available options, see [Twitter's docs](https://dev.twitter.com/web/javascript/creating-widgets#create-tweet). diff --git a/extensions/amp-user-notification/0.1/amp-user-notification.js b/extensions/amp-user-notification/0.1/amp-user-notification.js index c07905d326cd..466ea8c13be6 100644 --- a/extensions/amp-user-notification/0.1/amp-user-notification.js +++ b/extensions/amp-user-notification/0.1/amp-user-notification.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {CSS} from '../../../build/amp-user-notification-0.1.css'; import {assertHttpsUrl, addParamsToUrl} from '../../../src/url'; import {assert} from '../../../src/asserts'; import {cidFor} from '../../../src/cid'; @@ -407,4 +408,4 @@ export function installUserNotificationManager(window) { installUserNotificationManager(AMP.win); -AMP.registerElement('amp-user-notification', AmpUserNotification, $CSS$); +AMP.registerElement('amp-user-notification', AmpUserNotification, CSS); diff --git a/extensions/amp-user-notification/0.1/test/test-amp-user-notification.js b/extensions/amp-user-notification/0.1/test/test-amp-user-notification.js index f1ee2a91474e..bb5cc8bcb3cf 100644 --- a/extensions/amp-user-notification/0.1/test/test-amp-user-notification.js +++ b/extensions/amp-user-notification/0.1/test/test-amp-user-notification.js @@ -18,7 +18,7 @@ import * as sinon from 'sinon'; import { AmpUserNotification, UserNotificationManager, -} from '../../../../build/all/v0/amp-user-notification-0.1.max'; +} from '../amp-user-notification'; import {createIframePromise} from '../../../../testing/iframe'; diff --git a/extensions/amp-user-notification/amp-user-notification.md b/extensions/amp-user-notification/amp-user-notification.md index 15db75d8bc6b..724627a3f6f6 100644 --- a/extensions/amp-user-notification/amp-user-notification.md +++ b/extensions/amp-user-notification/amp-user-notification.md @@ -31,29 +31,7 @@ limitations under the License. Examples - amp-user-notification_with_local_storage.html
amp-user-notification_with_server_endpoint.html
user-notification.amp.html - - - -The following lists validation errors specific to the `amp-user-notification` tag -(see also `amp-user-notification` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): - - - - - - - - - - - - - - - - - +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-user-notification extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.amp-user-notification_with_local_storage.html
amp-user-notification_with_server_endpoint.html
user-notification.amp.html
@@ -230,3 +208,27 @@ Optionally one can delay generation of Client IDs used for analytics and similar - [CLIENT_ID URL substitution.](../../spec/amp-var-substitutions.md#CLIENT_ID) - [`amp-ad`](../../builtins/amp-ad.md) - [`amp-analytics`](../amp-analytics/amp-analytics.md) + +## Validation errors + +The following lists validation errors specific to the `amp-user-notification` tag +(see also `amp-user-notification` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
The 'example1' tag is missing or incorrect, but required by 'example2'.Error thrown when required amp-user-notification extension .js script tag is missing or incorrect.
The implied layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if implied layout is any other value.
The specified layout 'example1' is not supported by tag 'example2'.The only supported layout type is NODISPLAY. Error thrown if specified layout is any other value.
diff --git a/extensions/amp-vimeo/amp-vimeo.md b/extensions/amp-vimeo/amp-vimeo.md index a94268f64cbe..05898f832131 100644 --- a/extensions/amp-vimeo/amp-vimeo.md +++ b/extensions/amp-vimeo/amp-vimeo.md @@ -31,10 +31,31 @@ limitations under the License. Examples - vimeo.amp.html + amp-vimeo.html
vimeo.amp.html +## Example + +With responsive layout, the width and height from the example should yield correct layouts for 16:9 aspect ratio videos: + +```html + +``` + +## Attributes + +**data-videoid** + +The Vimeo video id found in every Vimeo video page URL + +E.g. in https://vimeo.com/27246366 27246366 is the video id. + +## Validation errors + The following lists validation errors specific to the `amp-vimeo` tag (see also `amp-vimeo` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -68,22 +89,3 @@ The following lists validation errors specific to the `amp-vimeo` tag Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types. - -## Example - -With responsive layout, the width and height from the example should yield correct layouts for 16:9 aspect ratio videos: - -```html - -``` - -## Attributes - -**data-videoid** - -The Vimeo video id found in every Vimeo video page URL - -E.g. in https://vimeo.com/27246366 27246366 is the video id. diff --git a/extensions/amp-vine/amp-vine.md b/extensions/amp-vine/amp-vine.md index d5f57e1e2417..f53222a6ac07 100644 --- a/extensions/amp-vine/amp-vine.md +++ b/extensions/amp-vine/amp-vine.md @@ -31,10 +31,28 @@ limitations under the License. Examples - vine.amp.html + amp-vine.html
vine.amp.html +## Example + +A Vine simple embed has equal width and height: + +```html + + +``` + +## Attributes + +**data-vineid** + +The ID of the Vine. In a URL like https://vine.co/v/MdKjXez002d `MdKjXez002d` is the vineID. + +## Validation errors + The following lists validation errors specific to the `amp-vine` tag (see also `amp-vine` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -64,19 +82,3 @@ The following lists validation errors specific to the `amp-vine` tag Error thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY. - -## Example - -A Vine simple embed has equal width and height: - -```html - - -``` - -## Attributes - -**data-vineid** - -The ID of the Vine. In a URL like https://vine.co/v/MdKjXez002d `MdKjXez002d` is the vineID. diff --git a/extensions/amp-youtube/0.1/amp-youtube.js b/extensions/amp-youtube/0.1/amp-youtube.js index bfbfe261e9e8..f2dcbd755e88 100644 --- a/extensions/amp-youtube/0.1/amp-youtube.js +++ b/extensions/amp-youtube/0.1/amp-youtube.js @@ -38,6 +38,11 @@ class AmpYoutube extends AMP.BaseElement { return isLayoutSizeDefined(layout); } + /** @override */ + renderOutsideViewport() { + return false; + } + /** @override */ buildCallback() { const width = this.element.getAttribute('width'); @@ -176,7 +181,7 @@ class AmpYoutube extends AMP.BaseElement { // load the needed size or even better match YTPlayer logic for loading // player thumbnails for different screen sizes for a cache win! imgPlaceholder.src = 'https://i.ytimg.com/vi/' + - encodeURIComponent(this.videoid_) + '/sddefault.jpg'; + encodeURIComponent(this.videoid_) + '/sddefault.jpg#404_is_fine'; imgPlaceholder.setAttribute('placeholder', ''); imgPlaceholder.width = this.width_; imgPlaceholder.height = this.height_; diff --git a/extensions/amp-youtube/0.1/test/test-amp-youtube.js b/extensions/amp-youtube/0.1/test/test-amp-youtube.js index a6151dfa2d72..ed69b7ec5d31 100644 --- a/extensions/amp-youtube/0.1/test/test-amp-youtube.js +++ b/extensions/amp-youtube/0.1/test/test-amp-youtube.js @@ -95,7 +95,7 @@ describe('amp-youtube', () => { expect(imgPlaceholder).to.not.be.null; expect(imgPlaceholder.className).to.not.match(/amp-hidden/); expect(imgPlaceholder.getAttribute('src')).to.be.equal( - 'https://i.ytimg.com/vi/mGENRKrdoGY/sddefault.jpg'); + 'https://i.ytimg.com/vi/mGENRKrdoGY/sddefault.jpg#404_is_fine'); }).then(yt => { const iframe = yt.querySelector('iframe'); expect(iframe).to.not.be.null; @@ -121,7 +121,7 @@ describe('amp-youtube', () => { expect(imgPlaceholder.className).to.match(/amp-hidden/); expect(imgPlaceholder.src).to.equal( - 'https://i.ytimg.com/vi/mGENRKrdoGY/sddefault.jpg'); + 'https://i.ytimg.com/vi/mGENRKrdoGY/sddefault.jpg#404_is_fine'); }); }); @@ -182,6 +182,5 @@ describe('amp-youtube', () => { yt.implementation_.documentInactiveCallback(); expect(yt.implementation_.pauseVideo_.called).to.be.true; }); - }); }); diff --git a/extensions/amp-youtube/amp-youtube.md b/extensions/amp-youtube/amp-youtube.md index 4836d38ee603..17de3cbc3710 100644 --- a/extensions/amp-youtube/amp-youtube.md +++ b/extensions/amp-youtube/amp-youtube.md @@ -31,10 +31,31 @@ limitations under the License. Examples - amp-youtube.html
everything.amp.html + amp-youtube.html
everything.amp.html +## Example + +With responsive layout the width and height from the example should yield correct layouts for 16:9 aspect ratio videos: + +```html + +``` + +## Attributes + +**data-videoid** + +The Youtube video id found in every Youtube video page URL + +E.g. in https://www.youtube.com/watch?v=Z1q71gFeRqM Z1q71gFeRqM is the video id. + +## Validation errors + The following lists validation errors specific to the `amp-youtube` tag (see also `amp-youtube` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii)): @@ -80,22 +101,3 @@ The following lists validation errors specific to the `amp-youtube` tag The attribute video-id is deprecated - use data-videoid instead - -## Example - -With responsive layout the width and height from the example should yield correct layouts for 16:9 aspect ratio videos: - -```html - -``` - -## Attributes - -**data-videoid** - -The Youtube video id found in every Youtube video page URL - -E.g. in https://www.youtube.com/watch?v=Z1q71gFeRqM Z1q71gFeRqM is the video id. diff --git a/gulpfile.js b/gulpfile.js index e654cc275342..40504046b545 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,29 +16,23 @@ checkMinVersion(); +var $$ = require('gulp-load-plugins')(); var autoprefixer = require('autoprefixer'); var babel = require('babelify'); var browserify = require('browserify'); var buffer = require('vinyl-buffer'); var closureCompile = require('./build-system/tasks/compile').closureCompile; var cssnano = require('cssnano'); -var file = require('gulp-file'); var fs = require('fs-extra'); -var gulp = require('gulp-help')(require('gulp')); -var gulpWatch = require('gulp-watch'); +var gulp = $$.help(require('gulp')); var lazypipe = require('lazypipe'); var minimist = require('minimist'); var postcss = require('postcss'); -var rename = require('gulp-rename'); -var replace = require('gulp-replace'); +var postcssImport = require('postcss-import'); var source = require('vinyl-source-stream'); -var sourcemaps = require('gulp-sourcemaps'); var touch = require('touch'); -var uglify = require('gulp-uglify'); -var util = require('gulp-util'); var watchify = require('watchify'); var windowConfig = require('./build-system/window-config'); -var wrap = require('gulp-wrap'); var internalRuntimeVersion = require('./build-system/internal-version').VERSION; var internalRuntimeToken = require('./build-system/internal-version').TOKEN; @@ -100,6 +94,7 @@ function buildExtensions(options) { * Please see {@link AmpCarousel} with `type=slides` attribute instead. */ buildExtension('amp-slides', '0.1', false, options); + buildExtension('amp-social-share', '0.1', true, options); buildExtension('amp-twitter', '0.1', false, options); buildExtension('amp-user-notification', '0.1', true, options); buildExtension('amp-vimeo', '0.1', false, options); @@ -160,7 +155,7 @@ function compileCss() { console.info('Recompiling CSS.'); return jsifyCssPromise('css/amp.css').then(function(css) { return gulp.src('css/**.css') - .pipe(file('css.js', 'export const cssText = ' + css)) + .pipe($$.file('css.js', 'export const cssText = ' + css)) .pipe(gulp.dest('build')); }); } @@ -178,7 +173,9 @@ function jsifyCssPromise(filename) { var transformers = [cssprefixer, cssnano]; // Remove copyright comment. Crude hack to get our own copyright out // of the string. - return postcss(transformers).process(css.toString()) + return postcss(transformers).use(postcssImport).process(css.toString(), { + 'from': filename + }) .then(function(result) { result.warnings().forEach(function(warn) { console.warn(warn.toString()); @@ -192,7 +189,7 @@ function jsifyCssPromise(filename) { * Enables watching for file changes in css, extensions, and examples. */ function watch() { - gulpWatch('css/**/*.css', function() { + $$.watch('css/**/*.css', function() { compileCss(); }); buildExtensions({ @@ -208,8 +205,9 @@ function watch() { * to * dist/v0/$name-$version.js * - * Optionally copies the CSS at extensions/$name/$version/$name.css into the - * JS file marked with $CSS$ as a third argument to the registerElement call. + * Optionally copies the CSS at extensions/$name/$version/$name.css into + * a generated JS file that can be required from the extensions as + * `import {CSS} from '../../../build/$name-0.1.css';` * * @param {string} name Name of the extension. Must be the sub directory in * the extensions directory and the name of the JS and optional CSS file. @@ -231,27 +229,26 @@ function buildExtension(name, version, hasCss, options) { // Do not set watchers again when we get called by the watcher. var copy = Object.create(options); copy.watch = false; - gulpWatch(path + '/*', function() { + $$.watch(path + '/*', function() { buildExtension(name, version, hasCss, copy); }); } - var js = fs.readFileSync(jsPath, 'utf8'); if (hasCss) { + mkdirSync('build'); return jsifyCssPromise(path + '/' + name + '.css').then(function(css) { - console.assert(/\$CSS\$/.test(js), - 'Expected to find $CSS$ marker in extension JS: ' + jsPath); - js = js.replace(/\$CSS\$/, css); - return buildExtensionJs(js, path, name, version, options); + var jsCss = 'export const CSS = ' + css + ';\n'; + var builtName = 'build/' + name + '-' + version + '.css.js'; + fs.writeFileSync(builtName, jsCss, 'utf-8'); + return buildExtensionJs(path, name, version, options); }); } else { - return buildExtensionJs(js, path, name, version, options); + return buildExtensionJs(path, name, version, options); } } /** * Build the JavaScript for the extension specified * - * @param {string} js JavaScript file content * @param {string} path Path to the extensions directory * @param {string} name Name of the extension. Must be the sub directory in * the extensions directory and the name of the JS and optional CSS file. @@ -260,23 +257,16 @@ function buildExtension(name, version, hasCss, options) { * @param {!Object} options * @return {!Stream} Gulp object */ -function buildExtensionJs(js, path, name, version, options) { - var builtName = name + '-' + version + '.max.js'; - var minifiedName = name + '-' + version + '.js'; - var latestName = name + '-latest.js'; - return gulp.src(path + '/*.js') - .pipe(file(builtName, js)) - .pipe(gulp.dest('build/all/v0/')) - .on('end', function() { - compileJs('./build/all/v0/', builtName, './dist/v0', { - watch: options.watch, - minify: options.minify, - minifiedName: minifiedName, - latestName: latestName, - wrapper: '(window.AMP = window.AMP || [])' + - '.push(function(AMP) {<%= contents %>\n});', - }); - }); +function buildExtensionJs(path, name, version, options) { + compileJs(path + '/', name + '.js', './dist/v0', { + watch: options.watch, + minify: options.minify, + toName: name + '-' + version + '.max.js', + minifiedName: name + '-' + version + '.js', + latestName: name + '-latest.js', + wrapper: '(window.AMP = window.AMP || [])' + + '.push(function(AMP) {<%= contents %>\n});', + }); } /** @@ -308,7 +298,7 @@ function dist() { */ function buildExamples(watch) { if (watch) { - gulpWatch('examples/*.html', function() { + $$.watch('examples/*.html', function() { buildExamples(false); }); } @@ -316,9 +306,9 @@ function buildExamples(watch) { fs.copy('examples/', 'examples.build/', {clobber: true}, function(err) { if (err) { - return util.log(util.colors.red('copy error: ', err)); + return $$.util.log($$.util.colors.red('copy error: ', err)); } - util.log(util.colors.green('copied examples to examples.build')); + $$.util.log($$.util.colors.green('copied examples to examples.build')); }); // Also update test-example-validation.js @@ -346,6 +336,7 @@ function buildExamples(watch) { buildExample('instagram.amp.html'); buildExample('pinterest.amp.html'); buildExample('released.amp.html'); + buildExample('social-share.amp.html'); buildExample('twitter.amp.html'); buildExample('soundcloud.amp.html'); buildExample('user-notification.amp.html'); @@ -375,14 +366,14 @@ function buildExample(name) { max = max.replace('https://cdn.ampproject.org/v0.max.js', '../dist/amp.js'); max = max.replace(/https:\/\/cdn.ampproject.org\/v0\//g, '../dist/v0/'); gulp.src(input) - .pipe(file(name.replace('.html', '.max.html'),max)) + .pipe($$.file(name.replace('.html', '.max.html'),max)) .pipe(gulp.dest('examples.build/')); var min = max; min = min.replace(/\.max\.js/g, '.js'); min = min.replace('../dist/amp.js', '../dist/v0.js'); gulp.src(input) - .pipe(file(name.replace('.html', '.min.html'), min)) + .pipe($$.file(name.replace('.html', '.min.html'), min)) .pipe(gulp.dest('examples.build/')); } @@ -396,7 +387,7 @@ function buildExample(name) { function thirdPartyBootstrap(watch, shouldMinify) { var input = '3p/frame.max.html'; if (watch) { - gulpWatch(input, function() { + $$.watch(input, function() { thirdPartyBootstrap(false); }); } @@ -414,7 +405,7 @@ function thirdPartyBootstrap(watch, shouldMinify) { // Convert default relative URL to absolute min URL. min = min.replace(/\.\/integration\.js/g, jsPrefix + '/f.js'); gulp.src(input) - .pipe(file('frame.html', min)) + .pipe($$.file('frame.html', min)) .pipe(gulp.dest('dist.3p/' + internalRuntimeVersion)) .on('end', function() { var aliasToLatestBuild = 'dist.3p/current'; @@ -454,13 +445,13 @@ function compileJs(srcDir, srcFilename, destDir, options) { var lazybuild = lazypipe() .pipe(source, srcFilename) .pipe(buffer) - .pipe(replace, /\$internalRuntimeVersion\$/g, internalRuntimeVersion) - .pipe(replace, /\$internalRuntimeToken\$/g, internalRuntimeToken) - .pipe(wrap, wrapper) - .pipe(sourcemaps.init.bind(sourcemaps), {loadMaps: true}); + .pipe($$.replace, /\$internalRuntimeVersion\$/g, internalRuntimeVersion) + .pipe($$.replace, /\$internalRuntimeToken\$/g, internalRuntimeToken) + .pipe($$.wrap, wrapper) + .pipe($$.sourcemaps.init.bind($$.sourcemaps), {loadMaps: true}); var lazywrite = lazypipe() - .pipe(sourcemaps.write.bind(sourcemaps), './') + .pipe($$.sourcemaps.write.bind($$.sourcemaps), './') .pipe(gulp.dest.bind(gulp), destDir); function rebundle() { @@ -469,18 +460,18 @@ function compileJs(srcDir, srcFilename, destDir, options) { .on('error', function(err) { activeBundleOperationCount--; if (err instanceof SyntaxError) { - console.error(util.colors.red('Syntax error:', err.message)); + console.error($$.util.colors.red('Syntax error:', err.message)); } else { - console.error(err); + console.error($$.util.colors.red(err.message)); } }) .pipe(lazybuild()) - .pipe(rename(options.toName || srcFilename)) + .pipe($$.rename(options.toName || srcFilename)) .pipe(lazywrite()) .on('end', function() { activeBundleOperationCount--; if (activeBundleOperationCount == 0) { - console.info(util.colors.green('All current JS updates done.')); + console.info($$.util.colors.green('All current JS updates done.')); } }); } @@ -518,10 +509,10 @@ function compileJs(srcDir, srcFilename, destDir, options) { bundler.bundle() .on('error', function(err) { console.error(err); this.emit('end'); }) .pipe(lazybuild()) - .pipe(uglify({ + .pipe($$.uglify({ preserveComments: 'some' })) - .pipe(rename(options.minifiedName)) + .pipe($$.rename(options.minifiedName)) .pipe(lazywrite()) .on('end', function() { fs.writeFileSync(destDir + '/version.txt', internalRuntimeVersion); @@ -552,9 +543,9 @@ function buildExperiments(options) { function copyHandler(name, err) { if (err) { - return util.log(util.colors.red('copy error: ', err)); + return $$.util.log($$.util.colors.red('copy error: ', err)); } - util.log(util.colors.green('copied ' + name)); + $$.util.log($$.util.colors.green('copied ' + name)); } var path = 'tools/experiments'; @@ -572,7 +563,7 @@ function buildExperiments(options) { // Do not set watchers again when we get called by the watcher. var copy = Object.create(options); copy.watch = false; - gulpWatch(path + '/*', function() { + $$.watch(path + '/*', function() { buildExperiments(copy); }); } @@ -583,7 +574,7 @@ function buildExperiments(options) { var minHtml = html.replace('../../dist.tools/experiments/experiments.max.js', 'https://cdn.ampproject.org/v0/experiments.js'); gulp.src(htmlPath) - .pipe(file('experiments.cdn.html', minHtml)) + .pipe($$.file('experiments.cdn.html', minHtml)) .pipe(gulp.dest('dist.tools/experiments/')); // Build JS. @@ -591,7 +582,7 @@ function buildExperiments(options) { var builtName = 'experiments.max.js'; var minifiedName = 'experiments.js'; return gulp.src(path + '/*.js') - .pipe(file(builtName, js)) + .pipe($$.file(builtName, js)) .pipe(gulp.dest('build/experiments/')) .on('end', function() { compileJs('./build/experiments/', builtName, './dist.tools/experiments/', { @@ -623,9 +614,9 @@ function buildLoginDoneVersion(version, options) { function copyHandler(name, err) { if (err) { - return util.log(util.colors.red('copy error: ', err)); + return $$.util.log($$.util.colors.red('copy error: ', err)); } - util.log(util.colors.green('copied ' + name)); + $$.util.log($$.util.colors.green('copied ' + name)); } var path = 'extensions/amp-access/' + version + '/'; @@ -643,7 +634,7 @@ function buildLoginDoneVersion(version, options) { // Do not set watchers again when we get called by the watcher. var copy = Object.create(options); copy.watch = false; - gulpWatch(path + '/*', function() { + $$.watch(path + '/*', function() { buildLoginDoneVersion(version, copy); }); } @@ -655,16 +646,6 @@ function buildLoginDoneVersion(version, options) { '../../../dist/v0/amp-login-done-' + version + '.max.js', 'https://cdn.ampproject.org/v0/amp-login-done-' + version + '.js'); - function mkdirSync(path) { - try { - fs.mkdirSync(path); - } catch(e) { - if (e.code != 'EEXIST') { - throw e; - } - } - } - mkdirSync('dist'); mkdirSync('dist/v0'); @@ -677,7 +658,7 @@ function buildLoginDoneVersion(version, options) { var minifiedName = 'amp-login-done-' + version + '.js'; var latestName = 'amp-login-done-latest.js'; return gulp.src(path + '/*.js') - .pipe(file(builtName, js)) + .pipe($$.file(builtName, js)) .pipe(gulp.dest('build/all/v0/')) .on('end', function() { compileJs('./build/all/v0/', builtName, './dist/v0/', { @@ -703,6 +684,16 @@ function checkMinVersion() { } } +function mkdirSync(path) { + try { + fs.mkdirSync(path); + } catch(e) { + if (e.code != 'EEXIST') { + throw e; + } + } +} + /** * Gulp tasks diff --git a/package.json b/package.json index f4f29c4d65c2..4fa91e0e7386 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "gulp-if": "2.0.0", "gulp-image-diff": "0.3.0", "gulp-live-server": "0.0.29", + "gulp-load-plugins": "1.2.0", "gulp-rename": "1.2.2", "gulp-replace": "0.5.4", "gulp-sourcemaps": "1.5.2", @@ -77,6 +78,7 @@ "mocha": "2.3.3", "path": "0.12.7", "postcss": "5.0.14", + "postcss-import": "8.0.2", "pretty-bytes": "2.0.1", "request": "2.69.0", "rimraf": "2.5.1", diff --git a/spec/amp-cors-requests.md b/spec/amp-cors-requests.md index 549a160c80c2..84591fca387f 100644 --- a/spec/amp-cors-requests.md +++ b/spec/amp-cors-requests.md @@ -46,6 +46,6 @@ CORS endpoints receive requesting origin via "Origin" HTTP header. This header h Source origin restrictions has to be implemented by requiring "__amp_source_origin" URL parameter to be within a set of the Publisher's own origins. The "__amp_source_origin" parameter is passed from AMP Runtime in all fetch requests and contains the source origin, e.g. "https://publisher1.com". The resulting HTTP response has to also contain the following headers: - - `Access-Control-Allow-Origin: `. Here "origin" refers to the requesting origin that was allowed via "Origin" request header above. Ex: "https://cdn.ampproject.org". This is a CORS spec requirement. Notice that while CORS spec allows the value of '*' to be returned in this header, AMP strongly discourages use of '*'. Instead the value of the "Origin" header should be validated and echoed for improved security. + - `Access-Control-Allow-Origin: `. Here "origin" refers to the requesting origin that was allowed via "Origin" request header above. Ex: "https://cdn.ampproject.org". This is a CORS spec requirement. Notice that while CORS spec allows the value of '\*' to be returned in this header, AMP strongly discourages use of '\*'. Instead the value of the "Origin" header should be validated and echoed for improved security. - `AMP-Access-Control-Allow-Source-Origin: `. Here "source-origin" indicates the source origin that is allowed to read the authorization response as was verified via "__amp_source_origin" URL parameter. Ex: "https://publisher1.com". - `Access-Control-Expose-Headers: AMP-Access-Control-Allow-Source-Origin`. This header simply allows CORS response to contain the "AMP-Access-Control-Allow-Source-Origin" header. diff --git a/spec/amp-var-substitutions.md b/spec/amp-var-substitutions.md index 52e0959e25fd..82fa99447f25 100644 --- a/spec/amp-var-substitutions.md +++ b/spec/amp-var-substitutions.md @@ -247,6 +247,24 @@ For instance: ``` +### VIEWPORT_HEIGHT + +Provides the viewport height in pixels available for the page rendering. In contrast to `AVAILABLE_SCREEN_HEIGHT`, this value takes window size and zooming into account. + +For instance: +```html + +``` + +### VIEWPORT_WIDTH + +Provides the viewport width in pixels available for the page rendering. In contrast to `AVAILABLE_SCREEN_WIDTH`, this value takes window size and zooming into account. + +For instance: +```html + +``` + ## Miscellaneous ### CLIENT_ID diff --git a/src/3p-frame.js b/src/3p-frame.js index 0e9b9513bc15..bfbe613d25b6 100644 --- a/src/3p-frame.js +++ b/src/3p-frame.js @@ -20,7 +20,6 @@ import {getLengthNumeral} from '../src/layout'; import {getService} from './service'; import {documentInfoFor} from './document-info'; import {getMode} from './mode'; -import {isExperimentOn} from './experiments'; import {preconnectFor} from './preconnect'; import {dashToCamelCase} from './string'; import {parseUrl, assertHttpsUrl} from './url'; @@ -157,11 +156,11 @@ export function addDataAndJsonAttributes_(element, attributes) { export function prefetchBootstrap(window) { const url = getBootstrapBaseUrl(window); const preconnect = preconnectFor(window); - preconnect.prefetch(url); + preconnect.prefetch(url, 'document'); // While the URL may point to a custom domain, this URL will always be // fetched by it. preconnect.prefetch( - 'https://3p.ampproject.net/$internalRuntimeVersion$/f.js'); + 'https://3p.ampproject.net/$internalRuntimeVersion$/f.js', 'script'); } /** @@ -205,10 +204,6 @@ function getDefaultBootstrapBaseUrl(parentWindow) { * @visibleForTesting */ export function getSubDomain(win) { - if (!isExperimentOn(win, 'unique-origins')) { - return '3p'; - } - let rand; if (win.crypto && win.crypto.getRandomValues) { // By default use 2 32 bit integers. diff --git a/src/base-element.js b/src/base-element.js index 5df453dfe1f5..e678a8acd3ee 100644 --- a/src/base-element.js +++ b/src/base-element.js @@ -484,7 +484,7 @@ export class BaseElement { * Returns a previously measured layout box of the element. * @return {!LayoutRect} */ - getInsersectionElementLayoutBox() { + getIntersectionElementLayoutBox() { return this.resources_.getResourceForElement(this.element).getLayoutBox(); } diff --git a/src/custom-element.js b/src/custom-element.js index 0e73eec21433..b749250f9cec 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -692,7 +692,7 @@ export function createAmpElementProto(win, name, implementationClass) { * @final */ ElementProto.getIntersectionChangeEntry = function() { - const box = this.implementation_.getInsersectionElementLayoutBox(); + const box = this.implementation_.getIntersectionElementLayoutBox(); const rootBounds = this.implementation_.getViewport().getRect(); return getIntersectionChangeEntry( timer.now(), diff --git a/src/document-click.js b/src/document-click.js index 30a3957f04e4..52766bd5f975 100644 --- a/src/document-click.js +++ b/src/document-click.js @@ -17,8 +17,11 @@ import {closestByTag} from './dom'; import {getService} from './service'; import {log} from './log'; +import {historyFor} from './history'; import {parseUrl} from './url'; +import {viewerFor} from './viewer'; import {viewportFor} from './viewport'; +import {platform} from './platform'; /** @@ -58,21 +61,31 @@ export class ClickHandler { this.win = window; /** @private @const {!Viewport} */ - this.viewport_ = viewportFor(window); + this.viewport_ = viewportFor(this.win); - /** @private @const {!Function} */ - this.boundHandle_ = this.handle_.bind(this); + /** @private @const {!Viewer} */ + this.viewer_ = viewerFor(this.win); - this.win.document.documentElement.addEventListener('click', - this.boundHandle_); + /** @private @const {!History} */ + this.history_ = historyFor(this.win); + + // Only intercept clicks when iframed. + if (this.viewer_.isEmbedded() && this.viewer_.isOvertakeHistory()) { + /** @private @const {!function(!Event)|undefined} */ + this.boundHandle_ = this.handle_.bind(this); + this.win.document.documentElement.addEventListener( + 'click', this.boundHandle_); + } } /** * Removes all event listeners. */ cleanup() { - this.win.document.documentElement.removeEventListener('click', - this.boundHandle_); + if (this.boundHandle_) { + this.win.document.documentElement.removeEventListener( + 'click', this.boundHandle_); + } } /** @@ -81,7 +94,7 @@ export class ClickHandler { * @param {!Event} e */ handle_(e) { - onDocumentElementClick_(e, this.viewport_); + onDocumentElementClick_(e, this.viewport_, this.history_); } } @@ -89,10 +102,15 @@ export class ClickHandler { /** * Intercept any click on the current document and prevent any * linking to an identifier from pushing into the history stack. + * + * This also handles custom protocols (e.g. whatsapp://) when iframed + * on iOS Safari. + * * @param {!Event} e * @param {!Viewport} viewport + * @param {!History} history */ -export function onDocumentElementClick_(e, viewport) { +export function onDocumentElementClick_(e, viewport, history) { if (e.defaultPrevented) { return; } @@ -102,12 +120,25 @@ export function onDocumentElementClick_(e, viewport) { return; } - let elem = null; const docElement = e.currentTarget; const doc = docElement.ownerDocument; const win = doc.defaultView; const tgtLoc = parseUrl(target.href); + + // On Safari iOS, custom protocol links will fail to open apps when the + // document is iframed - in order to go around this, we set the top.location + // to the custom protocol href. + const isSafariIOS = platform.isIos() && platform.isSafari(); + const isEmbedded = win.parent && win.parent != win; + const isNormalProtocol = /^(https?|mailto):$/.test(tgtLoc.protocol); + if (isSafariIOS && isEmbedded && !isNormalProtocol) { + win.open(target.href, '_blank'); + // Without preventing default the page would should an alert error twice + // in the case where there's no app to handle the custom protocol. + e.preventDefault(); + } + if (!tgtLoc.hash) { return; } @@ -123,31 +154,46 @@ export function onDocumentElementClick_(e, viewport) { return; } + // Has the fragment actually changed? + if (tgtLoc.hash == curLoc.hash) { + return; + } + // We prevent default so that the current click does not push // into the history stack as this messes up the external documents // history which contains the amp document. e.preventDefault(); + // Look for the referenced element. const hash = tgtLoc.hash.slice(1); - elem = doc.getElementById(hash); - - if (!elem) { - // Fallback to anchor[name] if element with id is not found. - // Linking to an anchor element with name is obsolete in html5. - elem = doc.querySelector(`a[name=${hash}]`); + let elem = null; + if (hash) { + elem = doc.getElementById(hash); + if (!elem) { + // Fallback to anchor[name] if element with id is not found. + // Linking to an anchor element with name is obsolete in html5. + elem = doc.querySelector(`a[name=${hash}]`); + } } + // If possible do update the URL with the hash. As explained above + // we do `replace` to avoid messing with the container's history. + // The choice of `location.replace` vs `history.replaceState` is important. + // Due to bugs, not every browser triggers `:target` pseudo-class when + // `replaceState` is called. See http://www.zachleat.com/web/moving-target/ + // for more details. + win.location.replace(`#${hash}`); + + // Scroll to the element if found. if (elem) { - // TODO(dvoytenko): consider implementing animated scroll. viewport./*OK*/scrollIntoView(elem); } else { log.warn('documentElement', `failed to find element with id=${hash} or a[name=${hash}]`); } - const history = win.history; - // If possible do update the URL with the hash. As explained above - // we do replaceState to avoid messing with the container's history. - if (history.replaceState) { - history.replaceState(null, '', `#${hash}`); - } + + // Push/pop history. + history.push(() => { + win.location.replace(`${curLoc.hash || '#'}`); + }); }; diff --git a/src/dom.js b/src/dom.js index 9a261f8fc607..be407ecec1cc 100644 --- a/src/dom.js +++ b/src/dom.js @@ -111,6 +111,9 @@ export function closest(element, callback) { * @return {?Element} */ export function closestByTag(element, tagName) { + if (element.closest) { + return element.closest(tagName); + } tagName = tagName.toUpperCase(); return closest(element, el => { return el.tagName == tagName; @@ -126,7 +129,7 @@ export function closestByTag(element, tagName) { */ export function elementByTag(element, tagName) { const elements = element.getElementsByTagName(tagName); - return elements.length > 0 ? elements[0] : null; + return elements[0] || null; } @@ -137,32 +140,58 @@ export function elementByTag(element, tagName) { * @return {?Element} */ export function childElement(parent, callback) { - const children = parent.children; - for (let i = 0; i < children.length; i++) { - if (callback(children[i])) { - return children[i]; + for (let child = parent.firstElementChild; child; + child = child.nextElementSibling) { + if (callback(child)) { + return child; } } return null; } +/** + * @type {boolean|undefined} + * @visiblefortesting + */ +let scopeSelectorSupported; + +/** + * @param {boolean|undefined} val + * @visiblefortesting + */ +export function setScopeSelectorSupportedForTesting(val) { + scopeSelectorSupported = val; +} + +/** + * @return {boolean} + */ +function isScopeSelectorSupported() { + try { + document.querySelector(':scope'); + return true; + } catch (e) { + return false; + } +} /** - * Finds the first child element that has the specified attribute, optionally - * with a value. + * Finds the first child element that has the specified attribute. * @param {!Element} parent * @param {string} attr - * @param {string=} opt_value * @return {?Element} */ -export function childElementByAttr(parent, attr, opt_value) { +export function childElementByAttr(parent, attr) { + if (scopeSelectorSupported == null) { + scopeSelectorSupported = isScopeSelectorSupported(); + } + if (scopeSelectorSupported) { + return parent.querySelector(':scope > [' + attr + ']'); + } return childElement(parent, el => { if (!el.hasAttribute(attr)) { return false; } - if (opt_value !== undefined && el.getAttribute(attr) != opt_value) { - return false; - } return true; }); } @@ -175,6 +204,12 @@ export function childElementByAttr(parent, attr, opt_value) { * @return {?Element} */ export function childElementByTag(parent, tagName) { + if (scopeSelectorSupported == null) { + scopeSelectorSupported = isScopeSelectorSupported(); + } + if (scopeSelectorSupported) { + return parent.querySelector(':scope > ' + tagName); + } tagName = tagName.toUpperCase(); return childElement(parent, el => { return el.tagName == tagName; diff --git a/src/performance.js b/src/performance.js index daafae18581d..fc72a795f975 100644 --- a/src/performance.js +++ b/src/performance.js @@ -106,6 +106,7 @@ export class Performance { // Tick window.onload event. loadPromise(win).then(() => { this.tick('ol'); + this.flush(); }); } @@ -159,6 +160,7 @@ export class Performance { // time since the viewer initialized the timer) this.tick('pc'); } + this.flush(); }); } diff --git a/src/preconnect.js b/src/preconnect.js index cfe6707af952..0b033384cac7 100644 --- a/src/preconnect.js +++ b/src/preconnect.js @@ -28,7 +28,7 @@ import {platformFor} from './platform'; const ACTIVE_CONNECTION_TIMEOUT_MS = 180 * 1000; const PRECONNECT_TIMEOUT_MS = 10 * 1000; -class Preconnect { +export class Preconnect { /** * @param {!Window} win @@ -51,6 +51,9 @@ class Preconnect { this.platform_ = platformFor(win); // Mark current origin as preconnected. this.origins_[parseUrl(win.location.href).origin] = true; + + /** @private {boolean} */ + this.preloadSupported_ = this.isPreloadSupported_(); } /** @@ -109,20 +112,26 @@ class Preconnect { /** * Asks the browser to prefetch a URL. Always also does a preconnect * because browser support for that is better. + * * @param {string} url + * @param {string=} opt_preloadAs */ - prefetch(url) { + prefetch(url, opt_preloadAs) { if (!this.isInterestingUrl_(url)) { return; } if (this.urls_[url]) { return; } + const command = this.preloadSupported_ ? 'preload' : 'prefetch'; this.urls_[url] = true; this.url(url, /* opt_alsoConnecting */ true); const prefetch = document.createElement('link'); - prefetch.setAttribute('rel', 'prefetch'); + prefetch.setAttribute('rel', command); prefetch.setAttribute('href', url); + if (opt_preloadAs) { + prefetch.setAttribute('as', opt_preloadAs); + } this.head_.appendChild(prefetch); // As opposed to preconnect we do not clean this tag up, because there is // no expectation as to it having an immediate effect. @@ -135,6 +144,17 @@ class Preconnect { return false; } + /** @private */ + isPreloadSupported_() { + const tokenList = document.createElement('link').relList; + if (!tokenList || !tokenList.supports) { + this.preloadSupported_ = false; + return this.preloadSupported_; + } + this.preloadSupported_ = tokenList.supports('preload'); + return this.preloadSupported_; + } + /** * Safari does not support preconnecting, but due to its significant * performance benefits we implement this crude polyfill. diff --git a/src/runtime.js b/src/runtime.js index f427f099f7c5..543c6397e5fa 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -57,9 +57,8 @@ export function adopt(global) { * Registers an extended element and installs its styles. * @param {string} name * @param {!Function} implementationClass - * @param {string=} opt_css Optional CSS to install with the component. Use - * the special variable $CSS$ in your code. It will be replaced with the - * CSS file associated with the element. + * @param {string=} opt_css Optional CSS to install with the component. + * Typically imported from generated CSS-in-JS file for each component. */ global.AMP.registerElement = function(name, implementationClass, opt_css) { const register = function() { diff --git a/src/service/fixed-layer.js b/src/service/fixed-layer.js index a26633e19669..7b22dbd7607d 100644 --- a/src/service/fixed-layer.js +++ b/src/service/fixed-layer.js @@ -238,7 +238,7 @@ export class FixedLayer { } }); }, - }).catch(error => { + }, {}).catch(error => { // Fail silently. setTimeout(() => {throw error;}); }); diff --git a/src/service/framerate-impl.js b/src/service/framerate-impl.js index 4e5bcca021a4..32292b471e97 100644 --- a/src/service/framerate-impl.js +++ b/src/service/framerate-impl.js @@ -127,9 +127,12 @@ export class Framerate { const duration = now - this.collectStartTime_; const framerate = 1000 / (duration / this.frameCount_); const performance = performanceFor(this.win); - performance.tickDelta('fps', framerate); + // We want good values to be low and CSI hates negative values, so we + // shift everything by 60. + const reportedValue = Math.max(60 - framerate, 0); + performance.tickDelta('fps', reportedValue); if (this.loadedAd_) { - performance.tickDelta('fal', framerate); + performance.tickDelta('fal', reportedValue); } performance.flush(); this.reset_(); diff --git a/src/service/resources-impl.js b/src/service/resources-impl.js index 03f3759fa765..6cdb2a4023d2 100644 --- a/src/service/resources-impl.js +++ b/src/service/resources-impl.js @@ -756,7 +756,7 @@ export class Resources { (newScrollHeight - state./*OK*/scrollHeight)); } }, - }); + }, {}); } } } @@ -1689,7 +1689,6 @@ export class Resource { this.layoutPromise_ = promise.then(() => this.layoutComplete_(true), reason => this.layoutComplete_(false, reason)); - this.layoutPromise_.then(this.whenFirstLayoutCompleteResolve_); return this.layoutPromise_; } diff --git a/src/service/viewer-impl.js b/src/service/viewer-impl.js index 5be3d3e915d2..71d9e5a6abf3 100644 --- a/src/service/viewer-impl.js +++ b/src/service/viewer-impl.js @@ -15,7 +15,7 @@ */ import {Observable} from '../observable'; -import {assert, assertEnumValue} from '../asserts'; +import {assertEnumValue} from '../asserts'; import {documentStateFor} from '../document-state'; import {getMode} from '../mode'; import {getService} from '../service'; @@ -270,48 +270,36 @@ export class Viewer { // Wait for document to become visible. this.docState_.onVisibilityChanged(this.recheckVisibilityState_.bind(this)); - - /** - * Creates an error for the case where a channel cannot be established. - * @param {!Error|undefined} reason - * @return {!Error} - */ - function getChannelError(reason) { - if (reason instanceof Error) { - reason.message = 'No messaging channel: ' + reason.message; - return reason; - } - return new Error('No messaging channel: ' + reason); - } - /** * This promise will resolve when communications channel has been - * established or timeout in 5 seconds. The timeout is needed to avoid + * established or timeout in 20 seconds. The timeout is needed to avoid * this promise becoming a memory leak with accumulating undelivered - * messages. - * @private @const {!Promise} + * messages. The promise is only available when the document is embedded. + * @private @const {?Promise} */ - this.messagingReadyPromise_ = timer.timeoutPromise( - 20000, - new Promise(resolve => { - /** @private @const {function(!Viewer)} */ - this.messagingReadyResolver_ = resolve; - })).catch(reason => { - throw getChannelError(reason); - }); + this.messagingReadyPromise_ = this.isEmbedded_ ? + timer.timeoutPromise( + 20000, + new Promise(resolve => { + /** @private @const {function()|undefined} */ + this.messagingReadyResolver_ = resolve; + })).catch(reason => { + throw getChannelError(reason); + }) : null; /** * A promise for non-essential messages. These messages should not fail * if there's no messaging channel set up. But ideally viewer would try to - * deliver if at all possible. - * @private @const {!Promise} + * deliver if at all possible. This promise is only available when the + * document is embedded. + * @private @const {?Promise} */ - this.messagingMaybePromise_ = this.messagingReadyPromise_.catch(reason => { - if (this.isEmbedded_) { - // Don't fail promise, but still report. - reportError(getChannelError(reason)); - } - }); + this.messagingMaybePromise_ = this.isEmbedded_ ? + this.messagingReadyPromise_ + .catch(reason => { + // Don't fail promise, but still report. + reportError(getChannelError(reason)); + }) : null; // Trusted viewer and referrer. let trustedViewerResolved; @@ -842,12 +830,18 @@ export class Viewer { * @export */ setMessageDeliverer(deliverer, origin) { - assert(!this.messageDeliverer_, 'message deliverer can only be set once'); + if (this.messageDeliverer_) { + throw new Error('message channel can only be initialized once'); + } + if (!origin) { + throw new Error('message channel must have an origin'); + } log.fine(TAG_, 'message channel established with origin: ', origin); this.messageDeliverer_ = deliverer; - this.messagingReadyResolver_(this); - // TODO(dvoytenko, #1764): Make `origin` required when viewers catch up. this.messagingOrigin_ = origin; + if (this.messagingReadyResolver_) { + this.messagingReadyResolver_(); + } if (this.trustedViewerResolver_) { this.trustedViewerResolver_( origin ? this.isTrustedViewerOrigin_(origin) : false); @@ -878,6 +872,9 @@ export class Viewer { * @return {!Promise<*>|undefined} */ sendMessage(eventType, data, awaitResponse) { + if (!this.messagingReadyPromise_) { + return Promise.reject(getChannelError()); + } return this.messagingReadyPromise_.then(() => { return this.sendMessageUnreliable_(eventType, data, awaitResponse); }); @@ -946,6 +943,10 @@ export class Viewer { * @private */ maybeSendMessage_(eventType, data) { + if (!this.messagingMaybePromise_) { + // Messaging is not expected. + return; + } this.messagingMaybePromise_.then(() => { if (this.messageDeliverer_) { this.sendMessageUnreliable_(eventType, data, false); @@ -964,7 +965,7 @@ export class Viewer { * @param {!Object} allParams * @private */ -export function parseParams_(str, allParams) { +function parseParams_(str, allParams) { const params = parseQueryString(str); for (const k in params) { allParams[k] = params[k]; @@ -972,6 +973,20 @@ export function parseParams_(str, allParams) { } +/** + * Creates an error for the case where a channel cannot be established. + * @param {!Error=} opt_reason + * @return {!Error} + */ +function getChannelError(opt_reason) { + if (opt_reason instanceof Error) { + opt_reason.message = 'No messaging channel: ' + opt_reason.message; + return opt_reason; + } + return new Error('No messaging channel: ' + opt_reason); +} + + /** * @typedef {{ * newStackIndex: number diff --git a/src/service/viewport-impl.js b/src/service/viewport-impl.js index 4d1ff7007c2e..475e0eba0ec2 100644 --- a/src/service/viewport-impl.js +++ b/src/service/viewport-impl.js @@ -126,9 +126,7 @@ export class Viewport { if (paddingTop != this.paddingTop_) { this.paddingTop_ = paddingTop; this.binding_.updatePaddingTop(this.paddingTop_); - if (this.fixedLayer_) { - this.fixedLayer_.updatePaddingTop(this.paddingTop_); - } + this.fixedLayer_.updatePaddingTop(this.paddingTop_); } }); this.binding_.updateViewerViewport(this.viewer_); @@ -346,27 +344,21 @@ export class Viewport { * Hides the fixed layer. */ hideFixedLayer() { - if (this.fixedLayer_) { - this.fixedLayer_.setVisible(false); - } + this.fixedLayer_.setVisible(false); } /** * Shows the fixed layer. */ showFixedLayer() { - if (this.fixedLayer_) { - this.fixedLayer_.setVisible(true); - } + this.fixedLayer_.setVisible(true); } /** * Updates the fixed layer. */ updatedFixedLayer() { - if (this.fixedLayer_) { - this.fixedLayer_.update(); - } + this.fixedLayer_.update(); } /** @@ -374,9 +366,7 @@ export class Viewport { * @param {!Element} element */ addToFixedLayer(element) { - if (this.fixedLayer_) { - this.fixedLayer_.addElement(element); - } + this.fixedLayer_.addElement(element); } /** @@ -384,9 +374,7 @@ export class Viewport { * @param {!Element} element */ removeFromFixedLayer(element) { - if (this.fixedLayer_) { - this.fixedLayer_.removeElement(element); - } + this.fixedLayer_.removeElement(element); } /** @@ -503,13 +491,9 @@ export class Viewport { const oldSize = this.size_; this.size_ = null; // Need to recalc. const newSize = this.getSize(); - if (this.fixedLayer_) { - this.fixedLayer_.update().then(() => { - this.changed_(!oldSize || oldSize.width != newSize.width, 0); - }); - } else { + this.fixedLayer_.update().then(() => { this.changed_(!oldSize || oldSize.width != newSize.width, 0); - } + }); } } diff --git a/src/service/vsync-impl.js b/src/service/vsync-impl.js index b726fe0fa8f3..11d1135ca82d 100644 --- a/src/service/vsync-impl.js +++ b/src/service/vsync-impl.js @@ -69,18 +69,40 @@ export class Vsync { */ this.tasks_ = []; + /** + * Double buffer for tasks. + * @private {!Array} + */ + this.nextTasks_ = []; + /** * States for tasks in the next frame in the same order. * @private {!Array} */ this.states_ = []; + /** + * Double buffer for states. + * @private {!Array} + */ + this.nextStates_ = []; + /** * Whether a new animation frame has been scheduled. * @private {boolean} */ this.scheduled_ = false; + /** + * @private {?Promise} + */ + this.nextFramePromise_ = null; + + /** + * @private {?function()} + */ + this.nextFrameResolver_ = null; + /** @const {!Function} */ this.boundRunScheduledTasks_ = this.runScheduledTasks_.bind(this); @@ -103,11 +125,11 @@ export class Vsync { * will be undefined. * * @param {!VsyncTaskSpecDef} task - * @param {!VsyncStateDef=} opt_state + * @param {VsyncStateDef=} opt_state */ run(task, opt_state) { this.tasks_.push(task); - this.states_.push(opt_state || {}); + this.states_.push(opt_state); this.schedule_(); } @@ -123,16 +145,12 @@ export class Vsync { * @return {!Promise} */ runPromise(task, opt_state) { - return new Promise(resolve => { - this.run({ - measure: state => { - task.measure(state); - }, - mutate: state => { - task.mutate(state); - resolve(); - }, - }, opt_state); + this.run(task, opt_state); + if (this.nextFramePromise_) { + return this.nextFramePromise_; + } + return this.nextFramePromise_ = new Promise(resolve => { + this.nextFrameResolver_ = resolve; }); } @@ -152,7 +170,10 @@ export class Vsync { * @param {function()} mutator */ mutate(mutator) { - this.run({mutate: mutator}); + this.run({ + measure: undefined, // For uniform hidden class. + mutate: mutator, + }); } /** @@ -161,11 +182,9 @@ export class Vsync { * @return {!Promise} */ mutatePromise(mutator) { - return new Promise(resolve => { - this.mutate(() => { - mutator(); - resolve(); - }); + return this.runPromise({ + measure: undefined, + mutate: mutator, }); } @@ -174,7 +193,10 @@ export class Vsync { * @param {function()} measurer */ measure(measurer) { - this.run({measure: measurer}); + this.run({ + measure: measurer, + mutate: undefined, // For uniform hidden class. + }); } /** @@ -294,11 +316,14 @@ export class Vsync { */ runScheduledTasks_() { this.scheduled_ = false; - // TODO(malteubl) Avoid array allocation with a double buffer. const tasks = this.tasks_; const states = this.states_; - this.tasks_ = []; - this.states_ = []; + const resolver = this.nextFrameResolver_; + this.nextFrameResolver_ = null; + this.nextFramePromise_ = null; + // Double buffering + this.tasks_ = this.nextTasks_; + this.states_ = this.nextStates_; for (let i = 0; i < tasks.length; i++) { if (tasks[i].measure) { tasks[i].measure(states[i]); @@ -309,6 +334,14 @@ export class Vsync { tasks[i].mutate(states[i]); } } + // Swap last arrays into double buffer. + this.nextTasks_ = tasks; + this.nextStates_ = states; + this.nextTasks_.length = 0; + this.nextStates_.length = 0; + if (resolver) { + resolver(); + } } /** diff --git a/src/url-replacements.js b/src/url-replacements.js index 71bb4a33c989..d50586e8ff22 100644 --- a/src/url-replacements.js +++ b/src/url-replacements.js @@ -21,7 +21,6 @@ import {documentInfoFor} from './document-info'; import {getMode} from './mode'; import {getService} from './service'; import {loadPromise} from './event-helper'; -import {log} from './log'; import {getSourceUrl, parseUrl, removeFragment, parseQueryString} from './url'; import {viewerFor} from './viewer'; import {viewportFor} from './viewport'; @@ -29,8 +28,6 @@ import {vsyncFor} from './vsync'; import {userNotificationManagerFor} from './user-notification'; import {activityFor} from './activity'; -/** @private {string} */ -const TAG_ = 'UrlReplacements'; /** * This class replaces substitution variables with their values. @@ -201,6 +198,18 @@ class UrlReplacements { return this.win_.screen.colorDepth; }); + // Returns the viewport height. + this.set_('VIEWPORT_HEIGHT', () => { + return vsyncFor(this.win_).measurePromise( + () => viewportFor(this.win_).getSize().height); + }); + + // Returns the viewport width. + this.set_('VIEWPORT_WIDTH', () => { + return vsyncFor(this.win_).measurePromise( + () => viewportFor(this.win_).getSize().width); + }); + // Returns document characterset. this.set_('DOCUMENT_CHARSET', () => { const doc = this.win_.document; @@ -268,9 +277,7 @@ class UrlReplacements { this.set_('AUTHDATA', field => { assert(field, 'The first argument to AUTHDATA, the field, is required'); return this.getAccessValue_(accessService => { - return accessService.whenFirstAuthorized().then(() => { - return accessService.getAuthdataField(field); - }); + return accessService.getAuthdataField(field); }, 'AUTHDATA'); }); @@ -378,15 +385,28 @@ class UrlReplacements { } const binding = (opt_bindings && (name in opt_bindings)) ? opt_bindings[name] : this.getReplacement_(name); - const val = (typeof binding == 'function') ? - binding.apply(null, args) : binding; + let val; + try { + val = (typeof binding == 'function') ? + binding.apply(null, args) : binding; + } catch (e) { + // Report error, but do not disrupt URL replacement. This will + // interpolate as the empty string. + setTimeout(() => { + throw e; + }); + } // In case the produced value is a promise, we don't actually // replace anything here, but do it again when the promise resolves. if (val && val.then) { - const p = val.then(v => { + const p = val.catch(err => { + // Report error, but do not disrupt URL replacement. This will + // interpolate as the empty string. + setTimeout(() => { + throw err; + }); + }).then(v => { url = url.replace(match, encodeValue(v)); - }, err => { - log.error(TAG_, 'Failed to expand: ' + name, err); }); if (replacementPromise) { replacementPromise = replacementPromise.then(() => p); diff --git a/test/fixtures/doubleclick.html b/test/fixtures/doubleclick.html index 6367084d097a..551d47d5e35d 100644 --- a/test/fixtures/doubleclick.html +++ b/test/fixtures/doubleclick.html @@ -3,7 +3,7 @@ We can render an ad - + diff --git a/test/functional/test-3p-environment.js b/test/functional/test-3p-environment.js index 24655e7c53ed..d941b25747fb 100644 --- a/test/functional/test-3p-environment.js +++ b/test/functional/test-3p-environment.js @@ -255,6 +255,16 @@ describe('3p environment', () => { if (win.webkitRequestAnimationFrame) { expect(win.webkitRequestAnimationFrame).to.not.match(/native/); } + expect(win.alert.toString()).to.not.match(/native/); + expect(win.prompt.toString()).to.not.match(/native/); + expect(win.confirm.toString()).to.not.match(/native/); + expect(win.alert()).to.be.undefined; + expect(win.prompt()).to.equal(''); + expect(win.confirm()).to.be.false; + // We only allow 3 calls to these functions. + expect(() => win.alert()).to.throw(/security error/); + expect(() => win.prompt()).to.throw(/security error/); + expect(() => win.confirm()).to.throw(/security error/); } function waitForMutationObserver(iframe) { diff --git a/test/functional/test-3p-frame.js b/test/functional/test-3p-frame.js index 1bff0ca88d2c..17120e40ddc8 100644 --- a/test/functional/test-3p-frame.js +++ b/test/functional/test-3p-frame.js @@ -22,7 +22,6 @@ import {loadPromise} from '../../src/event-helper'; import {setModeForTesting} from '../../src/mode'; import {resetServiceForTesting} from '../../src/service'; import {viewerFor} from '../../src/viewer'; -import {toggleExperiment} from '../../src/experiments'; describe('3p-frame', () => { @@ -31,7 +30,6 @@ describe('3p-frame', () => { }); afterEach(() => { - toggleExperiment(window, 'unique-origins', false); resetServiceForTesting(window, 'bootstrapBaseUrl'); setModeForTesting(null); const m = document.querySelector( @@ -152,15 +150,8 @@ describe('3p-frame', () => { 'http://ads.localhost:9876/dist.3p/current/frame.max.html'); }); - it('should pick the right bootstrap url (prod)', () => { - setModeForTesting({}); - expect(getBootstrapBaseUrl(window)).to.equal( - 'https://3p.ampproject.net/$internalRuntimeVersion$/frame.html'); - }); - it('should pick the right bootstrap unique url (prod)', () => { setModeForTesting({}); - toggleExperiment(window, 'unique-origins', true); expect(getBootstrapBaseUrl(window)).to.match( /^https:\/\/d-\d+\.ampproject\.net\/\$\internal\w+\$\/frame\.html$/); }); @@ -192,22 +183,18 @@ describe('3p-frame', () => { expect(fetches).to.have.length(2); expect(fetches[0].href).to.equal( 'http://ads.localhost:9876/dist.3p/current/frame.max.html'); + expect(fetches[0].getAttribute('as')).to.equal('document'); expect(fetches[1].href).to.equal( 'https://3p.ampproject.net/$internalRuntimeVersion$/f.js'); - }); - - it('should make sub domain: 3p', () => { - expect(getSubDomain(window)).to.equal('3p'); + expect(fetches[1].getAttribute('as')).to.equal('script'); }); it('should make sub domains (unique)', () => { - toggleExperiment(window, 'unique-origins', true); expect(getSubDomain(window)).to.match(/^d-\d+$/); expect(getSubDomain(window)).to.not.equal('d-00'); }); it('should make sub domains (Math)', () => { - toggleExperiment(window, 'unique-origins', true); const fakeWin = { document: document, Math: Math, @@ -216,7 +203,6 @@ describe('3p-frame', () => { }); it('should make sub domains (crypto)', () => { - toggleExperiment(window, 'unique-origins', true); const fakeWin = { document: document, crypto: { @@ -230,7 +216,6 @@ describe('3p-frame', () => { }); it('should make sub domains (fallback)', () => { - toggleExperiment(window, 'unique-origins', true); const fakeWin = { document: document, Math: { diff --git a/test/functional/test-amp-ad.js b/test/functional/test-amp-ad.js index cf1e29180012..9ada724cda47 100644 --- a/test/functional/test-amp-ad.js +++ b/test/functional/test-amp-ad.js @@ -21,7 +21,7 @@ import {installEmbed} from '../../builtins/amp-embed'; import {installCidService} from '../../src/service/cid-impl'; import { installUserNotificationManager, -} from '../../build/all/v0/amp-user-notification-0.1.max'; +} from '../../extensions/amp-user-notification/0.1/amp-user-notification'; import {markElementScheduledForTesting} from '../../src/custom-element'; import {setCookie} from '../../src/cookies'; import * as sinon from 'sinon'; @@ -291,6 +291,33 @@ function tests(name, installer) { expect(ad.style.display).to.equal('none'); }); }); + + it('should hide placeholder when ad falls back', () => { + return getAd({ + width: 300, + height: 750, + type: 'a9', + src: 'testsrc', + }, 'https://schema.org', ad => { + const placeholder = document.createElement('div'); + placeholder.setAttribute('placeholder', ''); + ad.appendChild(placeholder); + expect(placeholder.classList.contains('amp-hidden')).to.be.false; + + const fallback = document.createElement('div'); + fallback.setAttribute('fallback', ''); + ad.appendChild(fallback); + return ad; + }).then(ad => { + const placeholderEl = ad.querySelector('[placeholder]'); + sandbox.stub( + ad.implementation_, 'deferMutate', function(callback) { + callback(); + }); + ad.implementation_.noContentHandler_(); + expect(placeholderEl.classList.contains('amp-hidden')).to.be.true; + }); + }); }); describe('cid-ad support', () => { @@ -370,8 +397,8 @@ function tests(name, installer) { }); describe('renderOutsideViewport', () => { - function getGoodAd(cb, layoutCb) { - return getAd({ + function getGoodAd(cb, layoutCb, opt_loadingStrategy) { + const attributes = { width: 300, height: 250, type: 'a9', @@ -381,7 +408,11 @@ function tests(name, installer) { 'data-aax_src': '302', // Test precedence 'data-width': '6666', - }, 'https://schema.org', element => { + }; + if (opt_loadingStrategy) { + attributes['data-loading-strategy'] = opt_loadingStrategy; + } + return getAd(attributes, 'https://schema.org', element => { cb(element.implementation_); return element; }, layoutCb); @@ -402,6 +433,40 @@ function tests(name, installer) { expect(ad.renderOutsideViewport()).to.be.true; }); }); + + it('should prefer-viewability-over-views', () => { + let clock; + const elementBox = { + top: 4000, + }; + const viewportRect = { + height: 1000, + bottom: 1000, + }; + return getGoodAd(ad => { + ad.resources_.add(ad.element); + sandbox.stub(ad, 'getIntersectionElementLayoutBox', () => { + return elementBox; + }); + sandbox.stub(ad.getViewport(), 'getRect', () => { + return viewportRect; + }); + }, () => { + clock = sandbox.useFakeTimers(); + }, 'prefer-viewability-over-views').then(ad => { + clock.tick(10000); + // False because we just rendered one. + expect(ad.renderOutsideViewport()).to.be.false; + viewportRect.bottom = '2749'; + expect(ad.renderOutsideViewport()).to.be.false; + // 125% of viewport away + viewportRect.bottom = '2750'; + expect(ad.renderOutsideViewport()).to.be.true; + // We currently render above viewport. + viewportRect.bottom = '6000'; + expect(ad.renderOutsideViewport()).to.be.true; + }); + }); }); }; } diff --git a/test/functional/test-custom-element.js b/test/functional/test-custom-element.js index db056a98f313..964629725328 100644 --- a/test/functional/test-custom-element.js +++ b/test/functional/test-custom-element.js @@ -73,7 +73,7 @@ describe('CustomElement', () => { viewportCallback(inViewport) { testElementViewportCallback(inViewport); } - getInsersectionElementLayoutBox() { + getIntersectionElementLayoutBox() { testElementGetInsersectionElementLayoutBox(); return {top: 10, left: 10, width: 11, height: 1}; } diff --git a/test/functional/test-document-click.js b/test/functional/test-document-click.js index f528a54bcf95..a29de0518866 100644 --- a/test/functional/test-document-click.js +++ b/test/functional/test-document-click.js @@ -15,11 +15,15 @@ */ import {onDocumentElementClick_} from '../../src/document-click'; +import {platform} from '../../src/platform'; +import * as sinon from 'sinon'; describe('test-document-click onDocumentElementClick_', () => { + let sandbox; let evt; let doc; let win; + let history; let tgt; let elem; let docElem; @@ -27,16 +31,17 @@ describe('test-document-click onDocumentElementClick_', () => { let preventDefaultSpy; let scrollIntoViewSpy; let querySelectorSpy; - let replaceStateSpy; + let replaceLocSpy; let viewport; beforeEach(() => { - preventDefaultSpy = sinon.spy(); - scrollIntoViewSpy = sinon.spy(); - replaceStateSpy = sinon.spy(); + sandbox = sinon.sandbox.create(); + preventDefaultSpy = sandbox.spy(); + scrollIntoViewSpy = sandbox.spy(); + replaceLocSpy = sandbox.spy(); elem = {}; - getElementByIdSpy = sinon.stub(); - querySelectorSpy = sinon.stub(); + getElementByIdSpy = sandbox.stub(); + querySelectorSpy = sandbox.stub(); tgt = document.createElement('a'); tgt.href = 'https://www.google.com'; doc = { @@ -45,9 +50,7 @@ describe('test-document-click onDocumentElementClick_', () => { defaultView: { location: { href: 'https://www.google.com/some-path?hello=world#link', - }, - history: { - replaceState: replaceStateSpy, + replace: replaceLocSpy, }, }, }; @@ -63,18 +66,14 @@ describe('test-document-click onDocumentElementClick_', () => { viewport = { scrollIntoView: scrollIntoViewSpy, }; + history = { + push: () => {}, + }; }); afterEach(() => { - evt = null; - doc = null; - tgt = null; - doc = null; - docElem = null; - getElementByIdSpy = null; - preventDefaultSpy = null; - scrollIntoViewSpy = null; - querySelectorSpy = null; + sandbox.restore(); + sandbox = null; }); describe('when linking to a different origin or path', () => { @@ -85,7 +84,7 @@ describe('test-document-click onDocumentElementClick_', () => { it('should not do anything on path change', () => { tgt.href = 'https://www.google.com/some-other-path'; - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(0); expect(querySelectorSpy.callCount).to.equal(0); @@ -95,7 +94,7 @@ describe('test-document-click onDocumentElementClick_', () => { it('should not do anything on origin change', () => { tgt.href = 'https://maps.google.com/some-path#link'; - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(0); expect(querySelectorSpy.callCount).to.equal(0); @@ -105,7 +104,7 @@ describe('test-document-click onDocumentElementClick_', () => { it('should not do anything when there is no hash', () => { tgt.href = 'https://www.google.com/some-path'; - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(0); expect(querySelectorSpy.callCount).to.equal(0); @@ -115,7 +114,7 @@ describe('test-document-click onDocumentElementClick_', () => { it('should not do anything on a query change', () => { tgt.href = 'https://www.google.com/some-path?hello=foo#link'; - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(0); expect(querySelectorSpy.callCount).to.equal(0); @@ -134,7 +133,7 @@ describe('test-document-click onDocumentElementClick_', () => { it('should call getElementById on document', () => { getElementByIdSpy.returns(elem); expect(getElementByIdSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(1); expect(querySelectorSpy.callCount).to.equal(0); }); @@ -143,13 +142,13 @@ describe('test-document-click onDocumentElementClick_', () => { getElementByIdSpy.returns(null); querySelectorSpy.returns(null); expect(preventDefaultSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(preventDefaultSpy.callCount).to.equal(1); }); it('should not do anything if no anchor is found', () => { evt.target = document.createElement('span'); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(0); expect(querySelectorSpy.callCount).to.equal(0); }); @@ -158,7 +157,7 @@ describe('test-document-click onDocumentElementClick_', () => { 'found', () => { getElementByIdSpy.returns(null); expect(getElementByIdSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(1); expect(querySelectorSpy.callCount).to.equal(1); }); @@ -169,32 +168,148 @@ describe('test-document-click onDocumentElementClick_', () => { querySelectorSpy.returns(null); expect(getElementByIdSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(getElementByIdSpy.callCount).to.equal(1); expect(scrollIntoViewSpy.callCount).to.equal(0); - expect(replaceStateSpy.callCount).to.equal(1); - expect(replaceStateSpy.args[0][2]).to.equal('#test'); + expect(replaceLocSpy.callCount).to.equal(1); + expect(replaceLocSpy.args[0][0]).to.equal('#test'); }); it('should call scrollIntoView if element with id is found', () => { getElementByIdSpy.returns(elem); + expect(replaceLocSpy.callCount).to.equal(0); expect(scrollIntoViewSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(scrollIntoViewSpy.callCount).to.equal(1); - expect(replaceStateSpy.callCount).to.equal(1); - expect(replaceStateSpy.args[0][2]).to.equal('#test'); + expect(replaceLocSpy.callCount).to.equal(1); + expect(replaceLocSpy.args[0][0]).to.equal('#test'); }); it('should call scrollIntoView if element with name is found', () => { getElementByIdSpy.returns(null); querySelectorSpy.returns(elem); + expect(replaceLocSpy.callCount).to.equal(0); expect(scrollIntoViewSpy.callCount).to.equal(0); - onDocumentElementClick_(evt, viewport); + onDocumentElementClick_(evt, viewport, history); expect(scrollIntoViewSpy.callCount).to.equal(1); - expect(replaceStateSpy.callCount).to.equal(1); - expect(replaceStateSpy.args[0][2]).to.equal('#test'); + expect(replaceLocSpy.callCount).to.equal(1); + expect(replaceLocSpy.args[0][0]).to.equal('#test'); + }); + + it('should call location.replace before scrollIntoView', () => { + getElementByIdSpy.returns(null); + querySelectorSpy.returns(elem); + + const ops = []; + win.location.replace = () => { + ops.push('location.replace'); + }; + viewport.scrollIntoView = () => { + ops.push('scrollIntoView'); + }; + onDocumentElementClick_(evt, viewport, history); + + expect(ops).to.have.length(2); + expect(ops[0]).to.equal('location.replace'); + expect(ops[1]).to.equal('scrollIntoView'); + }); + + it('should push and pop history state', () => { + let historyOnPop; + const historyPushStub = sandbox.stub(history, 'push', onPop => { + historyOnPop = onPop; + }); + + // Click -> push. + onDocumentElementClick_(evt, viewport, history); + expect(scrollIntoViewSpy.callCount).to.equal(0); + expect(replaceLocSpy.callCount).to.equal(1); + expect(replaceLocSpy.args[0][0]).to.equal('#test'); + expect(historyPushStub.callCount).to.equal(1); + expect(historyOnPop).to.exist; + + // Pop. + historyOnPop(); + expect(replaceLocSpy.callCount).to.equal(2); + expect(replaceLocSpy.args[1][0]).to.equal('#'); + }); + + it('should push and pop history state with pre-existing hash', () => { + win.location.href = 'https://www.google.com/some-path?hello=world#first'; + let historyOnPop; + const historyPushStub = sandbox.stub(history, 'push', onPop => { + historyOnPop = onPop; + }); + + // Click -> push. + onDocumentElementClick_(evt, viewport, history); + expect(historyPushStub.callCount).to.equal(1); + expect(replaceLocSpy.callCount).to.equal(1); + expect(replaceLocSpy.args[0][0]).to.equal('#test'); + + // Pop. + historyOnPop(); + expect(replaceLocSpy.callCount).to.equal(2); + expect(replaceLocSpy.args[1][0]).to.equal('#first'); + }); + }); + + describe('when linking to custom protocols e.g. whatsapp:', () => { + beforeEach(() => { + win.open = sandbox.spy(); + win.parent = {}; + win.top = { + location: { + href: 'https://google.com', + }, + }; + tgt.href = 'whatsapp://send?text=hello'; + }); + + it('should set top.location.href on Safari iOS when embedded', () => { + sandbox.stub(platform, 'isIos').returns(true); + sandbox.stub(platform, 'isSafari').returns(true); + onDocumentElementClick_(evt, viewport, history); + expect(win.open.called).to.be.true; + expect(win.open.calledWith( + 'whatsapp://send?text=hello', '_blank')).to.be.true; + expect(preventDefaultSpy.callCount).to.equal(1); + }); + + it('should not do anything for mailto: protocol', () => { + tgt.href = 'mailto:hello@example.com'; + sandbox.stub(platform, 'isIos').returns(true); + sandbox.stub(platform, 'isSafari').returns(true); + onDocumentElementClick_(evt, viewport, history); + expect(win.open.called).to.be.false; + expect(preventDefaultSpy.callCount).to.equal(0); + }); + + it('should not do anything on other non-safari iOS', () => { + sandbox.stub(platform, 'isIos').returns(true); + sandbox.stub(platform, 'isSafari').returns(false); + onDocumentElementClick_(evt, viewport, history); + expect(win.open.called).to.be.false; + expect(preventDefaultSpy.callCount).to.equal(0); + }); + + it('should not do anything on other platforms', () => { + sandbox.stub(platform, 'isIos').returns(false); + sandbox.stub(platform, 'isSafari').returns(false); + onDocumentElementClick_(evt, viewport, history); + expect(win.top.location.href).to.equal('https://google.com'); + expect(preventDefaultSpy.callCount).to.equal(0); + }); + + it('should not do anything if not embedded', () => { + sandbox.stub(platform, 'isIos').returns(true); + sandbox.stub(platform, 'isSafari').returns(true); + win.parent = undefined; + onDocumentElementClick_(evt, viewport, history); + expect(win.open.called).to.be.false; + expect(preventDefaultSpy.callCount).to.equal(0); }); }); }); diff --git a/test/functional/test-dom.js b/test/functional/test-dom.js index 84d801ba5b0d..fd8888266eb6 100644 --- a/test/functional/test-dom.js +++ b/test/functional/test-dom.js @@ -19,6 +19,10 @@ import * as dom from '../../src/dom'; describe('DOM', () => { + afterEach(() => { + dom.setScopeSelectorSupportedForTesting(undefined); + }); + it('should remove all children', () => { const element = document.createElement('div'); element.appendChild(document.createElement('div')); @@ -113,7 +117,7 @@ describe('DOM', () => { .to.be.null; }); - it('childElementByTag should find first match', () => { + function testChildElementByTag() { const parent = document.createElement('parent'); const element1 = document.createElement('element1'); @@ -122,12 +126,23 @@ describe('DOM', () => { const element2 = document.createElement('element2'); parent.appendChild(element2); + const element3 = document.createElement('element3'); + element1.appendChild(element3); + expect(dom.childElementByTag(parent, 'element1')).to.equal(element1); expect(dom.childElementByTag(parent, 'element2')).to.equal(element2); expect(dom.childElementByTag(parent, 'element3')).to.be.null; + expect(dom.childElementByTag(parent, 'element4')).to.be.null; + } + + it('childElementByTag should find first match', testChildElementByTag); + + it('childElementByTag should find first match (polyfill)', () => { + dom.setScopeSelectorSupportedForTesting(false); + testChildElementByTag(); }); - it('childElementByAttr should find first match', () => { + function testChildElementByAttr() { const parent = document.createElement('parent'); const element1 = document.createElement('element1'); @@ -140,13 +155,22 @@ describe('DOM', () => { element2.setAttribute('attr12', '2'); parent.appendChild(element2); + const element3 = document.createElement('element2'); + element3.setAttribute('on-child', ''); + element2.appendChild(element3); + expect(dom.childElementByAttr(parent, 'attr1')).to.equal(element1); expect(dom.childElementByAttr(parent, 'attr2')).to.equal(element2); expect(dom.childElementByAttr(parent, 'attr12')).to.equal(element1); - expect(dom.childElementByAttr(parent, 'attr12', '1')).to.equal(element1); - expect(dom.childElementByAttr(parent, 'attr12', '2')).to.equal(element2); - expect(dom.childElementByAttr(parent, 'attr12', '3')).to.be.null; expect(dom.childElementByAttr(parent, 'attr3')).to.be.null; + expect(dom.childElementByAttr(parent, 'on-child')).to.be.null; + } + + it('childElementByAttr should find first match', testChildElementByAttr); + + it('childElementByAttr should find first match', () => { + dom.setScopeSelectorSupportedForTesting(false); + testChildElementByAttr(); }); describe('contains', () => { diff --git a/test/functional/test-framerate.js b/test/functional/test-framerate.js index 8b5dc6ca8b49..c0066eda3fe1 100644 --- a/test/functional/test-framerate.js +++ b/test/functional/test-framerate.js @@ -96,7 +96,7 @@ describe('the framerate service', () => { expect(performance.tickDelta.callCount).to.equal(1); expect(performance.flush.callCount).to.equal(1); expect(performance.tickDelta.args[0][0]).to.equal('fps'); - expect(performance.tickDelta.args[0][1]).to.within(15, 16); + expect(performance.tickDelta.args[0][1]).to.within(44, 45); expect(fr.frameCount_).to.equal(0); // Second round @@ -118,7 +118,7 @@ describe('the framerate service', () => { expect(performance.tickDelta.callCount).to.equal(2); expect(performance.flush.callCount).to.equal(2); expect(performance.tickDelta.args[1][0]).to.equal('fps'); - expect(performance.tickDelta.args[1][1]).to.within(9, 10); + expect(performance.tickDelta.args[1][1]).to.within(50, 51); }); it('does nothing with an invisible window', () => { @@ -153,9 +153,9 @@ describe('the framerate service', () => { expect(performance.tickDelta.callCount).to.equal(2); expect(performance.flush.callCount).to.equal(1); expect(performance.tickDelta.args[0][0]).to.equal('fps'); - expect(performance.tickDelta.args[0][1]).to.within(15, 16); + expect(performance.tickDelta.args[0][1]).to.within(44, 45); expect(performance.tickDelta.args[1][0]).to.equal('fal'); - expect(performance.tickDelta.args[1][1]).to.within(15, 16); + expect(performance.tickDelta.args[1][1]).to.within(44, 45); // Second round fr.collect(); for (let i = 0; i < 50; i++) { diff --git a/test/functional/test-integration.js b/test/functional/test-integration.js index 44e2c1e2004e..edb83d647cdd 100644 --- a/test/functional/test-integration.js +++ b/test/functional/test-integration.js @@ -39,11 +39,14 @@ describe('3p integration.js', () => { expect(registrations).to.include.key('adsense'); expect(registrations).to.include.key('adtech'); expect(registrations).to.include.key('adreactor'); + expect(registrations).to.include.key('criteo'); expect(registrations).to.include.key('doubleclick'); expect(registrations).to.include.key('flite'); expect(registrations).to.include.key('twitter'); expect(registrations).to.include.key('yieldmo'); + expect(registrations).to.include.key('triplelift'); expect(registrations).to.include.key('_ping_'); + expect(registrations).to.include.key('imobile'); }); it('should validateParentOrigin without ancestorOrigins', () => { diff --git a/test/functional/test-intersection-observer.js b/test/functional/test-intersection-observer.js index 7d8ead7dc347..d63e5e61a381 100644 --- a/test/functional/test-intersection-observer.js +++ b/test/functional/test-intersection-observer.js @@ -141,7 +141,7 @@ describe('IntersectionObserver', () => { viewportCallback(inViewport) { testElementViewportCallback(inViewport); } - getInsersectionElementLayoutBox() { + getIntersectionElementLayoutBox() { testElementGetInsersectionElementLayoutBox(); return {top: 10, left: 10, width: 11, height: 1}; } diff --git a/test/functional/test-preconnect.js b/test/functional/test-preconnect.js index 3aa97e4ae170..e90029ed4fd0 100644 --- a/test/functional/test-preconnect.js +++ b/test/functional/test-preconnect.js @@ -14,7 +14,8 @@ * limitations under the License. */ -import {preconnectFor} from '../../src/preconnect'; +import {createIframePromise} from '../../testing/iframe'; +import {preconnectFor, Preconnect} from '../../src/preconnect'; import * as sinon from 'sinon'; describe('preconnect', () => { @@ -22,12 +23,34 @@ describe('preconnect', () => { let sandbox; let clock; let preconnect; + let preloadSupported; + let isSafari; // Factored out to make our linter happy since we don't allow // bare javascript URLs. const javascriptUrlPrefix = 'javascript'; + function getPreconnectIframe() { + return createIframePromise().then(iframe => { + if (preloadSupported !== undefined) { + sandbox.stub(Preconnect.prototype, 'isPreloadSupported_', () => { + return preloadSupported; + }); + } + preconnect = preconnectFor(iframe.win); + if (isSafari !== undefined) { + sandbox.stub(preconnect.platform_, 'isSafari', () => { + return isSafari; + }); + } + return iframe; + }); + } beforeEach(() => { + isSafari = undefined; + // Default mock to not support preload - override in cases to test for + // preload support. + preloadSupported = false; sandbox = sinon.sandbox.create(); clock = sandbox.useFakeTimers(); preconnect = preconnectFor(window); @@ -39,133 +62,238 @@ describe('preconnect', () => { }); it('should preconnect', () => { - sandbox.stub(preconnect.platform_, 'isSafari', () => false); - const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); - preconnect.url('https://a.preconnect.com/foo/bar'); - preconnect.url('https://a.preconnect.com/other'); - preconnect.url(javascriptUrlPrefix + ':alert()'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://a.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://a.preconnect.com/'); - expect(document.querySelectorAll('link[rel=prefetch]')) - .to.have.length(0); - expect(open.callCount).to.equal(0); + isSafari = false; + return getPreconnectIframe().then(iframe => { + const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); + preconnect.url('https://a.preconnect.com/foo/bar'); + preconnect.url('https://a.preconnect.com/other'); + preconnect.url(javascriptUrlPrefix + ':alert()'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=prefetch]')) + .to.have.length(0); + expect(open.callCount).to.equal(0); + }); }); it('should preconnect with polyfill', () => { - sandbox.stub(preconnect.platform_, 'isSafari', () => true); - const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); - const send = sandbox.spy(XMLHttpRequest.prototype, 'send'); - preconnect.url('https://s.preconnect.com/foo/bar'); - preconnect.url('https://s.preconnect.com/other'); - preconnect.url(javascriptUrlPrefix + ':alert()'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://s.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://s.preconnect.com/'); - expect(document.querySelectorAll('link[rel=prefetch]')) - .to.have.length(0); - expect(open.callCount).to.equal(1); - expect(send.callCount).to.equal(1); - expect(open.args[0][1]).to.include( - 'https://s.preconnect.com/amp_preconnect_polyfill_404_or' + - '_other_error_expected._Do_not_worry_about_it'); + isSafari = true; + return getPreconnectIframe().then(iframe => { + const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); + const send = sandbox.spy(XMLHttpRequest.prototype, 'send'); + preconnect.url('https://s.preconnect.com/foo/bar'); + preconnect.url('https://s.preconnect.com/other'); + preconnect.url(javascriptUrlPrefix + ':alert()'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://s.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://s.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=prefetch]')) + .to.have.length(0); + expect(open.callCount).to.equal(1); + expect(send.callCount).to.equal(1); + expect(open.args[0][1]).to.include( + 'https://s.preconnect.com/amp_preconnect_polyfill_404_or' + + '_other_error_expected._Do_not_worry_about_it'); + }); }); it('should cleanup', () => { - preconnect.url('https://c.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(9000); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(1000); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(0); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://c.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(9000); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(1000); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(0); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + }); }); it('should preconnect to 2 different origins', () => { - preconnect.url('https://d.preconnect.com/foo/bar'); - // Different origin - preconnect.url('https://e.preconnect.com/other'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(2); - expect(document.querySelectorAll('link[rel=dns-prefetch]')[0].href) - .to.equal('https://d.preconnect.com/'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')[1].href) - .to.equal('https://e.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(2); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://d.preconnect.com/foo/bar'); + // Different origin + preconnect.url('https://e.preconnect.com/other'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(2); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')[0].href) + .to.equal('https://d.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')[1].href) + .to.equal('https://e.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(2); + }); }); it('should timeout preconnects', () => { - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(9000); - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(1000); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - // After timeout preconnect creates a new tag. - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(9000); + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(1000); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + // After timeout preconnect creates a new tag. + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + }); }); it('should timeout preconnects longer with active connect', () => { - preconnect.url('https://y.preconnect.com/foo/bar', - /* opt_alsoConnecting */ true); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(10000); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - preconnect.url('https://y.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - clock.tick(180 * 1000); - preconnect.url('https://y.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://y.preconnect.com/foo/bar', + /* opt_alsoConnecting */ true); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(10000); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + preconnect.url('https://y.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + clock.tick(180 * 1000); + preconnect.url('https://y.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + }); }); it('should prefetch', () => { - preconnect.prefetch('https://a.prefetch.com/foo/bar'); - preconnect.prefetch('https://a.prefetch.com/foo/bar'); - preconnect.prefetch('https://a.prefetch.com/other'); - preconnect.prefetch(javascriptUrlPrefix + ':alert()'); - // Also preconnects. - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://a.prefetch.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://a.prefetch.com/'); - // Actual prefetch - const fetches = document.querySelectorAll( - 'link[rel=prefetch]'); - expect(fetches).to.have.length(2); - expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); - expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + }); + }); + + it('should add links (prefetch or preload)', () => { + // Don't stub preload support allow the test to run through the browser + // default regardless of support or not. + preloadSupported = undefined; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch],link[rel=preload]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[0].getAttribute('as')).to.equal('script'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + expect(fetches[1].getAttribute('as')).to.equal('style'); + }); + }); + + it('should prefetch when preload is not supported', () => { + preloadSupported = false; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + + const preloads = iframe.doc.querySelectorAll( + 'link[rel=preload]'); + expect(preloads).to.have.length(0); + + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[0].getAttribute('as')).to.equal('script'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + expect(fetches[1].getAttribute('as')).to.equal('style'); + }); + }); + + it('should preload when supported', () => { + preloadSupported = true; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(0); + const preloads = iframe.doc.querySelectorAll( + 'link[rel=preload]'); + expect(preloads).to.have.length(2); + expect(preloads[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(preloads[0].getAttribute('as')).to.equal('script'); + expect(preloads[1].href).to.equal('https://a.prefetch.com/other'); + expect(preloads[1].getAttribute('as')).to.equal('style'); + }); }); }); diff --git a/test/functional/test-url-replacements.js b/test/functional/test-url-replacements.js index bdddeee2127d..4a5e4801c9f9 100644 --- a/test/functional/test-url-replacements.js +++ b/test/functional/test-url-replacements.js @@ -206,6 +206,18 @@ describe('UrlReplacements', () => { }); }); + it('should replace VIEWPORT_WIDTH', () => { + return expand('?vw=VIEWPORT_WIDTH').then(res => { + expect(res).to.match(/vw=\d+/); + }); + }); + + it('should replace VIEWPORT_HEIGHT', () => { + return expand('?vh=VIEWPORT_HEIGHT').then(res => { + expect(res).to.match(/vh=\d+/); + }); + }); + it('should replace PAGE_LOAD_TIME', () => { return expand('?sh=PAGE_LOAD_TIME').then(res => { expect(res).to.match(/sh=\d+/); @@ -354,6 +366,33 @@ describe('UrlReplacements', () => { .to.eventually.equal('?a=b&b=b'); }); + it('should report errors & replace them with empty string (sync)', () => { + const clock = sandbox.useFakeTimers(); + const replacements = urlReplacementsFor(window); + replacements.set_('ONE', () => { + throw new Error('boom'); + }); + const p = expect(replacements.expand('?a=ONE')).to.eventually.equal('?a='); + expect(() => { + clock.tick(1); + }).to.throw(/boom/); + return p; + }); + + it('should report errors & replace them with empty string (promise)', () => { + const clock = sandbox.useFakeTimers(); + const replacements = urlReplacementsFor(window); + replacements.set_('ONE', () => { + return Promise.reject(new Error('boom')); + }); + return expect(replacements.expand('?a=ONE')).to.eventually.equal('?a=') + .then(() => { + expect(() => { + clock.tick(1); + }).to.throw(/boom/); + }); + }); + it('should support positional arguments', () => { const replacements = urlReplacementsFor(window); replacements.set_('FN', one => one); @@ -530,7 +569,6 @@ describe('UrlReplacements', () => { accessService = { getAccessReaderId: () => {}, getAuthdataField: () => {}, - whenFirstAuthorized: () => {}, }; accessServiceMock = sandbox.mock(accessService); reportDevSpy = sandbox.spy(); @@ -571,12 +609,9 @@ describe('UrlReplacements', () => { }); it('should replace AUTHDATA', () => { - accessServiceMock.expects('whenFirstAuthorized') - .returns(Promise.resolve()) - .once(); accessServiceMock.expects('getAuthdataField') .withExactArgs('field1') - .returns('value1') + .returns(Promise.resolve('value1')) .once(); return expand('?a=AUTHDATA(field1)').then(res => { expect(res).to.match(/a=value1/); diff --git a/test/functional/test-viewer.js b/test/functional/test-viewer.js index dbadd357dcf7..09bf3c4062b8 100644 --- a/test/functional/test-viewer.js +++ b/test/functional/test-viewer.js @@ -383,44 +383,6 @@ describe('Viewer', () => { expect(viewer.messageQueue_[1].eventType).to.equal('cancelFullOverlay'); }); - it('should receive broadcast event', () => { - let broadcastMessage = null; - viewer.onBroadcast(message => { - broadcastMessage = message; - }); - viewer.receiveMessage('broadcast', {type: 'type1'}); - expect(broadcastMessage).to.exist; - expect(broadcastMessage.type).to.equal('type1'); - }); - - it('should post broadcast event', () => { - const delivered = []; - viewer.setMessageDeliverer((eventType, data) => { - delivered.push({eventType: eventType, data: data}); - }, 'https://acme.com'); - viewer.broadcast({type: 'type1'}); - expect(viewer.messageQueue_.length).to.equal(0); - return viewer.messagingMaybePromise_.then(() => { - expect(delivered.length).to.equal(1); - const m = delivered[0]; - expect(m.eventType).to.equal('broadcast'); - expect(m.data.type).to.equal('type1'); - }); - }); - - it('should post broadcast event but not fail w/o messaging', () => { - viewer.broadcast({type: 'type1'}); - expect(viewer.messageQueue_.length).to.equal(0); - clock.tick(20001); - return viewer.messagingReadyPromise_.then(() => 'OK', () => 'ERROR') - .then(res => { - expect(res).to.equal('ERROR'); - return viewer.messagingMaybePromise_; - }).then(() => { - expect(viewer.messageQueue_.length).to.equal(0); - }); - }); - it('should queue non-dupe events', () => { viewer.postDocumentReady(11, 12); viewer.postDocumentResized(13, 14); @@ -451,62 +413,129 @@ describe('Viewer', () => { expect(delivered[1].data.width).to.equal(13); }); - it('should wait for messaging channel', () => { - let m1Resolved = false; - let m2Resolved = false; - const m1 = viewer.sendMessage('message1', {}, /* awaitResponse */ false) - .then(() => { - m1Resolved = true; - }); - const m2 = viewer.sendMessage('message2', {}, /* awaitResponse */ true) - .then(() => { - m2Resolved = true; - }); - return Promise.resolve().then(() => { - // Not resolved yet. - expect(m1Resolved).to.be.false; - expect(m2Resolved).to.be.false; - - // Set message deliverer. - viewer.setMessageDeliverer(() => { - return Promise.resolve(); - }, 'https://acme.com'); - expect(m1Resolved).to.be.false; - expect(m2Resolved).to.be.false; + describe('Messaging not embedded', () => { + + it('should not expect messaging', () => { + expect(viewer.messagingReadyPromise_).to.be.null; + expect(viewer.messagingMaybePromise_).to.be.null; + }); - return Promise.all([m1, m2]); - }).then(() => { - // All resolved now. - expect(m1Resolved).to.be.true; - expect(m2Resolved).to.be.true; + it('should fail sendMessage', () => { + return viewer.sendMessage('message1', {}, /* awaitResponse */ false) + .then(() => { + throw new Error('should not succeed'); + }, error => { + expect(error.message).to.match(/No messaging channel/); + }); + }); + + it('should post broadcast event but not fail', () => { + viewer.broadcast({type: 'type1'}); + expect(viewer.messageQueue_.length).to.equal(0); }); }); - it('should timeout messaging channel', () => { - let m1Resolved = false; - let m2Resolved = false; - const m1 = viewer.sendMessage('message1', {}, /* awaitResponse */ false) - .then(() => { - m1Resolved = true; - }); - const m2 = viewer.sendMessage('message2', {}, /* awaitResponse */ true) - .then(() => { - m2Resolved = true; - }); - return Promise.resolve().then(() => { - // Not resolved yet. - expect(m1Resolved).to.be.false; - expect(m2Resolved).to.be.false; + describe('Messaging', () => { + beforeEach(() => { + windowApi.parent = {}; + viewer = new Viewer(windowApi); + }); + + it('should receive broadcast event', () => { + let broadcastMessage = null; + viewer.onBroadcast(message => { + broadcastMessage = message; + }); + viewer.receiveMessage('broadcast', {type: 'type1'}); + expect(broadcastMessage).to.exist; + expect(broadcastMessage.type).to.equal('type1'); + }); - // Timeout. + it('should post broadcast event', () => { + const delivered = []; + viewer.setMessageDeliverer((eventType, data) => { + delivered.push({eventType: eventType, data: data}); + }, 'https://acme.com'); + viewer.broadcast({type: 'type1'}); + expect(viewer.messageQueue_.length).to.equal(0); + return viewer.messagingMaybePromise_.then(() => { + expect(delivered.length).to.equal(1); + const m = delivered[0]; + expect(m.eventType).to.equal('broadcast'); + expect(m.data.type).to.equal('type1'); + }); + }); + + it('should post broadcast event but not fail w/o messaging', () => { + viewer.broadcast({type: 'type1'}); + expect(viewer.messageQueue_.length).to.equal(0); clock.tick(20001); - return Promise.all([m1, m2]); - }).then(() => { - throw new Error('must never be here'); - }, () => { - // Not resolved ever. - expect(m1Resolved).to.be.false; - expect(m2Resolved).to.be.false; + return viewer.messagingReadyPromise_.then(() => 'OK', () => 'ERROR') + .then(res => { + expect(res).to.equal('ERROR'); + return viewer.messagingMaybePromise_; + }).then(() => { + expect(viewer.messageQueue_.length).to.equal(0); + }); + }); + + it('should wait for messaging channel', () => { + let m1Resolved = false; + let m2Resolved = false; + const m1 = viewer.sendMessage('message1', {}, /* awaitResponse */ false) + .then(() => { + m1Resolved = true; + }); + const m2 = viewer.sendMessage('message2', {}, /* awaitResponse */ true) + .then(() => { + m2Resolved = true; + }); + return Promise.resolve().then(() => { + // Not resolved yet. + expect(m1Resolved).to.be.false; + expect(m2Resolved).to.be.false; + + // Set message deliverer. + viewer.setMessageDeliverer(() => { + return Promise.resolve(); + }, 'https://acme.com'); + expect(m1Resolved).to.be.false; + expect(m2Resolved).to.be.false; + + return Promise.all([m1, m2]); + }).then(() => { + // All resolved now. + expect(m1Resolved).to.be.true; + expect(m2Resolved).to.be.true; + }); + }); + + it('should timeout messaging channel', () => { + let m1Resolved = false; + let m2Resolved = false; + const m1 = viewer.sendMessage('message1', {}, /* awaitResponse */ false) + .then(() => { + m1Resolved = true; + }); + const m2 = viewer.sendMessage('message2', {}, /* awaitResponse */ true) + .then(() => { + m2Resolved = true; + }); + return Promise.resolve().then(() => { + // Not resolved yet. + expect(m1Resolved).to.be.false; + expect(m2Resolved).to.be.false; + + // Timeout. + clock.tick(20001); + return Promise.all([m1, m2]); + }).then(() => { + throw new Error('must never be here'); + }, () => { + // Not resolved ever. + expect(m1Resolved).to.be.false; + expect(m2Resolved).to.be.false; + }); }); }); @@ -559,14 +588,13 @@ describe('Viewer', () => { }); }); - it('should decide non-trusted on connection without origin', () => { + it('should NOT allow channel without origin', () => { windowApi.parent = {}; windowApi.location.ancestorOrigins = null; const viewer = new Viewer(windowApi); - viewer.setMessageDeliverer(() => {}); - return viewer.isTrustedViewer().then(res => { - expect(res).to.be.false; - }); + expect(() => { + viewer.setMessageDeliverer(() => {}); + }).to.throw(/message channel must have an origin/); }); it('should decide non-trusted on connection with wrong origin', () => { diff --git a/test/functional/test-viewport.js b/test/functional/test-viewport.js index 96d58f447b58..bc3aa33b49bc 100644 --- a/test/functional/test-viewport.js +++ b/test/functional/test-viewport.js @@ -56,7 +56,9 @@ describe('Viewport', () => { installViewerService(windowApi); binding = new ViewportBindingVirtual_(windowApi, viewer); viewport = new Viewport(windowApi, binding, viewer); - viewport.fixedLayer_ = null; + viewport.fixedLayer_ = {update: () => { + return {then: callback => callback()}; + }}; viewport.getSize(); }); @@ -132,6 +134,7 @@ describe('Viewport', () => { // Should call updatePaddingTop. bindingMock = sandbox.mock(binding); + viewport.fixedLayer_.updatePaddingTop = () => {}; viewerMock.expects('getPaddingTop').returns(21).atLeast(1); bindingMock.expects('updatePaddingTop').withArgs(21).once(); viewerViewportHandler(); diff --git a/test/integration/test-amp-ad-doubleclick.js b/test/integration/test-amp-ad-doubleclick.js index df9f58e65a95..ef2cc1803bfa 100644 --- a/test/integration/test-amp-ad-doubleclick.js +++ b/test/integration/test-amp-ad-doubleclick.js @@ -22,11 +22,36 @@ import { describe('Rendering of one ad', () => { let fixture; + let beforeHref; + + function replaceUrl(win) { + // TODO(#2402) Support glade as well. + const path = '/test/fixtures/doubleclick.html?google_glade=0'; + try { + win.location.hash = 'google_glade=0'; + win.history.replaceState(null, null, path); + } catch (e) { + // Browsers are weird. Firefox gets here. We do, however, also in + // firefox pass down the parent URL. So we change that, which we + // can. We just need to change it back after the test. + beforeHref = win.parent.location.href; + win.parent.history.replaceState(null, null, path); + } + } + beforeEach(() => { - return createFixtureIframe('test/fixtures/doubleclick.html', 3000) - .then(f => { - fixture = f; - }); + replaceParentHref = false; + return createFixtureIframe('test/fixtures/doubleclick.html', 3000, win => { + replaceUrl(win); + }).then(f => { + fixture = f; + }); + }); + + afterEach(() => { + if (beforeHref) { + fixture.win.parent.history.replaceState(null, null, beforeHref); + } }); it('should create an iframe loaded', function() { @@ -69,7 +94,7 @@ describe('Rendering of one ad', () => { return poll('main ad JS is injected', () => { return iframe.contentWindow.document.querySelector( 'script[src="https://www.googletagservices.com/tag/js/gpt.js"]'); - }, undefined, /* timeout */ 4000); + }, undefined, /* timeout */ 5000); }).then(() => { return poll('render-start message received', () => { return fixture.messages.filter(message => { @@ -85,7 +110,7 @@ describe('Rendering of one ad', () => { }).then(pubads => { const canvas = iframe.contentWindow.document.querySelector('#c'); expect(pubads.get('page_url')).to.equal( - 'http://localhost:9876/doubleclick.html'); + 'https://www.example.com/doubleclick.html'); const slot = canvas.slot; expect(slot).to.not.be.null; expect(slot.getCategoryExclusions()).to.jsonEqual(['health']); diff --git a/third_party/optimized-svg-icons/LICENSE b/third_party/optimized-svg-icons/LICENSE new file mode 100644 index 000000000000..b0225bcdd75f --- /dev/null +++ b/third_party/optimized-svg-icons/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://opensource.org/licenses/MIT diff --git a/third_party/optimized-svg-icons/README.amp b/third_party/optimized-svg-icons/README.amp new file mode 100644 index 000000000000..ca952b2901ec --- /dev/null +++ b/third_party/optimized-svg-icons/README.amp @@ -0,0 +1,14 @@ +URL: http://codepen.io/anon/pen/KVZNGp +License: The MIT License +License File: https://gist.githubusercontent.com/anonymous/920164b2cafcdade77a2/raw/97801f67daefc7cde3523d9c35ef73f6a43759f5/script.js + +Description: +Online SVGs of popular logos + +Local Modifications: +Created a derived css file from the index.html, contained in +amp-social-share-svgs.css. To be imported into the ampproject to provide in-line +svg icons through css. + +Gist created verbatim from source for referencing files: +https://gist.github.com/anonymous/920164b2cafcdade77a2 diff --git a/third_party/optimized-svg-icons/README.md b/third_party/optimized-svg-icons/README.md new file mode 100644 index 000000000000..a206c19fe886 --- /dev/null +++ b/third_party/optimized-svg-icons/README.md @@ -0,0 +1,10 @@ +Optimized Inline SVG Icons +-------------------------- +SVG-powered social media icon set, yo :P +Thanks for all the comments and suggestions! + +Forked from [Ruandre Janse Van Rensburg](http://codepen.io/ruandre/)'s Pen [Optimized Inline SVG Icons](http://codepen.io/ruandre/pen/howFi/). + +A [Pen](http://codepen.io/anon/pen/KVZNGp) by [Captain Anonymous](http://codepen.io/anon) on [CodePen](http://codepen.io/). + +[License](http://codepen.io/anon/pen/KVZNGp/license). diff --git a/third_party/optimized-svg-icons/amp-social-share-svgs.css b/third_party/optimized-svg-icons/amp-social-share-svgs.css new file mode 100644 index 000000000000..ea9dd61910a3 --- /dev/null +++ b/third_party/optimized-svg-icons/amp-social-share-svgs.css @@ -0,0 +1,19 @@ +amp-social-share .facebook a { + background-image: url('data:image/svg+xml;utf8,'); +} + +amp-social-share .pinterest a { + background-image: url('data:image/svg+xml;utf8,'); +} + +amp-social-share .linkedin a { + background-image: url('data:image/svg+xml;utf8,'); +} + +amp-social-share .gplus a { + background-image: url('data:image/svg+xml;utf8,'); +} + +amp-social-share .email a { + background-image: url('data:image/svg+xml;utf8,'); +} diff --git a/third_party/optimized-svg-icons/index.html b/third_party/optimized-svg-icons/index.html new file mode 100644 index 000000000000..a9e461524332 --- /dev/null +++ b/third_party/optimized-svg-icons/index.html @@ -0,0 +1,70 @@ + +
    + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + + + +
  • + + + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + +
  • + + + +
  • + +
  • + +
diff --git a/third_party/optimized-svg-icons/script.js b/third_party/optimized-svg-icons/script.js new file mode 100644 index 000000000000..6feda622109c --- /dev/null +++ b/third_party/optimized-svg-icons/script.js @@ -0,0 +1,30 @@ +/* + +If you get some use out of this please feel free to throw some money at me via PayPal: ruandrejvr@gmail.com + +--- + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://opensource.org/licenses/MIT + +--- + +I made these because I needed a good set of SVG icons and couldn't find one. Hopefully they come in handy for other people as well. + +Found most of the logos on iconmonstr.com and scaled them until they all looked about the same size (like they belong in a 'set' as opposed to just randomly clobbered together from visually dissimilar packs). + +I also wrote a little script for Adobe Illustrator to help me scale vectors but had to tweak them by hand anyhow because the negative space makes some icons look bigger than others, etc. You can check it out here if you'd like: https://gist.github.com/ruandre/7b47cbf2a4c55dac9adb#file-fittoartboard-jsx + +SVGs compressed using this indispensable tool: +http://petercollingridge.appspot.com/svg-editor + +Note: IE8 and lower gets a text version. + +*/ diff --git a/third_party/optimized-svg-icons/style.css b/third_party/optimized-svg-icons/style.css new file mode 100644 index 000000000000..41dd78fb46d2 --- /dev/null +++ b/third_party/optimized-svg-icons/style.css @@ -0,0 +1,73 @@ + +// Thanks to codepen.io/alaingalvan for suggesting this color variable: + +$background: hsl(210, 45, 10) + + + +// http://www.paulirish.com/2012/box-sizing-border-box-ftw/ + +html + box-sizing: border-box + +*, *:before, *:after + box-sizing: inherit + + + +html + background: $background + font-size: .625em // 10px for rems + +.soc + display: block + font-size: 0 + list-style: none + margin: 0 + padding: 48px // IE8 + padding: 4.8rem + text-align: center + li + display: inline-block + margin: 12px // IE8 + margin: 1.2rem + a, svg + display: block + a + position: relative // IE8 + height: 96px // IE8 + height: 9.6rem + width: 96px // IE8 + width: 9.6rem + svg + height: 100% + width: 100% + // IE8: + em + font-size: 14px + line-height: 1.5 + margin-top: -.75em + position: absolute + text-align: center + top: 50% + right: 0 + bottom: 0 + left: 0 + +// Using placeholder selectors and @extend (keeps output CSS lean) instead of [class*='icon-'] attribute selector because IE8 doesn't support it :< + +%social-icon-hover + border-radius: 100% + color: $background // IE8 + fill: $background + transform: scale(1.25) + transition: background-color .5s, transform .5s ease-out + +// Neat color trick, <3 Sass +@for $i from 1 through 30 + .icon-#{$i} + color: hsl($i * 12, 70, 50) // IE8 + fill: hsl($i * 12, 70, 50) + &:hover + background: hsl($i * 12, 70, 50) + @extend %social-icon-hover diff --git a/tools/errortracker/app.yaml b/tools/errortracker/app.yaml index fc90dbd42112..5d8390c3df83 100644 --- a/tools/errortracker/app.yaml +++ b/tools/errortracker/app.yaml @@ -1,5 +1,5 @@ application: amp-error-reporting -version: 7 +version: 8 runtime: go api_version: go1 diff --git a/tools/errortracker/errortracker.go b/tools/errortracker/errortracker.go index 71831014770d..fd5e366be88f 100644 --- a/tools/errortracker/errortracker.go +++ b/tools/errortracker/errortracker.go @@ -112,14 +112,17 @@ func handle(w http.ResponseWriter, r *http.Request) { level := logging.Info // But if the request comes from the cache (and thus only from valid AMP // docs) we log as "ERROR". - if strings.HasPrefix(r.Referer(), "https://cdn.ampproject.org/") { + if strings.HasPrefix(r.Referer(), "https://cdn.ampproject.org/") || + strings.Contains(r.Referer(), ".ampproject.net/") { severity = "ERROR" level = logging.Error errorType += "-cdn" } else { errorType += "-origin" } + is3p := false if r.URL.Query().Get("3p") == "1" { + is3p = true errorType += "-3p" } else { errorType += "-1p" @@ -129,7 +132,7 @@ func handle(w http.ResponseWriter, r *http.Request) { errorType += "-canary" isCanary = true; } - if !isCanary && level != logging.Error && rand.Float32() > 0.01 { + if !isCanary && !is3p && level != logging.Error && rand.Float32() > 0.01 { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "THROTTLED\n") diff --git a/tools/experiments/experiments.js b/tools/experiments/experiments.js index 36a1db9c7953..742dee6aced3 100644 --- a/tools/experiments/experiments.js +++ b/tools/experiments/experiments.js @@ -51,13 +51,6 @@ const EXPERIMENTS = [ 'README.md#amp-dev-channel', }, - // AMP Access Analytics - { - id: 'amp-access-analytics', - name: 'AMP Access Analytics', - spec: 'https://github.com/ampproject/amphtml/issues/1556', - }, - // Dynamic CSS Classes { id: 'dynamic-css-classes', @@ -73,12 +66,6 @@ const EXPERIMENTS = [ spec: 'https://github.com/ampproject/amphtml/blob/master/extensions/' + 'amp-accordion/amp-accordion.md', }, - - // Unique 3p origins - { - id: 'unique-origins', - name: 'Unique 3p origins', - }, ]; diff --git a/validator/htmlparser.js b/validator/htmlparser.js index 9e1dc7c8fd85..d5391ffd0bb8 100644 --- a/validator/htmlparser.js +++ b/validator/htmlparser.js @@ -196,10 +196,10 @@ amp.htmlparser.HtmlParser = class { if (m[1]) { // Attribute. // SetAttribute with uppercase names doesn't work on IE6. const attribName = amp.htmlparser.toLowerCase(m[1]); - // Use name as value for valueless attribs, so + // Use empty string as value for valueless attribs, so // - // gets attributes ['type', 'checkbox', 'checked', 'checked'] - let decodedValue = attribName; + // gets attributes ['type', 'checkbox', 'checked', ''] + let decodedValue = ""; if (m[2]) { let encodedValue = m[3]; switch (encodedValue.charCodeAt(0)) { // Strip quotes. diff --git a/validator/htmlparser_test.js b/validator/htmlparser_test.js index fd37833e0d97..ffd2b97415fb 100644 --- a/validator/htmlparser_test.js +++ b/validator/htmlparser_test.js @@ -97,7 +97,7 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(input,[type,checkbox,checked,checked])', + 'startDoc()', 'startTag(input,[type,checkbox,checked,])', 'endDoc()']); }); @@ -172,7 +172,7 @@ describe('HtmlParser', () => { // Note the two double quotes at the end of the tag. parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(a,[href,foo.html,","])', + 'startDoc()', 'startTag(a,[href,foo.html,",])', 'endTag(a)', 'endDoc()' ]); }); }); @@ -384,9 +384,9 @@ describe('HtmlParser with location', () => { ''); expect(handler.log).toEqual([ ':1:0: startDoc()', - ':1:0: startTag(!doctype,[html,html])', + ':1:0: startTag(!doctype,[html,])', ':1:14: pcdata("\n")', - ':2:0: startTag(html,[amp,amp,lang,tr])', + ':2:0: startTag(html,[amp,,lang,tr])', ':2:19: pcdata("\n")', ':3:0: startTag(head,[])', ':3:5: pcdata("\n")', @@ -396,7 +396,7 @@ describe('HtmlParser with location', () => { ':5:0: rcdata("")', ':5:7: endTag(title)', ':5:14: pcdata("\n")', - ':6:0: startTag(script,[async,async,src,'+ + ':6:0: startTag(script,[async,,src,'+ 'https://cdn.ampproject.org/v0.js])', ':6:0: cdata("")', ':6:53: endTag(script)', diff --git a/validator/parse-css.js b/validator/parse-css.js index 9d788e5bbf07..c24248edb3fd 100644 --- a/validator/parse-css.js +++ b/validator/parse-css.js @@ -23,6 +23,7 @@ goog.provide('parse_css.AtRule'); goog.provide('parse_css.BlockType'); goog.provide('parse_css.Declaration'); +goog.provide('parse_css.ParsedCssUrl'); goog.provide('parse_css.QualifiedRule'); goog.provide('parse_css.Rule'); goog.provide('parse_css.RuleVisitor'); @@ -30,6 +31,7 @@ goog.provide('parse_css.Stylesheet'); goog.provide('parse_css.TokenStream'); goog.provide('parse_css.extractAFunction'); goog.provide('parse_css.extractASimpleBlock'); +goog.provide('parse_css.extractUrls'); goog.provide('parse_css.parseAStylesheet'); goog.require('amp.validator.ValidationError.Code'); @@ -740,4 +742,201 @@ parse_css.extractAFunction = function(tokenStream) { const tokenList = consumedTokens.slice(0, -1); tokenList.push(createEOFTokenAt(consumedTokens[consumedTokens.length - 1])); return tokenList; +}; + +/** + * Used by parse_css.ExtractUrls to return urls it has seen. This represents + * URLs in CSS such as url(http://foo.com/) and url("http://bar.com/"). + * For this token, line() and col() indicate the position information + * of the left-most CSS token that's part of the URL. E.g., this would be + * the URLToken instance or the FunctionToken instance. + */ +parse_css.ParsedCssUrl = class extends parse_css.Token { + constructor() { + super(); + /** @type {parse_css.TokenType} */ + this.tokenType = parse_css.TokenType.PARSED_CSS_URL; + /** + * The decoded URL. This string will not contain CSS string escapes, + * quotes, or similar. Encoding is utf8. + * @type {!string} + */ + this.utf8Url = ''; + /** + * A rule scope, in case the url was encountered within an at-rule. + * If not within an at-rule, this string is empty. + * @type {!string} + */ + this.atRuleScope = ''; + } + + /** @inheritDoc */ + toJSON() { + const json = super.toJSON(); + json['utf8Url'] = this.utf8Url; + json['atRuleScope'] = this.atRuleScope; + return json; + } +}; + +/** + * Parses a CSS URL token; typically takes the form "url(http://foo)". + * Preconditions: tokens[token_idx] is a URL token + * and token_idx + 1 is in range. + * @param {!Array} tokens + * @param {!number} tokenIdx + * @param {!parse_css.ParsedCssUrl} parsed + */ +function parseUrlToken(tokens, tokenIdx, parsed) { + goog.asserts.assert(tokenIdx + 1 < tokens.length); + const token = tokens[tokenIdx]; + goog.asserts.assert(token.tokenType === parse_css.TokenType.URL); + parsed.line = token.line; + parsed.col = token.col; + parsed.utf8Url = /** @type {parse_css.URLToken}*/(token).value; +} + +/** + * Parses a CSS function token named 'url', including the string and closing + * paren. Typically takes the form "url('http://foo')". + * Returns the token_idx past the closing paren, or -1 if parsing fails. + * Preconditions: tokens[token_idx] is a URL token + * and tokens[token_idx]->StringValue() == "url" + * @param {!Array} tokens + * @param {!number} tokenIdx + * @param {!parse_css.ParsedCssUrl} parsed + * @return {!number} + */ +function parseUrlFunction(tokens, tokenIdx, parsed) { + const token = tokens[tokenIdx] + goog.asserts.assert(token.tokenType == parse_css.TokenType.FUNCTION_TOKEN); + goog.asserts.assert(/** @type {parse_css.FunctionToken} */(token).value === + 'url'); + goog.asserts.assert(tokens[tokens.length - 1].tokenType === + parse_css.TokenType.EOF_TOKEN); + parsed.line = token.line; + parsed.col = token.col; + ++tokenIdx; // We've digested the function token above. + // Safe: tokens ends w/ EOF_TOKEN. + goog.asserts.assert(tokenIdx < tokens.length); + + // Consume optional whitespace. + while (tokens[tokenIdx].tokenType === parse_css.TokenType.WHITESPACE) { + ++tokenIdx; + // Safe: tokens ends w/ EOF_TOKEN. + goog.asserts.assert(tokenIdx < tokens.length); + } + + // Consume URL. + if (tokens[tokenIdx].tokenType !== parse_css.TokenType.STRING) { + return -1; + } + parsed.utf8Url = /** @type {parse_css.StringToken} */(tokens[tokenIdx]).value; + + ++tokenIdx; + // Safe: tokens ends w/ EOF_TOKEN. + goog.asserts.assert(tokenIdx < tokens.length); + + // Consume optional whitespace. + while (tokens[tokenIdx].tokenType === parse_css.TokenType.WHITESPACE) { + ++tokenIdx; + // Safe: tokens ends w/ EOF_TOKEN. + goog.asserts.assert(tokenIdx < tokens.length); + } + + // Consume ')' + if (tokens[tokenIdx].tokenType !== parse_css.TokenType.CLOSE_PAREN) { + return -1; + } + return tokenIdx + 1; } + +/** + * Helper class for implementing parse_css.extractUrls. + * @private + */ +class UrlFunctionVisitor extends parse_css.RuleVisitor { + /** + * @param {!Array} parsedUrls + * @param {!Array} errors + */ + constructor(parsedUrls, errors) { + /** @type {!Array} */ + this.parsedUrls = parsedUrls; + /** @type {!Array} */ + this.errors = errors; + /** @type {!string} */ + this.atRuleScope = ''; + } + + /** @inheritDoc */ + visitStylesheet(stylesheet) { + this.atRuleScope = ''; + } + + /** @inheritDoc */ + visitAtRule(atRule) { + this.atRuleScope = atRule.name; + } + + /** @inheritDoc */ + visitQualifiedRule(qualifiedRule) { + this.atRuleScope = ''; + } + + /** @inheritDoc */ + visitDeclaration(declaration) { + goog.asserts.assert(declaration.value.length > 0); + goog.asserts.assert( + declaration.value[declaration.value.length - 1].tokenType === + parse_css.TokenType.EOF_TOKEN); + for (let ii = 0; ii < declaration.value.length - 1;) { + const token = declaration.value[ii]; + if (token.tokenType === parse_css.TokenType.URL) { + const parsedUrl = new parse_css.ParsedCssUrl(); + parseUrlToken(declaration.value, ii, parsedUrl); + parsedUrl.atRuleScope = this.atRuleScope; + this.parsedUrls.push(parsedUrl); + ++ii; + continue; + } + if (token.tokenType === parse_css.TokenType.FUNCTION_TOKEN && + /** @type {!parse_css.FunctionToken} */(token).value === 'url') { + const parsedUrl = new parse_css.ParsedCssUrl(); + ii = parseUrlFunction(declaration.value, ii, parsedUrl); + if (ii === -1) { + const error = new parse_css.ErrorToken( + amp.validator.ValidationError.Code.CSS_SYNTAX_BAD_URL, + /* params */ ['style']); + error.line = token.line; + error.col = token.col; + this.errors.push(error); + return; + } + parsedUrl.atRuleScope = this.atRuleScope; + this.parsedUrls.push(parsedUrl); + continue; + } + // It's neither a url token nor a function token named url. So, we skip. + ++ii; + } + } +} + +/** + * Extracts the URLs within the provided stylesheet, emitting them into + * parsedUrls and errors into errors. + * @param {!parse_css.Stylesheet} stylesheet + * @param {!Array} parsedUrls + * @param {!Array} errors + */ +parse_css.extractUrls = function(stylesheet, parsedUrls, errors) { + const parsedUrlsOldLength = parsedUrls.length; + const errorsOldLength = errors.length; + const visitor = new UrlFunctionVisitor(parsedUrls, errors); + stylesheet.accept(visitor); + // If anything went wrong, delete the urls we've already emitted. + if (errorsOldLength !== errors.length) { + parsedUrls.splice(parsedUrlsOldLength); + } +}; diff --git a/validator/parse-css_test.js b/validator/parse-css_test.js index d82da9560c8d..4c6c51b512be 100644 --- a/validator/parse-css_test.js +++ b/validator/parse-css_test.js @@ -676,8 +676,8 @@ describe('parseAStylesheet', () => { // The tests below are exploratory - they tell us what the css parser // currently produces for these selectors. For a list of selectors, see // http://www.w3.org/TR/css3-selectors/#selectors. - // - // TODO(johannes): Get complete coverage. + // Note also that css-selectors.js contains a parser for selectors + // which is covered in this unittest below. it('handles simple selector example', () => { assertJSONEquals( @@ -720,6 +720,119 @@ describe('parseAStylesheet', () => { }); }); +describe('extractUrls', () => { + + // Tests that font urls are parsed with font-face atRuleScope. + it('finds font in font-face', () => { + const css = + "@font-face {font-family: 'Foo'; src: url('http://foo.com/bar.ttf');}"; + const errors = []; + const tokenList = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokenList, ampAtRuleParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, errors); + assertJSONEquals([], errors); + assertJSONEquals( + [{'line': 1, 'col': 37, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + 'font-face', 'utf8Url': 'http://foo.com/bar.ttf'}], parsedUrls); + }); + + // Tests that image URLs are parsed with empty atRuleScope; also tests + // that unicode escapes (in this case \000026) within the URL are decoded. + it('supports image url with unicode', () => { + const css = + "body{background-image: url('http://a.com/b/c=d\\000026e=f_g*h');}"; + const errors = []; + const tokenList = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokenList, ampAtRuleParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, errors); + assertJSONEquals([], errors); + assertJSONEquals( + [{'line': 1, 'col': 23, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + '', 'utf8Url': 'http://a.com/b/c=d&e=f_g*h'}], parsedUrls); + }); + + // This example contains both image urls, other urls (fonts) and + // segments in between. + it('handles longer example', () => { + const css = + ".a { color:red; background-image:url(4.png) }" + + ".b { color:black; background-image:url('http://a.com/b.png') } " + + "@font-face {font-family: 'Medium';src: url('http://a.com/1.woff') " + + "format('woff'),url('http://b.com/1.ttf') format('truetype')," + + "src:url('') format('embedded-opentype');}"; + const errors = []; + const tokenList = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokenList, ampAtRuleParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, errors); + assertJSONEquals([], errors); + assertJSONEquals( + [{'line': 1, 'col': 33, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + '', 'utf8Url': '4.png'}, + {'line': 1, 'col': 80, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + '', 'utf8Url': 'http://a.com/b.png'}, + {'line': 1, 'col': 147, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + 'font-face', 'utf8Url': 'http://a.com/1.woff'}, + {'line': 1, 'col': 189, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + 'font-face', 'utf8Url': 'http://b.com/1.ttf'}, + {'line': 1, 'col': 238, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + 'font-face', 'utf8Url': ''}], parsedUrls); + }); + + // Windows newlines present extra challenges for position information. + it('handles windows newlines', () => { + const css = + ".a \r\n{ color:red; background-image:url(4.png) }\r\n" + + ".b { color:black; \r\nbackground-image:url('http://a.com/b.png') }"; + const errors = []; + const tokenList = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokenList, ampAtRuleParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, errors); + assertJSONEquals([], errors); + assertJSONEquals( + [{'line': 2, 'col': 30, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + '', 'utf8Url': '4.png'}, + {'line': 4, 'col': 17, 'tokenType': 'PARSED_CSS_URL', 'atRuleScope': + '', 'utf8Url': 'http://a.com/b.png'}], parsedUrls); + }); + + // This example parses as CSS without errors, however once the URL + // with parameters is extracted, we recognize that the arguments to + // the url function are invalid. + it('invalid arguments inside url function yields error', () => { + const css = + "\n" + + " @font-face {\n" + + " font-family: 'Roboto', sans-serif;\n" + + " src: url('');\n" + + " }\n"; + const errors = []; + const tokenList = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokenList, ampAtRuleParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, errors); + assertJSONEquals( + [{'line': 4, 'col': 11, 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_BAD_URL', 'params': ['style']}], errors); + assertJSONEquals([], parsedUrls); + }); +}); + function parseSelectorForTest(selector) { const css = selector + '{}'; const errors = []; diff --git a/validator/testdata/feature_tests/amp_brightcove.out b/validator/testdata/feature_tests/amp_brightcove.out index 8e9e92a75cb6..cde4ac0fab57 100644 --- a/validator/testdata/feature_tests/amp_brightcove.out +++ b/validator/testdata/feature_tests/amp_brightcove.out @@ -1,3 +1,3 @@ FAIL feature_tests/amp_brightcove.html:46:2 The mandatory attribute 'data-account' is missing in tag 'amp-brightcove'. (see https://www.ampproject.org/docs/reference/extended/amp-brightcove.html) [AMP_TAG_PROBLEM] -feature_tests/amp_brightcove.html:50:7 The 'amp-brightcove extension .js script' tag is missing or incorrect, but required by 'amp-brightcove'. (see https://www.ampproject.org/docs/reference/extended/amp-brightcove.html) [AMP_TAG_PROBLEM] +feature_tests/amp_brightcove.html:50:7 The tag 'amp-brightcove extension .js script' is missing or incorrect, but required by 'amp-brightcove'. (see https://www.ampproject.org/docs/reference/extended/amp-brightcove.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/amp_carousel.out b/validator/testdata/feature_tests/amp_carousel.out index 0e08c86019de..a45c7ceb0d10 100644 --- a/validator/testdata/feature_tests/amp_carousel.out +++ b/validator/testdata/feature_tests/amp_carousel.out @@ -1,3 +1,3 @@ FAIL feature_tests/amp_carousel.html:31:2 The attribute 'delay' in tag 'amp-carousel' is set to the invalid value ''. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] -feature_tests/amp_carousel.html:40:7 The 'amp-carousel extension .js script' tag is missing or incorrect, but required by 'amp-carousel'. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] +feature_tests/amp_carousel.html:40:7 The tag 'amp-carousel extension .js script' is missing or incorrect, but required by 'amp-carousel'. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/amp_identification_missing.out b/validator/testdata/feature_tests/amp_identification_missing.out index 772786065468..4f8c811f207d 100644 --- a/validator/testdata/feature_tests/amp_identification_missing.out +++ b/validator/testdata/feature_tests/amp_identification_missing.out @@ -1,3 +1,3 @@ FAIL -feature_tests/amp_identification_missing.html:22:0 The mandatory attribute '⚡' is missing in tag 'html ⚡ for top-level html'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/amp_identification_missing.html:33:7 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/amp_identification_missing.html:22:0 The mandatory attribute '⚡' is missing in tag 'html ⚡ for top-level html'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/amp_identification_missing.html:33:7 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/amp_layouts.out b/validator/testdata/feature_tests/amp_layouts.out index 6e75fd8cb6d2..c0fd01b6a2ab 100644 --- a/validator/testdata/feature_tests/amp_layouts.out +++ b/validator/testdata/feature_tests/amp_layouts.out @@ -7,5 +7,5 @@ feature_tests/amp_layouts.html:83:2 The attribute 'layout' in tag 'amp-img' is s feature_tests/amp_layouts.html:113:2 The implied layout 'FIXED_HEIGHT' is not supported by tag 'amp-pixel'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_LAYOUT_PROBLEM] feature_tests/amp_layouts.html:117:2 The attribute 'width' in tag 'amp-pixel' is set to the invalid value 'X'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_LAYOUT_PROBLEM] feature_tests/amp_layouts.html:121:2 The attribute 'height' in tag 'amp-pixel' is set to the invalid value 'X'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_LAYOUT_PROBLEM] -feature_tests/amp_layouts.html:133:2 The attribute 'heights' in tag 'amp-img' is disallowed by implied layout 'FIXED'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] +feature_tests/amp_layouts.html:133:2 The attribute 'heights' in tag 'amp-img' is disallowed by specified layout 'FIXED'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] feature_tests/amp_layouts.html:137:2 The attribute 'heights' in tag 'amp-img' is disallowed by implied layout 'FIXED_HEIGHT'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] diff --git a/validator/testdata/feature_tests/amp_list.out b/validator/testdata/feature_tests/amp_list.out index 470fc0d292e6..bbe64d66b141 100644 --- a/validator/testdata/feature_tests/amp_list.out +++ b/validator/testdata/feature_tests/amp_list.out @@ -2,4 +2,4 @@ FAIL feature_tests/amp_list.html:36:2 The attribute 'wdith' may not appear in tag 'amp-list'. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [AMP_TAG_PROBLEM] feature_tests/amp_list.html:41:2 The mandatory attribute 'src' is missing in tag 'amp-list'. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [AMP_TAG_PROBLEM] feature_tests/amp_list.html:46:2 The implied layout 'CONTAINER' is not supported by tag 'amp-list'. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [AMP_LAYOUT_PROBLEM] -feature_tests/amp_list.html:50:7 The 'amp-list extension .js script' tag is missing or incorrect, but required by 'amp-list'. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [AMP_TAG_PROBLEM] +feature_tests/amp_list.html:50:7 The tag 'amp-list extension .js script' is missing or incorrect, but required by 'amp-list'. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/amp_user_notification.out b/validator/testdata/feature_tests/amp_user_notification.out index 0dd2cb9e242b..5b8c3f6bb754 100644 --- a/validator/testdata/feature_tests/amp_user_notification.out +++ b/validator/testdata/feature_tests/amp_user_notification.out @@ -1,3 +1,3 @@ FAIL feature_tests/amp_user_notification.html:69:2 The specified layout 'CONTAINER' is not supported by tag 'amp-user-notification'. (see https://www.ampproject.org/docs/reference/extended/amp-user-notification.html) [AMP_LAYOUT_PROBLEM] -feature_tests/amp_user_notification.html:79:7 The 'amp-user-notification extension .js script' tag is missing or incorrect, but required by 'amp-user-notification'. (see https://www.ampproject.org/docs/reference/extended/amp-user-notification.html) [AMP_TAG_PROBLEM] +feature_tests/amp_user_notification.html:79:7 The tag 'amp-user-notification extension .js script' is missing or incorrect, but required by 'amp-user-notification'. (see https://www.ampproject.org/docs/reference/extended/amp-user-notification.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/bad_viewport.out b/validator/testdata/feature_tests/bad_viewport.out index 22cf40637ffc..3c65a0c0232d 100644 --- a/validator/testdata/feature_tests/bad_viewport.out +++ b/validator/testdata/feature_tests/bad_viewport.out @@ -1,5 +1,5 @@ FAIL -feature_tests/bad_viewport.html:25:2 The property 'foo' in attribute 'content' in tag 'meta name=viewport' is disallowed. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/bad_viewport.html:25:2 The property 'minimum-scale' in attribute 'content' in tag 'meta name=viewport' is set to 'not-a-number', which is invalid. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/bad_viewport.html:25:2 The property 'width' is missing from attribute 'content' in tag 'meta name=viewport'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/bad_viewport.html:32:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/bad_viewport.html:25:2 The property 'foo' in attribute 'content' in tag 'meta name=viewport' is disallowed. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/bad_viewport.html:25:2 The property 'minimum-scale' in attribute 'content' in tag 'meta name=viewport' is set to 'not-a-number', which is invalid. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/bad_viewport.html:25:2 The property 'width' is missing from attribute 'content' in tag 'meta name=viewport'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/bad_viewport.html:32:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/css_errors.out b/validator/testdata/feature_tests/css_errors.out index 608196f424da..eb7955d45df8 100644 --- a/validator/testdata/feature_tests/css_errors.out +++ b/validator/testdata/feature_tests/css_errors.out @@ -1,5 +1,5 @@ FAIL -feature_tests/css_errors.html:29:8 CSS syntax error in tag 'author stylesheet' - unterminated string. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/css_errors.html:30:25 CSS syntax error in tag 'author stylesheet' - stray trailing backslash. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/css_errors.html:31:4 CSS syntax error in tag 'author stylesheet' - bad url. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/css_errors.html:29:4 CSS syntax error in tag 'author stylesheet' - EOF in prelude of a qualified rule. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/css_errors.html:29:8 CSS syntax error in tag 'style amp-custom' - unterminated string. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/css_errors.html:30:25 CSS syntax error in tag 'style amp-custom' - stray trailing backslash. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/css_errors.html:31:4 CSS syntax error in tag 'style amp-custom' - bad url. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/css_errors.html:29:4 CSS syntax error in tag 'style amp-custom' - EOF in prelude of a qualified rule. [AUTHOR_STYLESHEET_PROBLEM] diff --git a/validator/testdata/feature_tests/dailymotion.out b/validator/testdata/feature_tests/dailymotion.out index b49809fe1f67..a349c14f3698 100644 --- a/validator/testdata/feature_tests/dailymotion.out +++ b/validator/testdata/feature_tests/dailymotion.out @@ -1,4 +1,4 @@ FAIL -feature_tests/dailymotion.html:48:0 The attribute 'data-videoid' in tag 'amp-dailymotion' is set to the invalid value 'i don't think so'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-dailymotion/amp-dailymotion.md) [AMP_TAG_PROBLEM] -feature_tests/dailymotion.html:52:0 The mandatory attribute 'data-videoid' is missing in tag 'amp-dailymotion'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-dailymotion/amp-dailymotion.md) [AMP_TAG_PROBLEM] -feature_tests/dailymotion.html:55:0 The attribute 'data-ui-highlight' in tag 'amp-dailymotion' is set to the invalid value 'blue'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-dailymotion/amp-dailymotion.md) [AMP_TAG_PROBLEM] +feature_tests/dailymotion.html:48:0 The attribute 'data-videoid' in tag 'amp-dailymotion' is set to the invalid value 'i don't think so'. (see https://www.ampproject.org/docs/reference/extended/amp-dailymotion.html) [AMP_TAG_PROBLEM] +feature_tests/dailymotion.html:52:0 The mandatory attribute 'data-videoid' is missing in tag 'amp-dailymotion'. (see https://www.ampproject.org/docs/reference/extended/amp-dailymotion.html) [AMP_TAG_PROBLEM] +feature_tests/dailymotion.html:55:0 The attribute 'data-ui-highlight' in tag 'amp-dailymotion' is set to the invalid value 'blue'. (see https://www.ampproject.org/docs/reference/extended/amp-dailymotion.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/dog_doc_type.out b/validator/testdata/feature_tests/dog_doc_type.out index 56e7e2817936..bf8c66edcfd5 100644 --- a/validator/testdata/feature_tests/dog_doc_type.out +++ b/validator/testdata/feature_tests/dog_doc_type.out @@ -1,5 +1,5 @@ FAIL feature_tests/dog_doc_type.html:23:0 The attribute '🐶' may not appear in tag 'html doctype'. [DISALLOWED_HTML] -feature_tests/dog_doc_type.html:24:0 The mandatory attribute '⚡' is missing in tag 'html ⚡ for top-level html'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/dog_doc_type.html:24:0 The mandatory attribute '⚡' is missing in tag 'html ⚡ for top-level html'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html doctype' is missing or incorrect. [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out index cc6a10bcc256..71e8dec0fab9 100644 --- a/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out +++ b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out @@ -1,4 +1,4 @@ FAIL -feature_tests/duplicate_unique_tags_and_wrong_parents.html:31:2 The tag 'author stylesheet' appears more than once in the document. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#stylesheets) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 The attribute 'amp-custom' may not appear in tag 'boilerplate (js enabled) - old variant'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [DISALLOWED_HTML] -feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 The parent tag of tag 'style' is 'body', but it can only be 'head'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [DISALLOWED_HTML] +feature_tests/duplicate_unique_tags_and_wrong_parents.html:31:2 The tag 'style amp-custom' appears more than once in the document. (see https://www.ampproject.org/docs/reference/spec.html#stylesheets) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 The attribute 'amp-custom' may not appear in tag 'head > style : boilerplate - old variant'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 The parent tag of tag 'style' is 'body', but it can only be 'head'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/empty.out b/validator/testdata/feature_tests/empty.out index d3e0a98eaace..b970b19002df 100644 --- a/validator/testdata/feature_tests/empty.out +++ b/validator/testdata/feature_tests/empty.out @@ -1,12 +1,12 @@ FAIL feature_tests/empty.html:1:0 The mandatory tag 'html doctype' is missing or incorrect. [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'head' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#canon) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'body' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'noscript enclosure for boilerplate' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'boilerplate (js enabled)' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/empty.html:1:0 The mandatory tag 'boilerplate (noscript)' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'head' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'body' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'noscript enclosure for boilerplate' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'head > style : boilerplate' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'noscript > style : boilerplate' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/incorrect_custom_style.out b/validator/testdata/feature_tests/incorrect_custom_style.out index 2b60c0a13eec..3f30a6147e55 100644 --- a/validator/testdata/feature_tests/incorrect_custom_style.out +++ b/validator/testdata/feature_tests/incorrect_custom_style.out @@ -1,4 +1,4 @@ FAIL -feature_tests/incorrect_custom_style.html:29:4 CSS syntax error in tag 'author stylesheet' - saw invalid at rule 'import'. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/incorrect_custom_style.html:33:4 CSS syntax error in tag 'author stylesheet' - saw invalid at rule 'viewport'. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/incorrect_custom_style.html:34:24 CSS syntax error in tag 'author stylesheet' - saw invalid at rule 'notallowednested'. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/incorrect_custom_style.html:29:4 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@import'. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/incorrect_custom_style.html:33:4 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@viewport'. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/incorrect_custom_style.html:34:24 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@notallowednested'. [AUTHOR_STYLESHEET_PROBLEM] diff --git a/validator/testdata/feature_tests/incorrect_mandatory_style.out b/validator/testdata/feature_tests/incorrect_mandatory_style.out index e5de918d4008..cbdfd2756ccf 100644 --- a/validator/testdata/feature_tests/incorrect_mandatory_style.out +++ b/validator/testdata/feature_tests/incorrect_mandatory_style.out @@ -1,3 +1,3 @@ FAIL -feature_tests/incorrect_mandatory_style.html:28:2 The mandatory text (CDATA) inside tag 'boilerplate (js enabled)' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/incorrect_mandatory_style.html:28:58 The mandatory text (CDATA) inside tag 'boilerplate (noscript)' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/incorrect_mandatory_style.html:28:2 The mandatory text (CDATA) inside tag 'head > style : boilerplate' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/incorrect_mandatory_style.html:28:58 The mandatory text (CDATA) inside tag 'noscript > style : boilerplate' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/link_meta_values.out b/validator/testdata/feature_tests/link_meta_values.out index 5c4228c9a05f..9d5a4c2b77ac 100644 --- a/validator/testdata/feature_tests/link_meta_values.out +++ b/validator/testdata/feature_tests/link_meta_values.out @@ -1,4 +1,4 @@ FAIL -feature_tests/link_meta_values.html:32:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'content-disposition'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [DISALLOWED_HTML] +feature_tests/link_meta_values.html:32:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'content-disposition'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] feature_tests/link_meta_values.html:33:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'unknown'. [DISALLOWED_HTML] -feature_tests/link_meta_values.html:43:2 The mandatory attribute 'rel' is missing in tag 'link rel='. [DISALLOWED_HTML] +feature_tests/link_meta_values.html:43:2 The relative URL 'foo' for attribute 'href' in tag 'link rel=mask-icon' is disallowed. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/mandatory_dimensions.html b/validator/testdata/feature_tests/mandatory_dimensions.html index 567091e2a3ba..d07b20f2f851 100644 --- a/validator/testdata/feature_tests/mandatory_dimensions.html +++ b/validator/testdata/feature_tests/mandatory_dimensions.html @@ -68,10 +68,10 @@ - - - - + + + + @@ -84,17 +84,17 @@ - + - + - + - + - + diff --git a/validator/testdata/feature_tests/mandatory_dimensions.out b/validator/testdata/feature_tests/mandatory_dimensions.out index 9ad5eee51e85..c424f7b6b79a 100644 --- a/validator/testdata/feature_tests/mandatory_dimensions.out +++ b/validator/testdata/feature_tests/mandatory_dimensions.out @@ -22,12 +22,12 @@ feature_tests/mandatory_dimensions.html:141:4 The attribute 'srcset' may not app feature_tests/mandatory_dimensions.html:142:4 The attribute 'srcset' may not appear in tag 'amp-instagram'. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:143:4 The attribute 'src' may not appear in tag 'amp-lightbox'. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:146:4 The attribute 'foo' may not appear in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-fit-text extension .js script' tag is missing or incorrect, but required by 'amp-fit-text'. (see https://www.ampproject.org/docs/reference/extended/amp-fit-text.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-carousel extension .js script' tag is missing or incorrect, but required by 'amp-carousel'. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-anim extension .js script' tag is missing or incorrect, but required by 'amp-anim'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-youtube extension .js script' tag is missing or incorrect, but required by 'amp-youtube'. (see https://www.ampproject.org/docs/reference/extended/amp-youtube.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-twitter extension .js script' tag is missing or incorrect, but required by 'amp-twitter'. (see https://www.ampproject.org/docs/reference/extended/amp-twitter.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-instagram extension .js script' tag is missing or incorrect, but required by 'amp-instagram'. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-iframe extension .js script' tag is missing or incorrect, but required by 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-audio extension .js script' tag is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/mandatory_dimensions.html:150:7 The 'amp-lightbox extension .js script' tag is missing or incorrect, but required by 'amp-lightbox'. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-fit-text extension .js script' is missing or incorrect, but required by 'amp-fit-text'. (see https://www.ampproject.org/docs/reference/extended/amp-fit-text.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-carousel extension .js script' is missing or incorrect, but required by 'amp-carousel'. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-anim extension .js script' is missing or incorrect, but required by 'amp-anim'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-youtube extension .js script' is missing or incorrect, but required by 'amp-youtube'. (see https://www.ampproject.org/docs/reference/extended/amp-youtube.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-twitter extension .js script' is missing or incorrect, but required by 'amp-twitter'. (see https://www.ampproject.org/docs/reference/extended/amp-twitter.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-instagram extension .js script' is missing or incorrect, but required by 'amp-instagram'. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-iframe extension .js script' is missing or incorrect, but required by 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-lightbox extension .js script' is missing or incorrect, but required by 'amp-lightbox'. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/mask-icon.html b/validator/testdata/feature_tests/mask-icon.html new file mode 100644 index 000000000000..1fb758adb1e2 --- /dev/null +++ b/validator/testdata/feature_tests/mask-icon.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + +Hello, world. + + diff --git a/validator/testdata/feature_tests/mask-icon.out b/validator/testdata/feature_tests/mask-icon.out new file mode 100644 index 000000000000..df86c47a1bd7 --- /dev/null +++ b/validator/testdata/feature_tests/mask-icon.out @@ -0,0 +1,4 @@ +FAIL +feature_tests/mask-icon.html:32:2 Invalid URL protocol 'http:' for attribute 'href' in tag 'link rel=mask-icon'. [DISALLOWED_HTML] +feature_tests/mask-icon.html:33:2 The relative URL '//example.com/website_icon.svg' for attribute 'href' in tag 'link rel=mask-icon' is disallowed. [DISALLOWED_HTML] +feature_tests/mask-icon.html:34:2 The relative URL 'website_icon.svg' for attribute 'href' in tag 'link rel=mask-icon' is disallowed. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/new_and_old_boilerplate_mixed.out b/validator/testdata/feature_tests/new_and_old_boilerplate_mixed.out index 30b2160edd02..f0a5fed39b70 100644 --- a/validator/testdata/feature_tests/new_and_old_boilerplate_mixed.out +++ b/validator/testdata/feature_tests/new_and_old_boilerplate_mixed.out @@ -1,2 +1,3 @@ FAIL -feature_tests/new_and_old_boilerplate_mixed.html:34:7 The 'boilerplate (noscript)' tag is missing or incorrect, but required by 'boilerplate (js enabled)'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [GENERIC] +feature_tests/new_and_old_boilerplate_mixed.html:28:12 The tag 'noscript > style : boilerplate - old variant' is deprecated - use 'noscript > style : boilerplate' instead. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DEPRECATION] +feature_tests/new_and_old_boilerplate_mixed.html:34:7 The tag 'noscript > style : boilerplate' is missing or incorrect, but required by 'head > style : boilerplate'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [GENERIC] diff --git a/validator/testdata/feature_tests/new_and_old_boilerplate_mixed2.out b/validator/testdata/feature_tests/new_and_old_boilerplate_mixed2.out index 3fc0f2881d6b..a0a23f776c94 100644 --- a/validator/testdata/feature_tests/new_and_old_boilerplate_mixed2.out +++ b/validator/testdata/feature_tests/new_and_old_boilerplate_mixed2.out @@ -1,2 +1,3 @@ FAIL -feature_tests/new_and_old_boilerplate_mixed2.html:35:7 The 'boilerplate (js enabled)' tag is missing or incorrect, but required by 'boilerplate (noscript)'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [GENERIC] +feature_tests/new_and_old_boilerplate_mixed2.html:28:2 The tag 'head > style : boilerplate - old variant' is deprecated - use 'head > style : boilerplate' instead. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DEPRECATION] +feature_tests/new_and_old_boilerplate_mixed2.html:35:7 The tag 'head > style : boilerplate' is missing or incorrect, but required by 'noscript > style : boilerplate'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [GENERIC] diff --git a/validator/testdata/feature_tests/no_custom_js.out b/validator/testdata/feature_tests/no_custom_js.out index 813659e519f3..a15c5da59727 100644 --- a/validator/testdata/feature_tests/no_custom_js.out +++ b/validator/testdata/feature_tests/no_custom_js.out @@ -1,3 +1,3 @@ FAIL -feature_tests/no_custom_js.html:28:3 The attribute 'src' in tag 'amphtml engine v0.js script' is set to the invalid value 'https://example.com/v0-not-allowed.js'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt) [CUSTOM_JAVASCRIPT_DISALLOWED] -feature_tests/no_custom_js.html:29:3 The attribute 'custom-element' in tag 'amp-access extension .js script' is set to the invalid value 'amp-foo'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-access/amp-access.md) [AMP_TAG_PROBLEM] +feature_tests/no_custom_js.html:28:3 The attribute 'src' in tag 'amphtml engine v0.js script' is set to the invalid value 'https://example.com/v0-not-allowed.js'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [CUSTOM_JAVASCRIPT_DISALLOWED] +feature_tests/no_custom_js.html:29:3 The attribute 'custom-element' in tag 'amp-access extension .js script' is set to the invalid value 'amp-foo'. (see https://www.ampproject.org/docs/reference/extended/amp-access.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/noscript.html b/validator/testdata/feature_tests/noscript.html index 09c24601e281..54f4f08e067d 100644 --- a/validator/testdata/feature_tests/noscript.html +++ b/validator/testdata/feature_tests/noscript.html @@ -30,11 +30,11 @@ @@ -49,13 +49,13 @@ - Iconic Lemurs + Iconic Lemurs diff --git a/validator/testdata/feature_tests/noscript.out b/validator/testdata/feature_tests/noscript.out index 04fb0dae5cfb..28f08ead6ac6 100644 --- a/validator/testdata/feature_tests/noscript.out +++ b/validator/testdata/feature_tests/noscript.out @@ -5,4 +5,4 @@ feature_tests/noscript.html:48:4 The mandatory attribute 'src' is missing in tag feature_tests/noscript.html:51:2 The tag 'audio' may only appear as a descendant of tag 'noscript'. Did you mean 'amp-audio'? (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] feature_tests/noscript.html:55:2 The tag 'img' may only appear as a descendant of tag 'noscript'. Did you mean 'amp-img'? (see https://www.ampproject.org/docs/reference/amp-img.html) [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] feature_tests/noscript.html:57:2 The tag 'video' may only appear as a descendant of tag 'noscript'. Did you mean 'amp-video'? (see https://www.ampproject.org/docs/reference/amp-video.html) [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] -feature_tests/noscript.html:61:13 The parent tag of tag 'noscript' is 'noscript', but it can only be 'head'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [DISALLOWED_HTML] +feature_tests/noscript.html:61:13 The parent tag of tag 'noscript' is 'noscript', but it can only be 'head'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/regexps.html b/validator/testdata/feature_tests/regexps.html index 82ca5b7c2a69..eab546527d20 100644 --- a/validator/testdata/feature_tests/regexps.html +++ b/validator/testdata/feature_tests/regexps.html @@ -72,11 +72,11 @@ autoplay value_regex: "^$|desktop|tablet|mobile" The first two examples are valid, the latter three examples are invalid. --> - - - - - + + + + + diff --git a/validator/testdata/feature_tests/regexps.out b/validator/testdata/feature_tests/regexps.out index 51cdb091581f..330343b2de07 100644 --- a/validator/testdata/feature_tests/regexps.out +++ b/validator/testdata/feature_tests/regexps.out @@ -1,15 +1,16 @@ FAIL -feature_tests/regexps.html:27:2 The mandatory text (CDATA) inside tag 'boilerplate (js enabled) - old variant' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/regexps.html:27:2 The tag 'head > style : boilerplate - old variant' is deprecated - use 'head > style : boilerplate' instead. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DEPRECATION] +feature_tests/regexps.html:27:2 The mandatory text (CDATA) inside tag 'head > style : boilerplate - old variant' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/regexps.html:36:2 The attribute 'src' in tag 'amp-vine extension .js script' is set to the invalid value 'https://cdn.ampproject.org/v0/amp-vine-0.1.js?foobar'. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [CUSTOM_JAVASCRIPT_DISALLOWED] feature_tests/regexps.html:37:2 The attribute 'src' in tag 'amp-vine extension .js script' is set to the invalid value 'http://xss.com/https://cdn.ampproject.org/v0/amp-vine-0.1.js?foobar'. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [CUSTOM_JAVASCRIPT_DISALLOWED] -feature_tests/regexps.html:45:2 The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'http://xss.com/https://fonts.googleapis.com/css?foobar'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#custom-fonts) [DISALLOWED_HTML] +feature_tests/regexps.html:45:2 The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'http://xss.com/https://fonts.googleapis.com/css?foobar'. (see https://www.ampproject.org/docs/reference/spec.html#custom-fonts) [DISALLOWED_HTML] feature_tests/regexps.html:55:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'foo'. [DISALLOWED_HTML] feature_tests/regexps.html:56:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'accessibility foo'. [DISALLOWED_HTML] feature_tests/regexps.html:57:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'foo accessibility'. [DISALLOWED_HTML] -feature_tests/regexps.html:65:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'content-disposition'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [DISALLOWED_HTML] -feature_tests/regexps.html:66:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'invalid content-disposition'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [DISALLOWED_HTML] +feature_tests/regexps.html:65:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'content-disposition'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +feature_tests/regexps.html:66:2 The attribute 'name' in tag 'meta name=viewport' is set to the invalid value 'invalid content-disposition'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] feature_tests/regexps.html:77:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'invalid'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] feature_tests/regexps.html:78:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'desktopfoo'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] feature_tests/regexps.html:79:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'foodesktop'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/regexps.html:82:7 The 'boilerplate (js enabled)' tag is missing or incorrect, but required by 'boilerplate (noscript)'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate) [GENERIC] -feature_tests/regexps.html:82:7 The 'amp-audio extension .js script' tag is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:82:7 The tag 'head > style : boilerplate' is missing or incorrect, but required by 'noscript > style : boilerplate'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [GENERIC] +feature_tests/regexps.html:82:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/several_errors.out b/validator/testdata/feature_tests/several_errors.out index 9f2a0550985b..14de1cded2a8 100644 --- a/validator/testdata/feature_tests/several_errors.out +++ b/validator/testdata/feature_tests/several_errors.out @@ -1,9 +1,9 @@ FAIL -feature_tests/several_errors.html:23:2 The attribute 'charset' in tag 'meta charset=utf-8' is set to the invalid value 'pick-a-key'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs) [DISALLOWED_HTML] +feature_tests/several_errors.html:23:2 The attribute 'charset' in tag 'meta charset=utf-8' is set to the invalid value 'pick-a-key'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] feature_tests/several_errors.html:26:2 The attribute 'type' in tag 'script type=application/ld+json' is set to the invalid value 'javascript'. [CUSTOM_JAVASCRIPT_DISALLOWED] feature_tests/several_errors.html:32:2 The mandatory attribute 'height' is missing in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] feature_tests/several_errors.html:34:2 The attribute 'width' in tag 'amp-ad' is set to the invalid value '100%'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_LAYOUT_PROBLEM] feature_tests/several_errors.html:37:2 The attribute 'made_up_attribute' may not appear in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] -feature_tests/several_errors.html:39:7 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/several_errors.html:39:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/several_errors.html:39:7 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:39:7 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:39:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:39:7 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/slash_attrs.html b/validator/testdata/feature_tests/slash_attrs.html new file mode 100644 index 000000000000..63c1a29ba292 --- /dev/null +++ b/validator/testdata/feature_tests/slash_attrs.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +

+

+

+

+

+ + + diff --git a/validator/testdata/feature_tests/slash_attrs.out b/validator/testdata/feature_tests/slash_attrs.out new file mode 100644 index 000000000000..1e883b84b128 --- /dev/null +++ b/validator/testdata/feature_tests/slash_attrs.out @@ -0,0 +1,5 @@ +FAIL +feature_tests/slash_attrs.html:33:2 The attribute 'data-\this-is-not' may not appear in tag 'p'. [DISALLOWED_HTML] +feature_tests/slash_attrs.html:34:2 The attribute 'data-/this-is-not' may not appear in tag 'p'. [DISALLOWED_HTML] +feature_tests/slash_attrs.html:35:2 The attribute 'data-\this-is-not' may not appear in tag 'p'. [DISALLOWED_HTML] +feature_tests/slash_attrs.html:36:2 The attribute 'data-/this-is-not' may not appear in tag 'p'. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/slash_attrs.reserialized b/validator/testdata/feature_tests/slash_attrs.reserialized new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/validator/testdata/feature_tests/soundcloud.out b/validator/testdata/feature_tests/soundcloud.out index ea7a7400fa3d..3583d5a839f1 100644 --- a/validator/testdata/feature_tests/soundcloud.out +++ b/validator/testdata/feature_tests/soundcloud.out @@ -1,4 +1,4 @@ FAIL -feature_tests/soundcloud.html:39:2 The attribute 'data-visual' in tag 'amp-soundcloud' is set to the invalid value ''. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-soundcloud/amp-soundcloud.md) [AMP_TAG_PROBLEM] -feature_tests/soundcloud.html:42:2 The attribute 'data-trackid' in tag 'amp-soundcloud' is set to the invalid value 'mahler_number_6.ogg'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-soundcloud/amp-soundcloud.md) [AMP_TAG_PROBLEM] -feature_tests/soundcloud.html:45:2 The specified layout 'RESPONSIVE' is not supported by tag 'amp-soundcloud'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-soundcloud/amp-soundcloud.md) [AMP_LAYOUT_PROBLEM] +feature_tests/soundcloud.html:39:2 The attribute 'data-visual' in tag 'amp-soundcloud' is set to the invalid value ''. (see https://www.ampproject.org/docs/reference/extended/amp-soundcloud.html) [AMP_TAG_PROBLEM] +feature_tests/soundcloud.html:42:2 The attribute 'data-trackid' in tag 'amp-soundcloud' is set to the invalid value 'mahler_number_6.ogg'. (see https://www.ampproject.org/docs/reference/extended/amp-soundcloud.html) [AMP_TAG_PROBLEM] +feature_tests/soundcloud.html:45:2 The specified layout 'RESPONSIVE' is not supported by tag 'amp-soundcloud'. (see https://www.ampproject.org/docs/reference/extended/amp-soundcloud.html) [AMP_LAYOUT_PROBLEM] diff --git a/validator/testdata/feature_tests/template.html b/validator/testdata/feature_tests/template.html index 2a305def3c8a..3370ea9849c6 100644 --- a/validator/testdata/feature_tests/template.html +++ b/validator/testdata/feature_tests/template.html @@ -104,9 +104,9 @@

- + diff --git a/validator/testdata/feature_tests/template.out b/validator/testdata/feature_tests/template.out index c797ae57ad07..1ff12aa54eba 100644 --- a/validator/testdata/feature_tests/template.out +++ b/validator/testdata/feature_tests/template.out @@ -1,38 +1,38 @@ FAIL -feature_tests/template.html:34:2 Mustache template syntax in attribute name '{{notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:35:2 Mustache template syntax in attribute name '{{notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:34:2 Mustache template syntax in attribute name '{{notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:35:2 Mustache template syntax in attribute name '{{notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] feature_tests/template.html:36:2 The attribute 'data-{notallowed}' may not appear in tag 'p'. [DISALLOWED_HTML] -feature_tests/template.html:37:2 Mustache template syntax in attribute name 'data-{{notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:38:2 Mustache template syntax in attribute name 'data-{{{notallowed}}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:39:2 Mustache template syntax in attribute name 'data-{{¬allowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:40:2 Mustache template syntax in attribute name 'data-{{#notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:41:2 Mustache template syntax in attribute name 'data-{{/notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:42:2 Mustache template syntax in attribute name 'data-{{^notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:43:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:44:2 Mustache template syntax in attribute name '{{#notallowed}}class' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:45:2 Mustache template syntax in attribute name '{{#notallowed}}class{{/notallowed}}' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:46:2 The attribute 'title' in tag 'p' is set to '{{{notallowed}}}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:47:2 The attribute 'title' in tag 'p' is set to '{{¬allowed}}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:48:2 The attribute 'title' in tag 'p' is set to '{{>notallowed}}', which contains a Mustache template partial. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:53:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:54:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:55:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:56:2 Mustache template syntax in attribute name 'data-{{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:57:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:58:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:59:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:60:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:61:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:62:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:63:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:64:2 The attribute 'title' in tag 'p' is set to '{{{ notallowed }}}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:65:2 The attribute 'title' in tag 'p' is set to '{{ ¬allowed }}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:66:2 The attribute 'title' in tag 'p' is set to '{{ >notallowed }}', which contains a Mustache template partial. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:67:2 The attribute 'title' in tag 'p' is set to '{{& notallowed }}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:68:2 The attribute 'title' in tag 'p' is set to '{{> notallowed }}', which contains a Mustache template partial. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:69:2 The attribute 'title' in tag 'p' is set to '{{ & notallowed }}', which contains unescaped Mustache template syntax. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:70:2 The attribute 'title' in tag 'p' is set to '{{ > notallowed }}', which contains a Mustache template partial. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] -feature_tests/template.html:95:4 The tag 'template' may not appear as a descendant of tag 'template'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:37:2 Mustache template syntax in attribute name 'data-{{notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:38:2 Mustache template syntax in attribute name 'data-{{{notallowed}}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:39:2 Mustache template syntax in attribute name 'data-{{¬allowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:40:2 Mustache template syntax in attribute name 'data-{{#notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:41:2 Mustache template syntax in attribute name 'data-{{/notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:42:2 Mustache template syntax in attribute name 'data-{{^notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:43:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:44:2 Mustache template syntax in attribute name '{{#notallowed}}class' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:45:2 Mustache template syntax in attribute name '{{#notallowed}}class{{/notallowed}}' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:46:2 The attribute 'title' in tag 'p' is set to '{{{notallowed}}}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:47:2 The attribute 'title' in tag 'p' is set to '{{¬allowed}}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:48:2 The attribute 'title' in tag 'p' is set to '{{>notallowed}}', which contains a Mustache template partial. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:53:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:54:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:55:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:56:2 Mustache template syntax in attribute name 'data-{{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:57:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:58:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:59:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:60:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:61:2 Mustache template syntax in attribute name 'data-{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:62:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:63:2 Mustache template syntax in attribute name '{{' in tag 'p'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:64:2 The attribute 'title' in tag 'p' is set to '{{{ notallowed }}}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:65:2 The attribute 'title' in tag 'p' is set to '{{ ¬allowed }}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:66:2 The attribute 'title' in tag 'p' is set to '{{ >notallowed }}', which contains a Mustache template partial. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:67:2 The attribute 'title' in tag 'p' is set to '{{& notallowed }}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:68:2 The attribute 'title' in tag 'p' is set to '{{> notallowed }}', which contains a Mustache template partial. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:69:2 The attribute 'title' in tag 'p' is set to '{{ & notallowed }}', which contains unescaped Mustache template syntax. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:70:2 The attribute 'title' in tag 'p' is set to '{{ > notallowed }}', which contains a Mustache template partial. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] +feature_tests/template.html:95:4 The tag 'template' may not appear as a descendant of tag 'template'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_MUSTACHE_TEMPLATE_PROBLEM] feature_tests/template.html:107:0 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value '{{invalid}}'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/template.html:112:7 The 'amp-mustache extension .js script' tag is missing or incorrect, but required by 'template'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md) [AMP_TAG_PROBLEM] -feature_tests/template.html:112:7 The 'amp-audio extension .js script' tag is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/template.html:112:7 The tag 'amp-mustache extension .js script' is missing or incorrect, but required by 'template'. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [AMP_TAG_PROBLEM] +feature_tests/template.html:112:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/urls.out b/validator/testdata/feature_tests/urls.out index 06c9dda513ee..1889595bf32e 100644 --- a/validator/testdata/feature_tests/urls.out +++ b/validator/testdata/feature_tests/urls.out @@ -1,5 +1,5 @@ FAIL -feature_tests/urls.html:25:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'link rel=canonical'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#canon) [DISALLOWED_HTML] +feature_tests/urls.html:25:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'link rel=canonical'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] feature_tests/urls.html:39:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] feature_tests/urls.html:40:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] feature_tests/urls.html:41:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] @@ -24,4 +24,4 @@ feature_tests/urls.html:63:2 Invalid URL protocol 'http:' for attribute 'src' in feature_tests/urls.html:64:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:65:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-pixel'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:66:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-video'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:68:7 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#canon) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/urls.html:68:7 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/urls_in_css.html b/validator/testdata/feature_tests/urls_in_css.html new file mode 100644 index 000000000000..6e6d6dd4180c --- /dev/null +++ b/validator/testdata/feature_tests/urls_in_css.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +Hello, world. + + diff --git a/validator/testdata/feature_tests/urls_in_css.out b/validator/testdata/feature_tests/urls_in_css.out new file mode 100644 index 000000000000..c5453625d01c --- /dev/null +++ b/validator/testdata/feature_tests/urls_in_css.out @@ -0,0 +1,2 @@ +FAIL +feature_tests/urls_in_css.html:32:11 CSS syntax error in tag 'style amp-custom' - bad url. [AUTHOR_STYLESHEET_PROBLEM] diff --git a/validator/testdata/feature_tests/vimeo.html b/validator/testdata/feature_tests/vimeo.html index 72a9f4fbf167..5056b226420d 100644 --- a/validator/testdata/feature_tests/vimeo.html +++ b/validator/testdata/feature_tests/vimeo.html @@ -36,15 +36,18 @@

Vimeo

- - + + + + - + - + + + + diff --git a/validator/testdata/feature_tests/vimeo.out b/validator/testdata/feature_tests/vimeo.out index 3e2e464996ab..213169d508ff 100644 --- a/validator/testdata/feature_tests/vimeo.out +++ b/validator/testdata/feature_tests/vimeo.out @@ -1,3 +1,4 @@ FAIL -feature_tests/vimeo.html:43:0 The attribute 'data-videoid' in tag 'amp-vimeo' is set to the invalid value 'i don't think so'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-vimeo/amp-vimeo.md) [AMP_TAG_PROBLEM] -feature_tests/vimeo.html:47:0 The mandatory attribute 'data-videoid' is missing in tag 'amp-vimeo'. (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-vimeo/amp-vimeo.md) [AMP_TAG_PROBLEM] +feature_tests/vimeo.html:45:0 The attribute 'data-videoid' in tag 'amp-vimeo' is set to the invalid value 'i don't think so'. (see https://www.ampproject.org/docs/reference/extended/amp-vimeo.html) [AMP_TAG_PROBLEM] +feature_tests/vimeo.html:48:0 The mandatory attribute 'data-videoid' is missing in tag 'amp-vimeo'. (see https://www.ampproject.org/docs/reference/extended/amp-vimeo.html) [AMP_TAG_PROBLEM] +feature_tests/vimeo.html:51:0 The attribute 'noloading' in tag 'amp-vimeo' is set to the invalid value 'foo'. (see https://www.ampproject.org/docs/reference/extended/amp-vimeo.html) [AMP_TAG_PROBLEM] diff --git a/validator/tokenize-css.js b/validator/tokenize-css.js index ba52e41e0c2d..b0f359e0cfa3 100644 --- a/validator/tokenize-css.js +++ b/validator/tokenize-css.js @@ -1023,7 +1023,10 @@ parse_css.TokenType = { QUALIFIED_RULE: 'QUALIFIED_RULE', DECLARATION: 'DECLARATION', BLOCK: 'BLOCK', - FUNCTION: 'FUNCTION' + FUNCTION: 'FUNCTION', + + // For ExtractUrls + PARSED_CSS_URL: 'PARSED_CSS_URL' }; /** diff --git a/validator/validator.js b/validator/validator.js index 32cd5d84feff..6c852d6dca9a 100644 --- a/validator/validator.js +++ b/validator/validator.js @@ -42,7 +42,9 @@ goog.require('goog.structs.Map'); goog.require('goog.structs.Set'); goog.require('goog.uri.utils'); goog.require('parse_css.BlockType'); +goog.require('parse_css.ParsedCssUrl'); goog.require('parse_css.RuleVisitor'); +goog.require('parse_css.extractUrls'); goog.require('parse_css.parseAStylesheet'); goog.require('parse_css.tokenize'); @@ -144,71 +146,75 @@ function specificity(code) { return 14; case amp.validator.ValidationError.Code.DUPLICATE_UNIQUE_TAG: return 15; - case amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG: + case amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG_OLD_VARIANT: return 16; - case amp.validator.ValidationError.Code.CSS_SYNTAX: + case amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG: return 17; - case amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_AT_RULE: + case amp.validator.ValidationError.Code.CSS_SYNTAX: return 18; + case amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_AT_RULE: + return 19; case amp.validator.ValidationError.Code. MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE: - return 19; + return 20; case amp.validator.ValidationError.Code. INVALID_PROPERTY_VALUE_IN_ATTR_VALUE: - return 20; - case amp.validator.ValidationError.Code.DISALLOWED_PROPERTY_IN_ATTR_VALUE: return 21; - case amp.validator.ValidationError.Code.MUTUALLY_EXCLUSIVE_ATTRS: + case amp.validator.ValidationError.Code.DISALLOWED_PROPERTY_IN_ATTR_VALUE: return 22; - case amp.validator.ValidationError.Code.UNESCAPED_TEMPLATE_IN_ATTR_VALUE: + case amp.validator.ValidationError.Code.MUTUALLY_EXCLUSIVE_ATTRS: return 23; - case amp.validator.ValidationError.Code.TEMPLATE_PARTIAL_IN_ATTR_VALUE: + case amp.validator.ValidationError.Code.UNESCAPED_TEMPLATE_IN_ATTR_VALUE: return 24; - case amp.validator.ValidationError.Code.TEMPLATE_IN_ATTR_NAME: + case amp.validator.ValidationError.Code.TEMPLATE_PARTIAL_IN_ATTR_VALUE: return 25; + case amp.validator.ValidationError.Code.TEMPLATE_IN_ATTR_NAME: + return 26; case amp.validator.ValidationError.Code. INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT: - return 26; - case amp.validator.ValidationError.Code.IMPLIED_LAYOUT_INVALID: return 27; - case amp.validator.ValidationError.Code.SPECIFIED_LAYOUT_INVALID: + case amp.validator.ValidationError.Code.IMPLIED_LAYOUT_INVALID: return 28; - case amp.validator.ValidationError.Code.DEV_MODE_ENABLED: + case amp.validator.ValidationError.Code.SPECIFIED_LAYOUT_INVALID: return 29; - case amp.validator.ValidationError.Code.ATTR_DISALLOWED_BY_IMPLIED_LAYOUT: + case amp.validator.ValidationError.Code.DEV_MODE_ENABLED: return 30; - case amp.validator.ValidationError.Code.ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT: + case amp.validator.ValidationError.Code.ATTR_DISALLOWED_BY_IMPLIED_LAYOUT: return 31; - case amp.validator.ValidationError.Code.MISSING_URL: + case amp.validator.ValidationError.Code.ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT: return 32; - case amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL: + case amp.validator.ValidationError.Code.DISALLOWED_RELATIVE_URL: return 33; - case amp.validator.ValidationError.Code.INVALID_URL: + case amp.validator.ValidationError.Code.MISSING_URL: return 34; - case amp.validator.ValidationError.Code.CSS_SYNTAX_STRAY_TRAILING_BACKSLASH: + case amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL: return 35; - case amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_COMMENT: + case amp.validator.ValidationError.Code.INVALID_URL: return 36; - case amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_STRING: + case amp.validator.ValidationError.Code.CSS_SYNTAX_STRAY_TRAILING_BACKSLASH: return 37; - case amp.validator.ValidationError.Code.CSS_SYNTAX_BAD_URL: + case amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_COMMENT: return 38; + case amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_STRING: + return 39; + case amp.validator.ValidationError.Code.CSS_SYNTAX_BAD_URL: + return 40; case amp.validator.ValidationError.Code .CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE: - return 39; + return 41; case amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_DECLARATION: - return 40; + return 42; case amp.validator.ValidationError.Code.CSS_SYNTAX_INCOMPLETE_DECLARATION: - return 41; + return 43; case amp.validator.ValidationError.Code.CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR: - return 42; + return 44; case amp.validator.ValidationError.Code.CSS_SYNTAX_MISSING_SELECTOR: - return 43; + return 45; case amp.validator.ValidationError.Code.CSS_SYNTAX_NOT_A_SELECTOR_START: - return 44; + return 46; case amp.validator.ValidationError.Code. CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR: - return 45; + return 47; case amp.validator.ValidationError.Code.DEPRECATED_ATTR: return 101; @@ -593,7 +599,8 @@ class CdataMatcher { const bytes = byteLength(cdata); if (bytes > cdataSpec.maxBytes) { context.addError(amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG, - /* params */ [bytes, cdataSpec.maxBytes], + /* params */ [getDetailOrName(this.tagSpec_), + bytes, cdataSpec.maxBytes], cdataSpec.maxBytesSpecUrl, validationResult); // We return early if the byte length is violated as parsing // really long stylesheets is slow and not worth our time. @@ -645,6 +652,13 @@ class CdataMatcher { const sheet = parse_css.parseAStylesheet( tokenList, atRuleParsingSpec, computeAtRuleDefaultParsingSpec(atRuleParsingSpec), cssErrors); + + // We extract the urls from the stylesheet. As a side-effect, this can + // generate errors for url(…) functions with invalid parameters. + /** @type {!Array} */ + const parsedUrls = []; + parse_css.extractUrls(sheet, parsedUrls, cssErrors); + for (const errorToken of cssErrors) { // Override the first parameter with the name of this style tag. let params = errorToken.params; @@ -975,12 +989,15 @@ class ParsedAttrSpec { // open-source version uses). // begin oneof { if (this.spec_.value !== null) { - if (attrValue != this.spec_.value) { - context.addError( - amp.validator.ValidationError.Code.INVALID_ATTR_VALUE, - /* params */ [attrName, getDetailOrName(tagSpec), attrValue], - tagSpec.specUrl, result); - } + if (attrValue == this.spec_.value) { return; } + // Allow spec's with value: "" to also be equal to their attribute + // name (e.g. script's spec: async has value: "" so both + // async and async="async" is okay in a script tag). + if ((this.spec_.value == "") && (attrValue == attrName)) { return; } + context.addError( + amp.validator.ValidationError.Code.INVALID_ATTR_VALUE, + /* params */ [attrName, getDetailOrName(tagSpec), attrValue], + tagSpec.specUrl, result); } else if (this.spec_.valueRegex !== null) { const valueRegex = new RegExp('^(' + this.spec_.valueRegex + ')$'); @@ -1026,11 +1043,20 @@ class ParsedAttrSpec { return; } if (uri.hasScheme() && - !this.valueUrlAllowedProtocols_.contains(uri.getScheme().toLowerCase())) { + !this.valueUrlAllowedProtocols_.contains( + uri.getScheme().toLowerCase())) { context.addError( amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL, - /* params */ [attrName, getDetailOrName(tagSpec), - uri.getScheme().toLowerCase()], tagSpec.specUrl, result); + /* params */ + [attrName, getDetailOrName(tagSpec), uri.getScheme().toLowerCase()], + tagSpec.specUrl, result); + return; + } + if (!this.spec_.valueUrl.allowRelative && !uri.hasScheme()) { + context.addError( + amp.validator.ValidationError.Code.DISALLOWED_RELATIVE_URL, + /* params */[attrName, getDetailOrName(tagSpec), url], + tagSpec.specUrl, result); return; } } @@ -1692,9 +1718,13 @@ class ParsedTagSpec { // However, mostly to avoid confusion, we want to make sure that // nobody tries to make a Mustache template data attribute, // e.g.
, so we also exclude those characters. + // We also don't allow slashes as they can be parsed differently by + // different clients. if (goog.string./*OK*/startsWith(attrName, 'data-') && !goog.string.contains(attrName, '}') && - !goog.string.contains(attrName, '{')) { + !goog.string.contains(attrName, '{') && + !goog.string.contains(attrName, '/') && + !goog.string.contains(attrName, '\\')) { return; } @@ -1765,11 +1795,6 @@ class ParsedTagSpec { const attrName = attrKey.toLowerCase(); let attrValue = encounteredAttrs[i + 1]; - // Our html parser repeats the key as the value if there is no value. We - // replace the value with an empty string instead in this case. - if (attrKey === attrValue) - attrValue = ''; - const parsedSpec = this.attrsByName_.get(attrName); if (parsedSpec === undefined) { this.validateAttrNotFoundInSpec(context, attrName, result); @@ -2103,12 +2128,21 @@ class ParsedValidatorRules { resultForBestAttempt.status = resultForAttempt.status; resultForBestAttempt.errors = resultForAttempt.errors; + const spec = parsedSpec.getSpec(); + + if (spec.deprecation !== null) { + context.addError( + amp.validator.ValidationError.Code.DEPRECATED_TAG, + /* params */ [getDetailOrName(spec), spec.deprecation], + spec.deprecationUrl, resultForBestAttempt); + // Deprecation is only a warning, so we don't return. + } + if (parsedSpec.shouldRecordTagspecValidated()) { const isUnique = context.recordTagspecValidated(parsedSpec.getId()); // If a duplicate tag is encountered for a spec that's supposed // to be unique, we've found an error that we must report. - if (parsedSpec.getSpec().unique && !isUnique) { - const spec = parsedSpec.getSpec(); + if (spec.unique && !isUnique) { context.addError( amp.validator.ValidationError.Code.DUPLICATE_UNIQUE_TAG, /* params */ [getDetailOrName(spec)], spec.specUrl, @@ -2117,14 +2151,14 @@ class ParsedValidatorRules { } } - if (parsedSpec.getSpec().mandatoryAlternatives !== null) { - const satisfied = parsedSpec.getSpec().mandatoryAlternatives; + if (spec.mandatoryAlternatives !== null) { + const satisfied = spec.mandatoryAlternatives; goog.asserts.assert(satisfied !== null); context.recordMandatoryAlternativeSatisfied(satisfied); } // (Re)set the cdata matcher to the expectations that this tag // brings with it. - context.setCdataMatcher(new CdataMatcher(parsedSpec.getSpec())); + context.setCdataMatcher(new CdataMatcher(spec)); } /** @@ -2520,21 +2554,21 @@ amp.validator.categorizeError = function(error) { } return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; } - // E.g. "The text (CDATA) inside tag 'author stylesheet' matches + // E.g. "The text (CDATA) inside tag 'style amp-custom' matches // 'CSS !important', which is disallowed." if (error.code === amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG || (error.code === amp.validator.ValidationError.Code.CDATA_VIOLATES_BLACKLIST - && error.params[0] === "author stylesheet")) { + && error.params[0] === "style amp-custom")) { return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; } - // E.g. "CSS syntax error in tag 'author stylesheet' - Invalid Declaration." + // E.g. "CSS syntax error in tag 'style amp-custom' - Invalid Declaration." // TODO(powdercloud): Legacy generic css error code. Remove after 2016-06-01. if (error.code === amp.validator.ValidationError.Code.CSS_SYNTAX && - error.params[0] === "author stylesheet") { + error.params[0] === "style amp-custom") { return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; } - // E.g. "CSS syntax error in tag 'author stylesheet' - unterminated string." + // E.g. "CSS syntax error in tag 'style amp-custom' - unterminated string." if ((error.code === amp.validator.ValidationError.Code.CSS_SYNTAX_STRAY_TRAILING_BACKSLASH || error.code === @@ -2561,7 +2595,7 @@ amp.validator.categorizeError = function(error) { error.code === amp.validator.ValidationError.Code. CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR) && - error.params[0] === "author stylesheet") { + error.params[0] === "style amp-custom") { return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; } // E.g. "The mandatory tag 'boilerplate (noscript)' is missing or @@ -2572,7 +2606,10 @@ amp.validator.categorizeError = function(error) { && error.params[0] === "⚡") || (error.code === amp.validator.ValidationError. Code.MANDATORY_CDATA_MISSING_OR_INCORRECT - && goog.string./*OK*/startsWith(error.params[0], "boilerplate"))) { + && (goog.string./*OK*/startsWith( + error.params[0], "head > style : boilerplate") || + goog.string./*OK*/startsWith( + error.params[0], "noscript > style : boilerplate")))) { return amp.validator.ErrorCategory.Code. MANDATORY_AMP_TAG_MISSING_OR_INCORRECT; } @@ -2704,10 +2741,12 @@ amp.validator.categorizeError = function(error) { // E.g. "Missing URL for attribute 'href' in tag 'a'." // E.g. "Invalid URL protocol 'http:' for attribute 'src' in tag // 'amp-iframe'." Note: Parameters in the format strings appear out - // of order so that error.params(1) is the tag for all three of these. + // of order so that error.params(1) is the tag for all four of these. if (error.code == amp.validator.ValidationError.Code.MISSING_URL || error.code == amp.validator.ValidationError.Code.INVALID_URL || - error.code == amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL) { + error.code == amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL || + error.code == + amp.validator.ValidationError.Code.DISALLOWED_RELATIVE_URL) { if (goog.string./*OK*/startsWith(error.params[1], "amp-")) { return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; } diff --git a/validator/validator.proto b/validator/validator.proto index d6a4c574367e..d1b03179f578 100644 --- a/validator/validator.proto +++ b/validator/validator.proto @@ -42,6 +42,7 @@ message PropertySpecList { message UrlSpec { // allowed_protocol must be in lowercase (e.g. "javascript" not "JavaScript"). repeated string allowed_protocol = 1; + optional bool allow_relative = 2 [default = true]; } // Attributes that are not covered by at least one of these specs are @@ -82,6 +83,7 @@ message AttrSpec { // The value of the deprecation field indicates what to use instead, // e.g. the name of an attribute or tag. optional string deprecation = 7; + // If provided, a URL which links to the AMP HTML spec for this deprecation. optional string deprecation_url = 8; // If set true, the TagSpec containing this AttrSpec will be evaluated first @@ -199,7 +201,7 @@ message AmpLayout { // Some tags are mandatory. Note that the tag name is not unique, that is, // there can be multiple tag specs covering the same name, e.g., for // multiple meta tags (with different attributes). -// NEXT AVAILABLE TAG: 17 +// NEXT AVAILABLE TAG: 19 message TagSpec { // Use lower-case tag names only. If adding the same tag twice, then they must // also have a detail string which is unique throughout all detail. @@ -239,6 +241,13 @@ message TagSpec { // present as well. repeated string also_requires = 14; + // If set, generates a DEPRECATED_TAG error with severity WARNING. + // The value of the deprecation field indicates what to use instead, + // e.g. the name of a tag. + optional string deprecation = 17; + // If provided, a URL which links to the AMP HTML spec for this deprecation. + optional string deprecation_url = 18; + // Attribute specifications related to this tag. repeated AttrSpec attrs = 7; // Top level attr lists of shared tags, identified by unique key @@ -337,7 +346,7 @@ message ValidationError { DEV_WARNING = 3; // DEPRECATED DO NOT USE. } optional Severity severity = 6 [ default = ERROR ]; - // NEXT AVAILABLE TAG: 49 + // NEXT AVAILABLE TAG: 51 enum Code { UNKNOWN_CODE = 0; MANDATORY_TAG_MISSING = 1; @@ -352,7 +361,8 @@ message ValidationError { MANDATORY_ONEOF_ATTR_MISSING = 28; DUPLICATE_UNIQUE_TAG = 6; WRONG_PARENT_TAG = 7; - STYLESHEET_TOO_LONG = 8; + STYLESHEET_TOO_LONG_OLD_VARIANT = 8; + STYLESHEET_TOO_LONG = 50; MANDATORY_CDATA_MISSING_OR_INCORRECT = 9; CDATA_VIOLATES_BLACKLIST = 30; DEV_MODE_ENABLED = 10; // Deprecated, remove after 2016-04-01. @@ -363,6 +373,7 @@ message ValidationError { MISSING_URL = 35; INVALID_URL = 36; INVALID_URL_PROTOCOL = 37; + DISALLOWED_RELATIVE_URL = 49; DISALLOWED_PROPERTY_IN_ATTR_VALUE = 16; MUTUALLY_EXCLUSIVE_ATTRS = 17; UNESCAPED_TEMPLATE_IN_ATTR_VALUE = 18; diff --git a/validator/validator.protoascii b/validator/validator.protoascii index b33e87fa1d95..7320d2ef5836 100644 --- a/validator/validator.protoascii +++ b/validator/validator.protoascii @@ -20,19 +20,19 @@ # in production from crashing. This id is not relevant to validator.js # because thus far, engine (validator.js) and spec file (validator.protoascii) # are always released together. -min_validator_revision_required: 95 +min_validator_revision_required: 100 # The spec file revision allows the validator engine to distinguish # newer versions of the spec file. This is currently a Google internal # mechanism, validator.js does not use this facility. However, any # change to this file requires updating this revision id. -spec_file_revision: 161 +spec_file_revision: 173 # Rules for AMP HTML -# (https://github.com/google/amphtml/blob/master/spec/amp-html-format.md). +# (https://www.ampproject.org/docs/reference/spec.html) # # Specific URL to reference for violations having to do with mustache # templates. These are specific to any particular tag, so must live at # the top level of this rules file. -template_spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-templates.md" +template_spec_url: "https://www.ampproject.org/docs/reference/extended/amp-mustache.html" tags: { name: "!doctype" @@ -57,7 +57,7 @@ tags: { name: "html" detail: "html ⚡ for top-level html" mandatory: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" mandatory_parent: "!doctype" unique: true attrs: { @@ -75,7 +75,7 @@ tags: { mandatory: true mandatory_parent: "html" unique: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # 4.2.2 The title element tags: { @@ -109,9 +109,9 @@ tags: { value_regex: "(" "accessibility|" "alternate|" + "appendix|" "archives?|" "attachment|" - "appendix|" "author|" "bibliography|" "category|" @@ -143,7 +143,6 @@ tags: { "license|" "made|" "map|" - "mask-icon|" "me|" "meta|" "micropub|" @@ -192,9 +191,9 @@ tags: { ")( +(" "accessibility|" "alternate|" + "appendix|" "archives?|" "attachment|" - "appendix|" "author|" "bibliography|" "category|" @@ -226,7 +225,6 @@ tags: { "license|" "made|" "map|" - "mask-icon|" "me|" "meta|" "micropub|" @@ -291,12 +289,13 @@ tags: { name: "href" mandatory: true value_url: { + allow_relative: true allowed_protocol: "http" allowed_protocol: "https" } } attrs: { name: "hreflang" } - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#canon" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # rel=amphtml is designed to point from a non-amphtml page to an amp-html # page, but for ease of use and to match the syntax of rel=canonical, a @@ -318,11 +317,33 @@ tags: { } attrs: { name: "hreflang" } } +# Safari pinned tab icon +# https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/pinnedTabs/pinnedTabs.html +tags: { + name: "link" + detail: "link rel=mask-icon" + mandatory_parent: "head" + attrs: { + name: "rel" + value: "mask-icon" + mandatory: true + dispatch_key: true + } + attrs: { + name: "href" + mandatory: true + value_url: { + allow_relative: false + allowed_protocol: "https" + } + } + attrs: { name: "color" } +} # Whitelisted font providers tags: { name: "link" detail: "link rel=stylesheet for fonts" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#custom-fonts" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#custom-fonts" mandatory_parent: "head" attrs: { name: "rel" @@ -399,7 +420,7 @@ tags: { mandatory: true mandatory_parent: "head" unique: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" attrs: { dispatch_key: true name: "charset" @@ -413,7 +434,7 @@ tags: { mandatory: true mandatory_parent: "head" unique: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" attrs: { dispatch_key: true name: "name" @@ -467,7 +488,7 @@ tags: { # Special custom 'author' spreadsheet. name: "style" mandatory_parent: "head" unique: true - detail: "author stylesheet" + detail: "style amp-custom" attrs: { name: "amp-custom" value: "" @@ -476,7 +497,7 @@ tags: { # Special custom 'author' spreadsheet. } cdata: { max_bytes: 50000 - max_bytes_spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#maximum-size" + max_bytes_spec_url: "https://www.ampproject.org/docs/reference/spec.html#maximum-size" css_spec: { at_rule_spec: { name: 'media' @@ -526,38 +547,42 @@ tags: { # Special custom 'author' spreadsheet. error_message: "CSS !important" } } - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#stylesheets" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#stylesheets" } tags: { name: "style" mandatory_parent: "head" - detail: "boilerplate (js enabled) - old variant" + detail: "head > style : boilerplate - old variant" + deprecation: "head > style : boilerplate" + deprecation_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" unique: true - mandatory_alternatives: "boilerplate (js enabled)" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate" + mandatory_alternatives: "head > style : boilerplate" cdata: { cdata_regex: "body ?{opacity: ?0}" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } tags: { name: "style" mandatory_parent: "noscript" - detail: "boilerplate (noscript) - old variant" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate" + detail: "noscript > style : boilerplate - old variant" + deprecation: "noscript > style : boilerplate" + deprecation_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" unique: true - mandatory_alternatives: "boilerplate (noscript)" + mandatory_alternatives: "noscript > style : boilerplate" cdata { cdata_regex: "body ?{opacity: ?1}" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } tags: { name: "style" mandatory_parent: "head" - detail: "boilerplate (js enabled)" - also_requires: "boilerplate (noscript)" + detail: "head > style : boilerplate" + also_requires: "noscript > style : boilerplate" unique: true - mandatory_alternatives: "boilerplate (js enabled)" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate" + mandatory_alternatives: "head > style : boilerplate" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" cdata: { # This regex allows arbitrary whitespace whenever some whitespace # is required in the boilerplate. It was made by replacing ' ' with \\s+. @@ -572,11 +597,11 @@ tags: { tags: { name: "style" mandatory_parent: "noscript" - detail: "boilerplate (noscript)" - also_requires: "boilerplate (js enabled)" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate" + detail: "noscript > style : boilerplate" + also_requires: "head > style : boilerplate" unique: true - mandatory_alternatives: "boilerplate (noscript)" + mandatory_alternatives: "noscript > style : boilerplate" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" cdata { mandatory_cdata: "body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}" } @@ -594,7 +619,7 @@ tags: { mandatory: true mandatory_parent: "html" unique: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # 4.3.2 The article element tags: { @@ -737,6 +762,7 @@ tags: { attrs: { name: "href" value_url: { + allow_relative: true allowed_protocol: "http" allowed_protocol: "https" allowed_protocol: "mailto" @@ -764,15 +790,11 @@ tags: { # TODO(gregable): Come up with a better scheme for this. value_regex: "(" "accessibility|" - "attachment|" # Commonly used by wordpress - "author|" "alternate|" "appendix|" - "alternate|" "archived|" "archives?|" "attachment|" - "appendix|" "author|" "bookmark|" "bibliography|" @@ -857,15 +879,11 @@ tags: { "yandex-tableau-widget" ")( +(" "accessibility|" - "attachment|" # Commonly used by wordpress - "author|" "alternate|" "appendix|" - "alternate|" "archived|" "archives?|" "attachment|" - "appendix|" "author|" "bookmark|" "bibliography|" @@ -1145,6 +1163,7 @@ tags: { alternative_names: "srcset" mandatory: true value_url: { + allow_relative: true # Will be set to false at a future date. allowed_protocol: "data" allowed_protocol: "https" } @@ -1188,6 +1207,7 @@ tags: { attrs: { name: "src" value_url: { + allow_relative: true # Will be set to false at a future date. allowed_protocol: "https" } } @@ -1201,6 +1221,7 @@ tags: { attrs: { name: "src" value_url: { + allow_relative: true # Will be set to false at a future date. allowed_protocol: "https" } } @@ -1214,6 +1235,7 @@ tags: { name: "src" mandatory: true value_url: { + allow_relative: true # Will be set to false at a future date. allowed_protocol: "https" } } @@ -1230,6 +1252,7 @@ tags: { name: "src" mandatory: true value_url: { + allow_relative: true # Will be set to false at a future date. allowed_protocol: "https" } } @@ -1370,6 +1393,7 @@ tags: { attrs: { name: "class" } attrs: { name: "externalresourcesrequired" } attrs: { name: "version" value_regex: "(1.0|1.1)" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "g" @@ -1380,6 +1404,7 @@ tags: { attrs: { name: "class" } attrs: { name: "externalresourcesrequired" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "path" @@ -1393,6 +1418,7 @@ tags: { attrs: { name: "pathlength" } attrs: { name: "sketch:type" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "glyph" @@ -1409,6 +1435,7 @@ tags: { attrs: { name: "glyph-name" } attrs: { name: "orientation" } attrs: { name: "arabic-form" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "glyphref" @@ -1423,6 +1450,7 @@ tags: { attrs: { name: "dy" } attrs: { name: "glyphref" } attrs: { name: "format" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "marker" @@ -1440,6 +1468,7 @@ tags: { attrs: { name: "markerwidth" } attrs: { name: "markerheight" } attrs: { name: "orient" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "view" @@ -1450,6 +1479,7 @@ tags: { attrs: { name: "preserveaspectratio" } attrs: { name: "zoomandpan" } attrs: { name: "viewtarget" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # Shapes tags: { @@ -1465,6 +1495,7 @@ tags: { attrs: { name: "r" } attrs: { name: "sketch:type" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "line" @@ -1480,6 +1511,7 @@ tags: { attrs: { name: "x2" } attrs: { name: "y1" } attrs: { name: "y2" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "polygon" @@ -1492,6 +1524,7 @@ tags: { attrs: { name: "points" } attrs: { name: "sketch:type" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "polyline" @@ -1504,6 +1537,7 @@ tags: { attrs: { name: "points" } attrs: { name: "sketch:type" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "rect" @@ -1521,6 +1555,7 @@ tags: { attrs: { name: "width" } attrs: { name: "x" } attrs: { name: "y" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # Text tags: { @@ -1540,6 +1575,7 @@ tags: { attrs: { name: "rotate" } attrs: { name: "textlength" } attrs: { name: "lengthadjust" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "textpath" @@ -1553,6 +1589,7 @@ tags: { attrs: { name: "startoffset" } attrs: { name: "method" } attrs: { name: "spacing" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "tref" @@ -1563,6 +1600,7 @@ tags: { attr_lists: "svg-xlink-attributes" attrs: { name: "class" } attrs: { name: "externalresourcesrequired" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "tspan" @@ -1579,6 +1617,7 @@ tags: { attrs: { name: "rotate" } attrs: { name: "textlength" } attrs: { name: "lengthadjust" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # Rendering tags: { @@ -1591,6 +1630,7 @@ tags: { attrs: { name: "externalresourcesrequired" } attrs: { name: "transform" } attrs: { name: "clippathunits" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "filter" @@ -1607,6 +1647,7 @@ tags: { attrs: { name: "filterres" } attrs: { name: "filterunits" } attrs: { name: "primitiveunits" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "lineargradient" @@ -1623,6 +1664,7 @@ tags: { attrs: { name: "x2" } attrs: { name: "y2" } attrs: { name: "spreadmethod" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "stop" @@ -1631,6 +1673,7 @@ tags: { attrs: { name: "offset" } attrs: { name: "stop-color" } attrs: { name: "stop-opacity" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "radialgradient" @@ -1648,6 +1691,7 @@ tags: { attrs: { name: "fx" } attrs: { name: "fy" } attrs: { name: "spreadmethod" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "stop" @@ -1656,6 +1700,7 @@ tags: { attrs: { name: "offset" } attrs: { name: "stop-color" } attrs: { name: "stop-opacity" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "mask" @@ -1671,6 +1716,7 @@ tags: { attrs: { name: "y" } attrs: { name: "width" } attrs: { name: "height" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "pattern" @@ -1690,6 +1736,7 @@ tags: { attrs: { name: "width" } attrs: { name: "height" } attrs: { name: "preserveaspectratio" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "vkern" @@ -1700,6 +1747,7 @@ tags: { attrs: { name: "u2" } attrs: { name: "g2" } attrs: { name: "k" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "hkern" @@ -1710,6 +1758,7 @@ tags: { attrs: { name: "u2" } attrs: { name: "g2" } attrs: { name: "k" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # Special tags: { @@ -1721,6 +1770,7 @@ tags: { attrs: { name: "class" } attrs: { name: "externalresourcesrequired" } attrs: { name: "transform" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "use" @@ -1736,6 +1786,7 @@ tags: { attrs: { name: "y" } attrs: { name: "width" } attrs: { name: "height" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "symbol" @@ -1746,6 +1797,7 @@ tags: { attrs: { name: "externalresourcesrequired" } attrs: { name: "preserveaspectratio" } attrs: { name: "viewbox" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # HTML inside SVG, esp. to allow amp-img. # See https://github.com/ampproject/amphtml/issues/717 @@ -1762,6 +1814,7 @@ tags: { attrs: { name: "y" } attrs: { name: "width" } attrs: { name: "height" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # ARIA tags: { @@ -1769,6 +1822,7 @@ tags: { mandatory_ancestor: "svg" attr_lists: "svg-core-attributes" attrs: { name: "class" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } tags: { name: "title" @@ -1776,6 +1830,7 @@ tags: { detail: "svg title" attr_lists: "svg-core-attributes" attrs: { name: "class" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#svg" } # 4.8 Links @@ -1879,7 +1934,7 @@ tags: { mandatory_parent: "head" unique: true detail: "amphtml engine v0.js script" - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" attrs: { name: "src" mandatory: true @@ -1945,7 +2000,7 @@ tags: { # amp-access name: "script" detail: "amp-access extension .js script" mandatory_parent: "head" - spec_url: "https://github.com/ampproject/amphtml/blob/master/extensions/amp-access/amp-access.md" + spec_url: "https://www.ampproject.org/docs/reference/extended/amp-access.html" attrs: { name: "custom-element" value: "amp-access" @@ -2497,7 +2552,7 @@ tags: { # amp-vimeo name: "script" mandatory_parent: "head" detail: "amp-vimeo extension .js script" - spec_url: "https://github.com/ampproject/amphtml/blob/master/extensions/amp-vimeo/amp-vimeo.md" + spec_url: "https://www.ampproject.org/docs/reference/extended/amp-vimeo.html" attrs: { name: "custom-element" value: "amp-vimeo" @@ -2521,7 +2576,7 @@ tags: { # amp-dailymotion name: "script" mandatory_parent: "head" detail: "amp-dailymotion extension .js script" - spec_url: "https://github.com/ampproject/amphtml/blob/master/extensions/amp-dailymotion/amp-dailymotion.md" + spec_url: "https://www.ampproject.org/docs/reference/extended/amp-dailymotion.html" attrs: { name: "custom-element" value: "amp-dailymotion" @@ -2545,7 +2600,7 @@ tags: { # amp-soundcloud name: "script" mandatory_parent: "head" detail: "amp-soundcloud extension .js script" - spec_url: "https://github.com/ampproject/amphtml/blob/master/extensions/amp-soundcloud/amp-soundcloud.md" + spec_url: "https://www.ampproject.org/docs/reference/extended/amp-soundcloud.html" attrs: { name: "custom-element" value: "amp-soundcloud" @@ -2590,7 +2645,7 @@ tags: { detail: "noscript enclosure for boilerplate" unique: true mandatory: true - spec_url: "https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#boilerplate" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # We allow noscript in the body to contain tags otherwise disallowed by AMP. tags: { @@ -2605,7 +2660,7 @@ tags: { name: "template" #