diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index ccc1dd378b9fe..0000000000000 --- a/AUTHORS +++ /dev/null @@ -1,6 +0,0 @@ -# This is a list of contributors to AMP HTML. - -# Names should be added to this file like so: -# Name or Organization - -Google Inc. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 807d5b5f9ac0a..9d6f9106d02b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,68 +14,70 @@ See the License for the specific language governing permissions and limitations under the License. --> -## Contributing to AMP HTML +# Contributing to AMP HTML The AMP HTML project strongly encourages technical [contributions](https://www.ampproject.org/contribute/)! We hope you'll become an ongoing participant in our open source community but we also welcome one-off contributions for the issues you're particularly passionate about. -### Filing Issues +## Filing issues -**Suggestions** +### Suggestions -The AMP HTML project is meant to evolve with feedback - the project and its users greatly appreciate any thoughts on ways to improve the design or features. Please use the `enhancement` tag to specifically denote issues that are suggestions - this helps us triage and respond appropriately. +The AMP Project is meant to evolve with feedback. The project and its users appreciate your thoughts on ways to improve the design or features. -**Bugs** +To make a suggestion [file an issue](https://github.com/ampproject/amphtml/issues/new) and add the [Type: Feature Request](https://github.com/ampproject/amphtml/labels/Type%3A%20Feature%20Request) label. -As with all pieces of software, you may end up running into bugs. Please submit bugs as regular issues on GitHub - AMP HTML developers are regularly monitoring issues and will try to fix open bugs quickly. +If you are suggesting a feature that you are intending to implement, please see the [Contributing features](#contributing-features) section below for next steps. -The best bug reports include a detailed way to predictably reproduce the issue, and possibly even a working example that demonstrates the issue. +### Bugs -### Ongoing Participation +If you find a bug in AMP, please [file an issue](https://github.com/ampproject/amphtml/issues/new). Members of the community are regularly monitoring issues and will try to fix open bugs quickly. -We actively encourage ongoing participation by community members. +The best bug reports provide a detailed description of the issue (including screenshots if possible), step-by-step instructions for predictably reproducing the issue, and possibly even a working example that demonstrates the issue. + +If you want to learn more about our issues priorities and implementation guidelines check out [this document](https://github.com/ampproject/amphtml/blob/master/contributing/issue-priorities.md). + +## Contributing code + +The AMP Project accepts and greatly appreciates code contributions! -* **Discussions** for implementations, designs, etc. often happen in the [amphtml-discuss Google Group](https://groups.google.com/forum/#!forum/amphtml-discuss) and the [amphtml Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). -* **Weekly status updates** from individual community members are posted as GitHub issues labeled [Type: Weekly Status](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Weekly+Status%22). If you have a weekly status update related to your work on AMP that you'd like to share with the community please add it as a comment on the relevant Weekly Status issue. -* **Weekly design reviews** are held as video conferences via Google Hangouts on Wednesdays at [1pm Pacific](https://www.google.com/?#q=1pm+pacific+in+local+time). Design reviews are used to discuss/refine engineering designs after an initial draft of the design has been created and shared with the community. The design reviews are meant as an *optional* venue for concentrated feedback and discussion. **Going through this design review is *not* required to make a contribution to AMP.** - * We use GitHub issues labeled [Type: Design Review](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Design+Review%22) to track design reviews. The Design Review issue for a given week will have a link to the design docs being discussed that week as well as a link to the Hangout. - * When you attend a design review please read through the design docs before the review starts. - * If you have an engineering design you would like to discuss at a design review: - * Create a software design document in a shared Google Document open to public comments. A short design doc is fine as long as it covers your design in sufficient detail to allow for a review by other members of the community. Take a look at [Design docs - A design doc](https://medium.com/@cramforce/design-docs-a-design-doc-a152f4484c6b) for tips on putting together a good design doc. Some examples: - * [Phone call tracking in AMP](https://docs.google.com/document/d/1UDMYv0f2R9CvMUSBQhxjtkSnC4984t9dJeqwm_8WiAM/edit) - * [New AMP Boilerplate](https://docs.google.com/document/d/1gZFaKvcDffceJNaI3bYfuYPtYU5u2y6UhE5wBPTsJ9w/edit) - * Perform a design pre-review with at least one [core committer](https://github.com/ampproject/amphtml/blob/master/GOVERNANCE.md); you can request a pre-review in the [#design-review Slack channel](https://amphtml.slack.com/messages/design-review/). It is fine to request a pre-review before your design doc is complete. - * When your design is ready to be discussed at a design review add a comment on the appropriate Design Review GitHub issue. Post a link to the design doc and a brief summary by **1pm Pacific Monday** on the week of your design review. +### Tips for new open source contributors -### Contributing Code +If you are new to contributing to an open source project, Git/GitHub, etc. welcome! We are glad you're interested in contributing to the AMP Project. -The AMP HTML project accepts and greatly appreciates contributions. The project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. +The [Getting Started End-to-End Guide](contributing/getting-started-e2e.md) provides step-by-step instructions for everything from creating a GitHub account to getting your code reviewed and merged. Even if you've never contributed to an open source project before you'll soon be building AMP, making improvements and seeing your code live across the web. -When contributing code, please also include appropriate tests as part of the pull request, and follow the same comment and coding style as the rest of the project. Take a look through the existing code for examples of the testing and style practices the project follows. +The community has created a list of [Great First Issues](https://github.com/ampproject/amphtml/milestone/25) specifically for new contributors to the project. Feel free to find one that interests you and jump in! Make sure to comment on the issue first so others know you are starting on it. -A key feature of the AMP HTML project is performance - all pull requests will be analyzed for any performance impact, and the project greatly appreciates ways it can get even faster. Please include any measured performance impact with substantial pull requests. +If you run into any problems we have plenty of people who are willing to help; see the [How to get help](contributing/getting-started-e2e.md#how-to-get-help) section of the Getting Started guide. -* We follow [Google's JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html). +### How to contribute code -**Google Individual Contributor License** +The [Getting Started Quick Start Guide](contributing/getting-started-quick.md) has installation steps and instructions for building/testing AMP. -Code contributors to the AMP HTML project must sign a Contributor License Agreement, either for an [individual](https://developers.google.com/open-source/cla/individual) or [corporation](https://developers.google.com/open-source/cla/corporate). The CLA is meant to protect contributors, users of the AMP HTML runtime, and Google in issues of intellectual property. +[DEVELOPING.md](DEVELOPING.md) has some more advanced instructions that may be necessary depending on the complexity of the changes you are making. -### Contributing Features +A few things to note: -All pull requests for new features must go through the following process: -* Please familiarize yourself with the [AMP Design Principles](DESIGN_PRINCIPLES.md) -* Start an Intent-to-implement GitHub issue for discussion of the new feature. -* LGTM from Tech Lead and one other core committer is required -* Development occurs on a separate branch of a separate fork, noted in the intent-to-implement issue -* A pull request is created, referencing the issue. -* AMP HTML developers will provide feedback on pull requests, looking at code quality, style, tests, performance, and directional alignment with the goals of the project. That feedback should be discussed and incorporated -* LGTM from Tech Lead and one other core committer, who confirm engineering quality and direction. +* The AMP Project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. +* Familiarize yourself with our [Design Principles](DESIGN_PRINCIPLES.md). +* We follow [Google's JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html). More generally make sure to follow the same comment and coding style as the rest of the project. +* Include tests when contributing code. There are plenty of tests that you can use as examples. +* A key feature of AMP is performance. All changes will be analyzed for any performance impact; we particularly appreciate changes that make things even faster. Please include any measured performance impact with substantial pull requests. -#### Contributing Extended Components +## Contributing features -A key feature of the AMP HTML project is its extensibility - it is meant to support “Extended Components” that provide first-class support for additional rich features. The project currently accepts pull requests to include these types of extended components. +Follow this process for contributing new features: +* Familiarize yourself with the [AMP Design Principles](DESIGN_PRINCIPLES.md) +* [Create a new GitHub issue](https://github.com/ampproject/amphtml/issues/new) to start discussion of the new feature. +* Before starting on the code get approval for your feature from an [OWNER](https://github.com/ampproject/amphtml/search?utf8=%E2%9C%93&q=filename%3AOWNERS.yaml&type=Code) of your feature's area and a [core committer](https://github.com/ampproject/amphtml/blob/master/GOVERNANCE.md#core-committers). In most cases the people who can give this approval and are most familiar with your feature's area will get involved proactively or someone else in the community will add them. If you are having trouble finding the right people add a comment on the issue or reach out on one of the channels in [How to get help](contributing/getting-started-e2e.md#how-to-get-help). +* Consider bringing the eng design for your feature to our [weekly design review](#weekly-design-review). +* Follow the guidelines for [Contributing code](#contributing-code) described above. + +## Contributing extended components + +A key feature of the AMP HTML project is its extensibility - it is meant to support “Extended Components” that provide first-class support for additional rich features. Because Extended Components may have significant impact on AMP HTML performance, security, and usage, Extended Component contributions will be very carefully analyzed and scrutinized. @@ -83,9 +85,58 @@ In particular we strive to design the overall component set, so that a large num For further detail on integrating third party services, fonts, embeds, etc. see our [3p contribution guidelines](https://github.com/ampproject/amphtml/tree/master/3p). + +## Contributor License Agreement + +The AMP Project hosted at GitHub requires all contributors to sign a Contributor License Agreement ([individual](https://developers.google.com/open-source/cla/individual) or [corporation](https://developers.google.com/open-source/cla/corporate)) in order to protect contributors, users and Google in issues of intellectual property. + +When you create a Pull Request a check will be run to ensure that you have signed the CLA. Make sure that you sign the CLA with the same email address you associate with your commits (likely via the `user.email` Git config as described on GitHub's [Set up Git](https://help.github.com/articles/set-up-git/) page). + +## Ongoing participation + +We actively encourage ongoing participation by community members. + +### Discussion channels + +Technical issues, designs, etc. are discussed on [GitHub issues](https://github.com/ampproject/amphtml/issues) and [pull requests](https://github.com/ampproject/amphtml/pulls), + the [amphtml-discuss Google Group](https://groups.google.com/forum/#!forum/amphtml-discuss) and the [amphtml Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). + +### Weekly status updates + +GitHub issues labeled [Type: Weekly Status](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Weekly+Status%22) are used to track weekly updates from members of the community. We encourage everyone who is actively contributing to AMP to add a comment to the relevant Weekly Status issue. + +### Weekly design reviews + +The community holds weekly design reviews as video conferences via Google Hangouts on Wednesdays at [1pm Pacific](https://www.google.com/?#q=1pm+pacific+in+local+time). + +We use GitHub issues labeled [Type: Design Review](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Design+Review%22) to track design reviews. The Design Review issue for a given week will have a link to the design docs being discussed that week as well as a link to the Hangout. **When attending a design review please read through the design docs _before_ the review starts.** + +If the design for an issue/feature you are working on has a scope larger than you can cover in a discussion in the GitHub issue or one of the other discussion channels, consider bringing it to a design review: + +* Create a software design document in a shared Google Document open to public comments. + * A short design doc is fine as long as it covers your design in sufficient detail to allow for a review by other members of the community. + * Take a look at [Design docs - A design doc](https://medium.com/@cramforce/design-docs-a-design-doc-a152f4484c6b) for tips on putting together a good design doc. [Phone call tracking in AMP](https://docs.google.com/document/d/1UDMYv0f2R9CvMUSBQhxjtkSnC4984t9dJeqwm_8WiAM/edit) and [New AMP Boilerplate](https://docs.google.com/document/d/1gZFaKvcDffceJNaI3bYfuYPtYU5u2y6UhE5wBPTsJ9w/edit) are examples of past AMP Project design docs. + * Add this license text to the top of your design doc before sharing it with anyone else in the community (updating the year if necessary): + ``` + Copyright 2017 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. + ``` + +* Perform a design pre-review with at least one [core committer](https://github.com/ampproject/amphtml/blob/master/GOVERNANCE.md); you can request a pre-review in the [#design-review Slack channel](https://amphtml.slack.com/messages/design-review/). It is fine to request a pre-review before your design doc is complete. + +* When your design is ready to be discussed at a design review add a comment on the appropriate Design Review GitHub issue. Post a link to the design doc and a brief summary by **1pm Pacific Monday** on the week of your design review. + +* Update your design based on the feedback in the design review and any followup conversations in other channels. Once your design is finalized, please provide a brief update at the start of a future design review (if you are able to attend) and submit a PDF version of your design doc in the [ampproject design-doc](https://github.com/ampproject/design-docs) repository. + ### See Also * [Code of conduct](CODE_OF_CONDUCT.md) -* [DEVELOPING](DEVELOPING.md) resources * [3p contribution guidelines](https://github.com/ampproject/amphtml/tree/master/3p) * The [GOVERNANCE](GOVERNANCE.md) model diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 16c7edb242da2..0000000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,18 +0,0 @@ -# Names should be added to this file as: -# Name - -Avi Mehta -David Sedano -Dima Voytenko -Erwin Mombay -Greg Grothaus -Jake Moening -Johannes Henkel -Jordan Adler -Justin Ridgewell -Kent Brewster -Malte Ubl -Mohammad Khatib -Niall Kennedy -Sriram Krishnan -Taylor Savage diff --git a/DEVELOPING.md b/DEVELOPING.md index c2ef66118dedc..0735ce8910638 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -14,33 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. --> -## Development on AMP HTML +# Development on AMP HTML -### Slack and mailing list +## How to get started -Please join our [announcements mailing list](https://groups.google.com/forum/#!forum/amphtml-announce). This is a curated, low volume list for announcements about breaking changes and similar issues in AMP. +Before you start developing in AMP, check out these resources: +* [CONTRIBUTING.md](CONTRIBUTING.md) has details on various ways you can contribute to the AMP Project. + * If you're developing in AMP, you should read the [Contributing code](CONTRIBUTING.md#contributing-code) and [Contributing features](CONTRIBUTING.md#contributing-features) sections.. + * The [Ongoing participation](CONTRIBUTING.md#ongoing-participation) section has details on various ways of getting in touch with others in the community including email and Slack. + * **If you are new to open source projects, Git/GitHub, etc.**, check out the [Tips for new open source contributors](CONTRIBUTING.md#tips-for-new-open-source-contributors) which includes information on getting help and finding your first bug to work on. +* The [Getting Started Quick Start Guide](contributing/getting-started-quick.md) has installation steps and basic instructions for [one-time setup](contributing/getting-started-quick.md#one-time-setup), how to [build AMP & run a local server](contributing/getting-started-quick.md#build-amp--run-a-local-server) and how to [test AMP](contributing/getting-started-quick.md#test-amp). -We discuss implementation issues on [amphtml-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/amphtml-discuss). -For more immediate feedback, [sign up for our Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). +## Build & Test -### Great First Issues +For most developers the instructions in the [Getting Started Quick Start Guide](contributing/getting-started-quick.md) will be sufficient for building/running/testing during development. This section provides a more detailed reference. -We're curating a [list of GitHub "great first issues"](https://github.com/ampproject/amphtml/labels/Great%20First%20Issues) of small to medium complexity that are great to jump into development on AMP. - -If you have any questions, feel free to ask on the issue or join us on [Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877)! - -### Installation - -1. Install [NodeJS](https://nodejs.org). -2. In the repo directory, run `npm i` command to install the required npm packages. -3. Run `npm i -g gulp` command to install gulp system-wide (on Mac or Linux you may need to prefix this with `sudo`, depending on how Node was installed). -4. Edit your hosts file (`/etc/hosts` on Mac or Linux, `%SystemRoot%\System32\drivers\etc\hosts` on Windows) and map `ads.localhost` and `iframe.localhost` to `127.0.0.1`. -
-  127.0.0.1               ads.localhost iframe.localhost
-
- -### Build & Test +The Quick Start Guide's [One-time setup](contributing/getting-started-quick.md#one-time-setup) has instructions for installing Node.js, Yarn, and Gulp which you'll need before running these commands. | Command | Description | | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | @@ -71,26 +61,11 @@ If you have any questions, feel free to ask on the issue or join us on [Slack](h [1] On Windows, this command must be run as administrator. -#### Saucelabs - -Running tests on Sauce Labs requires an account. You can get one by signing up for [Open Sauce](https://saucelabs.com/opensauce/). This will provide you with a user name and access code that you need to add to your `.bashrc` or equivalent like this: - -``` -export SAUCE_USERNAME=sauce-labs-user-name -export SAUCE_ACCESS_KEY=access-key -``` - -Also for local testing, download [saucelabs connect](https://docs.saucelabs.com/reference/sauce-connect/) (If you are having trouble, downgrade to 4.3.10) and establish a tunnel by running the `sc` before running tests. - -If your pull request contains JS or CSS changes and it does not change the build system, it will be automatically built and tested on [Travis](https://travis-ci.org/ampproject/amphtml/builds). After the travis run completes, the result will be logged to your PR. +## Manual testing -If a test flaked on a pull request you can ask a project owner to restart the tests for you. Use [`this.retries(x)`](https://mochajs.org/#retry-tests) as the last resort. +For manual testing build AMP and start the Node.js server by running `gulp`. -### Manual testing - -The below assume you ran `gulp` in a terminal. - -#### Examples +### Examples The content in the `examples` directory can be reached at: http://localhost:8000/examples/ @@ -101,7 +76,7 @@ For each example there are 3 modes: - `/examples/abc.min.html` points to a local minified AMP. This is closer to the prod setup. Only available after running `gulp dist --fortesting`. -#### Document proxy +### Document proxy AMP ships with a local proxy for testing production AMP documents with the local JS version. @@ -119,9 +94,9 @@ code elimination to trim down the file size for the file we deploy to production If the origin resource is on HTTPS, the URLs are http://localhost:8000/max/s/output.jsbin.com/pegizoq/quiet and http://localhost:8000/min/s/output.jsbin.com/pegizoq/quiet -#### A4A envelope +### A4A envelope -AMP ships with a local A4A envelope for testing local and production AMP documents with the local JS version. +If you are working on AMP 4 Ads (A4A), you can use the local A4A envelope for testing local and production AMP documents with the local JS version. A4A can be run either of these two modes: @@ -149,15 +124,41 @@ Additionally, the following query parameters can be provided: - `height` - the height of the `amp-ad` (default "250") -#### Chrome extension +### Chrome extension For testing documents on arbitrary URLs with your current local version of the AMP runtime we created a [Chrome extension](testing/local-amp-chrome-extension/README.md). -#### Deploying AMP on Cloud for testing on devices +## Testing on Sauce Labs + +In general local testing (i.e. `gulp test`) and the automatic test run on [Travis](https://travis-ci.org/ampproject/amphtml/pull_requests) that happens when you send a pull request are sufficient. If you want to run your tests across multiple environments/browsers before sending your PR you can use Sauce Labs. + +To run the tests on Sauce Labs: + +* Create a Sauce Labs account. If you are only going to use your account for open source projects like this one you can sign up for a free [Open Sauce](https://saucelabs.com/opensauce/) account. (If you create an account through the normal account creation mechanism you'll be signing up for a free trial that expires; you can contact Sauce Labs customer service to switch your account to Open Sauce if you did this accidentally.) +* Set the `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables. On Linux add this to your `.bashrc`: + + ``` + export SAUCE_USERNAME= + export SAUCE_ACCESS_KEY= + ``` + + You can find your Sauce Labs access key on the [User Settings](https://saucelabs.com/beta/user-settings) page. +* Install the [Sauce Connect Proxy](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect+Proxy). +* Run the proxy and then run the tests: + ``` + # start the proxy + sc + + # after seeing the "Sauce Connect is up" msg, run the tests + gulp test --saucelabs + ``` +* It may take a few minutes for the tests to start. You can see the status of your tests on the Sauce Labs [Automated Tests](https://saucelabs.com/beta/dashboard/tests) dashboard. (You can also see the status of your proxy on the [Tunnels](https://saucelabs.com/beta/tunnels) dashboard. + +## Deploying AMP on Cloud for testing on devices For deploying and testing local AMP builds on [HEROKU](https://www.heroku.com/) , please follow the steps outlined in this [document](https://docs.google.com/document/d/1LOr8SEBEpLkqnFjzTNIZGi2VA8AC8_aKmDVux6co63U/edit?usp=sharing). -Meantime, you can also use our automatic build on Heroku [link](http://amphtml-nightly.herokuapp.com/), which is normally built with latest head on master branch (please allow delay). The first time load is normally slow due to Heroku's free account throttling policy. +In the meantime you can also use our automatic build on Heroku [link](http://amphtml-nightly.herokuapp.com/), which is normally built with latest head on master branch (please allow delay). The first time load is normally slow due to Heroku's free account throttling policy. To correctly get ads and third party working when testing on hosted services you will need set the `AMP_TESTING_HOST` environment variable. (On heroku this @@ -173,12 +174,13 @@ is done through builtins/ - tags built into the core AMP runtime *.md - documentation for use of the builtin *.js - source code for builtin tag + contributing/ - docs for people contributing to the AMP Project css/ - default css dist/ - (generated) main JS binaries are created here. This is what gets deployed to cdn.ampproject.org. dist.3p/ - (generated) JS binaries and HTML files for 3p embeds and ads. This is what gets deployed to 3p.ampproject.net. - docs/ - documentation + docs/ - documentation about AMP examples/ - example AMP HTML files and corresponding assets extensions/ - plugins which extend the AMP HTML runtime's core set of tags spec/ - The AMP HTML Specification files @@ -197,7 +199,6 @@ In particular, we try to maintain "it might not be perfect but isn't broken"-sup ## Eng docs -- [Design Principles](DESIGN_PRINCIPLES.md) - [Life of an AMP *](https://docs.google.com/document/d/1WdNj3qNFDmtI--c2PqyRYrPrxSg2a-93z5iX0SzoQS0/edit#) - [AMP Layout system](spec/amp-html-layout.md) - [Building an AMP Extension](https://docs.google.com/document/d/19o7eDta6oqPGF4RQ17LvZ9CHVQN53whN-mCIeIMM8Qk/edit#) diff --git a/README.md b/README.md index a5f9cc06b967b..fe9440dbce307 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,41 @@ - - -[![Build Status](https://travis-ci.org/ampproject/amphtml.svg?branch=master)](https://travis-ci.org/ampproject/amphtml) - # AMP HTML ⚡ -[AMP HTML](https://www.ampproject.org/docs/get_started/about-amp.html) is a way to build web pages for static content that render with reliable, fast performance. It is our attempt at fixing what many perceive as painfully slow page load times – especially when reading content on the mobile web. - -AMP HTML is entirely built on existing web technologies. It achieves reliable performance by restricting some parts of HTML, CSS and JavaScript. These restrictions are enforced with a validator that ships with AMP HTML. To make up for those limitations AMP HTML defines a set of [custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) for rich content beyond basic HTML. Learn more about [how AMP speeds up performance](https://www.ampproject.org/docs/get_started/technical_overview.html). - -# How does AMP HTML work? - -AMP HTML works by including the AMP JS library and adding a bit of boilerplate to a web page, so that it meets the AMP HTML Specification. The simplest AMP HTML file looks like this: - -```html - - - - - - - - - - Hello World! - -``` - -This allows the AMP library to include: -* The AMP JS library, that manages the loading of external resources to ensure a - fast rendering of the page. -* An AMP validator that provides a way for web developers to easily validate - that their code meets the AMP HTML specification. -* Some custom elements, called AMP HTML components, which make common patterns - easy to implement in a performant way. - -Get started [creating your first AMP page](https://www.ampproject.org/docs/get_started/create_page.html). - -[Full docs and reference.](https://www.ampproject.org/docs/get_started/about-amp.html) - -## The AMP JS library - -The AMP JS library provides [builtin](builtins/README.md) AMP Components, manages the loading of external resources, and ensures a reliably fast time-to-paint. - -## The AMP Validator - -[The AMP Validator](validator/README.md) allows a web developer to easily -identify if the web page doesn't meet the -[AMP HTML specification](https://www.ampproject.org/docs/reference/spec.html). - -Adding "#development=1" to the URL of the page instructs the AMP Runtime to run -a series of assertions confirming the page's markup meets the AMP HTML -Specification. Validation errors are logged to the browser's console when the -page is rendered, allowing web developers to easily see how complex changes in -web code might impact performance and user experience. - -It also allows apps that integrate web content to validate the web page against -the specification. This allows an app to make sure the page is fast and -mobile-friendly, as pages adhering to the AMP HTML specification are reliably -fast. +[AMP HTML](https://www.ampproject.org/docs/get_started/about-amp.html) is a way to build web pages that render with reliable and fast performance. It is our attempt at fixing what many perceive as painfully slow page load times – especially when reading content on the mobile web. AMP HTML is built on existing web technologies; an AMP page will load (quickly) in any modern browser. -Learn more about -[validating your AMP pages](https://www.ampproject.org/docs/guides/validate.html). -Also see [additional choices to invoke the validator](validator/README.md). +You can learn more at [ampproject.org](https://www.ampproject.org/) including [what AMP is](https://www.ampproject.org/learn/about-amp/), [how it works](https://www.ampproject.org/learn/how-amp-works/) and the importance of [validation in AMP](https://www.ampproject.org/docs/guides/validate). You can also walk through [creating an AMP page](https://www.ampproject.org/docs/get_started/create) and read through the [reference docs](https://www.ampproject.org/docs/reference/components). -## AMP HTML Components +## We'd ❤️️ your help making AMP better! -AMP HTML Components are a series of extended custom elements that supplement -or replace functionality of core HTML5 elements to allow the runtime to ensure -it is solely responsible for loading external assets and to provide for shared -best practices in implementation. +**There are a lot of ways you can [contribute](CONTRIBUTING.md) to making AMP better!** You can [report bugs and feature requests](CONTRIBUTING.md#filing-issues) or ideally become an [ongoing participant](CONTRIBUTING.md#ongoing-participation) in the AMP Project community and [contribute code to the open source project](CONTRIBUTING.md#contributing-code). -See our [docs and reference](https://www.ampproject.org/docs/get_started/about-amp.html) for more info. +We **_⚡❤️️⚡ enthusiastically ⚡❤️️⚡_ welcome new contributors** to the AMP Project **_even if you have no experience being part of an open source project_**. We've got some [tips for new contributors](https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md#tips-for-new-open-source-contributors) and guides to getting started (both a [detailed version](contributing/getting-started-e2e.md) and a [TL;DR](contributing/getting-started-quick.md)). -# Releases +If you're new here, sign up for our [Slack](https://docs.google.com/forms/d/e/1FAIpQLSd83J2IZA6cdR6jPwABGsJE8YL4pkypAbKMGgUZZriU7Qu6Tg/viewform?fbzx=4406980310789882877) and say "Hi!" in the appropriately named [#welcome-contributors](https://amphtml.slack.com/messages/welcome-contributors/) channel. -We push a new release of AMP to all AMP pages every week on Thursday. The more detailed schedule is as follows: - -- Every Thursday we cut a green release from our `master` branch. -- This is then pushed to users of AMP who opted into the [AMP Dev Channel](#amp-dev-channel). -- On Monday we check error rates for opt-in users and bug reports and if everything looks fine, we push this new release to 1% of AMP pages. -- We then continue to monitor error rates and bug reports throughout the week. -- On Thursday the "Dev Channel" release from last Thursday is then pushed to all users. - -You can always follow the current release state of AMP on our [releases page](https://github.com/ampproject/amphtml/releases). The release used by most users is marked as `Latest release` and the current Dev Channel release is marked as `Pre-release`. - -## AMP Dev Channel - -AMP Dev Channel is a way to opt a browser into using a newer version of the AMP JS libraries. - -This release **may be less stable** and it may contain features not available to all users. Opt into this option if you'd like to help test new versions of AMP, report bugs or build documents that require a new feature that is not yet available to everyone. - -Opting into Dev Channel is great to: - -- test and play with new features not yet available to all users. -- use in Q&A to ensure that your site is compatible with the next version of AMP. - -If you find an issue that appears to only occur in the Dev Channel version of AMP, please [file an issue](https://github.com/ampproject/amphtml/issues/new) with a description of the problem. Please always include a URL to a page that reproduces the issue. - -To opt your browser into the AMP Dev Channel, go to [the AMP experiments page](https://cdn.ampproject.org/experiments.html) and activate the "AMP Dev Channel" experiment. Please subscribe to our [low-volume announcements](https://groups.google.com/forum/#!forum/amphtml-announce) mailing list to get notified about important/breaking changes about AMP. - -# Further Reading +## Further reading If you are creating AMP pages, check out the docs on [ampproject.org](https://www.ampproject.org/) and samples on [ampbyexample.com](https://ampbyexample.com/). -These docs are public and open-source: [https://github.com/ampproject/docs/](https://github.com/ampproject/docs/). -See something that's missing from the docs, or that could be worded better? -[Create an issue](https://github.com/ampproject/docs/issues) and -we will do our best to respond quickly. - -Resources: -* [AMP HTML samples](examples/) -* [AMP-HTML on StackOverflow](https://stackoverflow.com/questions/tagged/amp-html) - - - Reference: -* [AMP HTML core built-in elements](builtins/README.md) -* [AMP HTML optional extended elements](extensions/README.md) - -Technical Specifications: -* [AMP HTML format specification](spec/amp-html-format.md) -* [AMP HTML custom element specification](spec/amp-html-components.md) +* [Component reference](https://www.ampproject.org/docs/reference/components) -# Who makes AMP HTML? +Resources: +* [AMP on StackOverflow](https://stackoverflow.com/questions/tagged/amp-html) +* [Release schedule](contributing/release-schedule.md) -AMP HTML is made by the [AMP Project](https://www.ampproject.org/), and is licensed -under the [Apache License, Version 2.0](LICENSE). +Technical specifications: +* [Format specification](spec/amp-html-format.md) +* [Custom element specification](spec/amp-html-components.md) -## Contributing +## Who makes AMP HTML? -Please see [the CONTRIBUTING file](CONTRIBUTING.md) for information on contributing to the AMP Project, and [the DEVELOPING file](DEVELOPING.md) for documentation on the AMP library internals and [hints how to get started](DEVELOPING.md#starter-issues). +AMP HTML is made by the [AMP Project](https://www.ampproject.org/). If you're a [contributor to the open source community](https://github.com/ampproject/amphtml/graphs/contributors) this includes you! -### Security disclosures +## Security disclosures The AMP Project accepts responsible security disclosures through the [Google Application Security program](https://www.google.com/about/appsecurity/). -### [Code of conduct](CODE_OF_CONDUCT.md) +## Code of conduct + +The AMP Project strives for a positive and growing project community that provides a safe environment for everyone. All members, committers and volunteers in the community are required to act according to the [code of conduct](CODE_OF_CONDUCT.md). diff --git a/ads/ads.extern.js b/ads/ads.extern.js index 907de85110c68..2d1ff64e91852 100644 --- a/ads/ads.extern.js +++ b/ads/ads.extern.js @@ -29,34 +29,158 @@ function AmpAdUIHandler$$module$extensions$amp_ad$0_1$amp_ad_ui() {}; // Long list of, uhm, stuff the ads code needs to compile. // All unquoted external properties need to be added here. + +// Under 3p folder + +// facebook.js +data.embedAs; +data.href; + +// reddit.js +data.uuid; +data.embedcreated; +data.embedparent; +data.embedlive; +data.embedtype; +data.src; + +//twitter.js +data.tweetid + +// Under ads/google folder + +// adsense.js + +// csa.js +var _googCsa; +window._googCsa; + +// doubleclick.js +var googletag; +window.googletag; +googletag.cmd; +googletag.cmd.push; +googletag.pubads; +googletag.defineSlot; +var pubads; +pubads.addService; +pubads.markAsGladeOptOut; +pubads.markAsAmp; +pubads.setCorrelator; +pubads.markAsGladeControl; +googletag.enableServices; +pubads.setCookieOptions; +pubads.setTagForChildDirectedTreatment; +data.slot.setCategoryExclusion; +data.slot.setTargeting; +data.slot.setAttribute; +data.useSameDomainRenderingUntilDeprecated; +data.multiSize; +data.overrideWidth; +data.width; +data.overrideHeight; +data.height; +data.multiSizeValidation; +data.categoryExclusions; +data.categoryExclusions.length;; +data.cookieOptions; +data.tagForChildDirectedTreatment; +data.targeting; +data.slot; + +// 3P ads +// Please sort by alphabetic order of the ad server name to avoid conflict + +// a9.js +data.aax_size; +data.aax_pubname; +data.aax_src; + +// adblade.js data.cid; + +// adform.js data.bn; data.mid; + +// adgeneration.js +data.option; +data.id; +data.adtype; +data.adtype.toUpperCase; +data.async; +data.async.toLowerCase; +data.displayid; +data.targetid; + +// adman.js data.ws; +data.host; data.s; -data.sid; -data.client; + +// adreactor.js data.zid; data.pid; data.custom3; + +// adsnative.js +data.ankv; +data.ankv.split; +data.ancat; +data.ancat.split; +data.anapiid; +data.annid; +data.anwid; +data.antid; + +// adtech.js +data.atwco; +data.atwdiv; +data.atwheight; +data.atwhtnmat; +data.atwmn; +data.atwmoat; +data.atwnetid; +data.atwothat; +data.atwplid; +data.atwpolar; +data.atwsizes; +data.atwwidth; + +// adthrive.js +data.siteId; + +// aduptech.js window.uAd = {}; window.uAd.embed; +data.responsive; +data.onAds; +data.onNoAds; + +// amoad.js +data.sid; + +// appnexus.js +data.tagid; +data.member; +data.code; data.pageOpts; +data.debug; data.adUnits; +data.target; + +// colombia.js data.clmb_slot; data.clmb_position; -data.clmb_divid; data.clmb_section; -data.epl_si; -data.epl_isv; -data.epl_sv; -data.epl_sec; -data.epl_ksv; -data.epl_kvs; -data.epl_e; -data.guid; -data.adslot; +data.clmb_divid; +// contentad.js +data.d; +data.wid; +data.url; + +// criteo.js var Criteo; Criteo.DisplayAd; Criteo.Log.Debug; @@ -66,60 +190,134 @@ Criteo.PubTag = {}; Criteo.PubTag.RTA = {}; Criteo.PubTag.RTA.DefaultCrtgContentName; Criteo.PubTag.RTA.DefaultCrtgRtaCookieName -data.varname; data.tagtype; -data.cookiename;; -data.networkid;; +data.networkid; +data.cookiename; +data.varname; data.zone; data.adserver; -data.slot; -data.width; -data.height; -var googletag; -window.googletag; -googletag.cmd; -googletag.cmd.push; -googletag.pubads; -googletag.defineSlot -data.slot; +// distroscale.js +data.tid; -var _googCsa; -window._googCsa; +// eplanning.js +data.epl_si; +data.epl_isv; +data.epl_sv; +data.epl_sec; +data.epl_kvs; +data.epl_e; +// ezoic.js +/** + * @constructor + * @param {!Window} global + * @param {!Object} data + */ +window.EzoicAmpAd = function(global, data) {}; +window.EzoicAmpAd.prototype.createAd; + +// flite.js +data.guid; +data.mixins; + +// fusion.js +var ev; +ev.msg; +var Fusion; +Fusion.on; +Fusion.on.warning; +Fusion.loadAds; +data.space; + +// holder.js +data.queue; + +// improvedigital.js +data.placement; +data.optin; +data.keyvalue; + +// inmobi.js var _inmobi; window._inmobi; _inmobi.getNewAd; data.siteid; data.slotid; -var pubads; -pubads.addService; -pubads.markAsGladeOptOut; -pubads.markAsAmp; -pubads.setCorrelator; -pubads.markAsGladeControl; -googletag.enableServices; -data.slot.setCategoryExclusion; -pubads.setCookieOptions; -pubads.setTagForChildDirectedTreatment; -data.slot.setTargeting; -data.slot.setAttribute; -data.optin; -data.keyvalue; +// ix.js +data.ixId; +data.ixId; +data.ixSlot; +data.ixSlot; + +// kargo.js +data.options; +data.slot; + +// kixer.js +data.adslot; + +// mads.js +window.MADSAdrequest = {}; +window.MADSAdrequest.adrequest; +data.adrequest; + +// mediaimpact.js var asmi; asmi.sas; asmi.sas.call; asmi.sas.setup; -data.spot; +data.site; +data.page; +data.format; +data.slot.replace; + +// medianet.js +data.crid; +data.hasOwnProperty; +data.requrl; +data.refurl; +data.versionId; +data.timeout; + +// microad.js var MicroAd; MicroAd.Compass; MicroAd.Compass.showAd; +data.spot; + +// mixpo.js +data.subdomain; +data.guid; +data.embedv; +data.clicktag; +data.customtarget; +data.dynclickthrough; +data.viewtracking; +data.customcss; +data.local; +data.enablemraid; +data.jsplayer; + +// nativo.js +var PostRelease; +PostRelease.Start; +PostRelease.checkIsAdVisible; +var _prx; +data.delayByTime; +data.delayByTime; + +// nokta.js +data.category; + +// openadstream.js data.adhost +data.sitepage; data.pos; -var dfpData; -dfpData.dfp; -dfpData.targeting; +data.query; + +// openx.js var OX; OX._requestArgs; var OX_bidder_options; @@ -130,30 +328,100 @@ var oxRequest; oxRequest.addAdUnit; oxRequest.setAdSizes; oxRequest.getOrCreateAdUnit; -data.host; -data.nc; +var dfpData; +dfpData.dfp; +dfpData.targeting; data.dfpSlot; -data.zone; -data.sitepage; +data.nc; data.auid; + +// plista.js data.widgetname; +data.publickey; data.urlprefix; +data.item; +data.geo; +data.categories; + +// pubmatic.js +data.kadpageurl; + +// pubmine.js +data.adsafe; +data.wordads; +data.section; + +// pulsepoint.js +window.PulsePointHeaderTag; + +// rubicon.js var rubicontag; rubicontag.setFPV; rubicontag.setFPI; rubicontag.getSlot; rubicontag.getAdServerTargeting; -data.account; rubicontag.addKW; rubicontag.setUrl; rubicontag.setIntegration; +data.method; +data.overrideWidth; +data.width; +data.overrideHeight; +data.height; data.account; data.kw; data.visitor; data.inventory; +data.size; data.callback; + +// sharethrough.js +data.pkey; + +// sklik.js +data.elm; + +// smartadserver.js +var sas; +sas.callAmpAd; + +// smartclip.js +data.plc; +data.sz; +data.extra; + +// sortable.js +data.name; + +// sovrn.js +data.domain; +data.u; +data.iid; +data.aid; +data.z; +data.tf; + +// swoop.js +var Swoop +Swoop.announcePlace + +// taboola.js +data.referrer; +data.publisher; +data.mode; + +// teads.js +data.tag; +data.tag; +data.tag.tta; +data.tag.ttp; + +// webediads.js var wads; wads.init; +data.position; + +// weborama.js data.wbo_account_id; data.wbo_customparameter; data.wbo_tracking_element_id; @@ -170,87 +438,39 @@ data.wbo_script_variant; data.wbo_is_mobile; data.wbo_vars; data.wbo_weak_encoding; -data.psn; + +// yieldbot.js var yieldbot; yieldbot.psn; yieldbot.enableAsync; yieldbot.defineSlot; yieldbot.go; -data.ybSlot; yieldbot.nextPageview; yieldbot.getSlotCriteria; +data.psn; +data.ybSlot; + +// yieldmo.js data.ymid; -var PostRelease; -PostRelease.Start; -PostRelease.checkIsAdVisible; -var _prx; -data.delayByTime; -window.PulsePointHeaderTag; -data.tagid; -data.tagtype; -data.zergid; -window.zergnetWidgetId; -data.ankv; -data.ancat; -data.annid; -data.anwid; -data.antid; -data.anapiid; -window.MADSAdrequest = {}; -window.MADSAdrequest.adrequest; -data.divid; -/** - * @constructor - * @param {!Window} global - * @param {!Object} data - */ -window.EzoicAmpAd = function(global, data) {}; -window.EzoicAmpAd.prototype.createAd; -data.id; -data.d; -data.wid; -data.url; -data.customtarget; -data.dynclickthrough; -data.viewtracking; -data.customcss; -data.enablemraid; -data.jsplayer; -var sas; -sas.callAmpAd; -data.uuid; -data.embedcreated; -data.embedparent -data.embedlive + +// zedo.js var ZGTag; var geckoTag; var placement; -data.superId; -data.network; geckoTag.setAMP; geckoTag.addPlacement; +placement.includeRenderer; +geckoTag.loadAds; +geckoTag.placementReady; +data.charset; +data.superId; +data.network; data.placementId; data.channel; data.publisher; data.dim; -placement.includeRenderer; -geckoTag.loadAds; -geckoTag.placementReady; -data.plc; -data.sz; -data.extra; -var Fusion; -Fusion.on; -Fusion.on.warning; -Fusion.loadAds; -var ev; -ev.msg; -data.adServer; -data.mediaZone; -data.layout; -data.space; - -var Swoop -Swoop.announcePlace +data.renderer; -data.siteId +// zergnet.js +window.zergnetWidgetId; +data.zergid; diff --git a/ads/adsnative.md b/ads/adsnative.md index 22c1521eb86b6..51810cddf0f01 100644 --- a/ads/adsnative.md +++ b/ads/adsnative.md @@ -21,13 +21,13 @@ limitations under the License. ```html ``` ## Configuration -For configuration, please see [ad network documentation](https://dev.adsnative.com). +For configuration, please see [ad server documentation](http://dev.adsnative.com). Supported parameters: diff --git a/ads/google/a4a/test/test-traffic-experiments.js b/ads/google/a4a/test/test-traffic-experiments.js index adef5b8ff03a1..2bd5f86436c07 100644 --- a/ads/google/a4a/test/test-traffic-experiments.js +++ b/ads/google/a4a/test/test-traffic-experiments.js @@ -24,8 +24,8 @@ import { randomlySelectUnsetPageExperiments, validateExperimentIds, } from '../traffic-experiments'; +import {isExperimentOn, toggleExperiment} from '../../../../src/experiments'; import {EXPERIMENT_ATTRIBUTE} from '../utils'; -import {isExperimentOn} from '../../../../src/experiments'; import {dev} from '../../../../src/log'; import * as sinon from 'sinon'; @@ -71,8 +71,7 @@ describe('all-traffic-experiments-tests', () => { it('handles empty experiments list', () => { // Opt out of experiment. - // TODO(tdrl): remove the direct access to AMP_CONFIG - sandbox.win.AMP_CONFIG['testExperimentId'] = 0.0; + toggleExperiment(sandbox.win, 'testExperimentId', false, true); randomlySelectUnsetPageExperiments(sandbox.win, {}); expect(isExperimentOn(sandbox.win, 'testExperimentId'), 'experiment is on').to.be.false; @@ -80,7 +79,7 @@ describe('all-traffic-experiments-tests', () => { }); it('handles experiment not diverted path', () => { // Opt out of experiment. - sandbox.win.AMP_CONFIG['testExperimentId'] = 0.0; + toggleExperiment(sandbox.win, 'testExperimentId', false, true); randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); expect(isExperimentOn(sandbox.win, 'testExperimentId'), 'experiment is on').to.be.false; @@ -88,10 +87,10 @@ describe('all-traffic-experiments-tests', () => { 'testExperimentId')).to.not.be.ok; }); it('handles experiment diverted path: control', () => { - // Force experiment on by setting its triggering probability to 1, then + // Force experiment on. + toggleExperiment(sandbox.win, 'testExperimentId', true, true); // force the control branch to be chosen by making the accurate PRNG // return a value < 0.5. - sandbox.win.AMP_CONFIG['testExperimentId'] = 1.0; RANDOM_NUMBER_GENERATORS.accuratePrng.onFirstCall().returns(0.3); randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); expect(isExperimentOn(sandbox.win, 'testExperimentId'), @@ -100,10 +99,10 @@ describe('all-traffic-experiments-tests', () => { testExperimentSet['testExperimentId'].control); }); it('handles experiment diverted path: experiment', () => { - // Force experiment on by setting its triggering probability to 1, then - // force the experiment branch to be chosen by making the accurate PRNG + // Force experiment on. + toggleExperiment(sandbox.win, 'testExperimentId', true, true); + // Force the experiment branch to be chosen by making the accurate PRNG // return a value > 0.5. - sandbox.win.AMP_CONFIG['testExperimentId'] = 1.0; RANDOM_NUMBER_GENERATORS.accuratePrng.onFirstCall().returns(0.6); randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); expect(isExperimentOn(sandbox.win, 'testExperimentId'), @@ -112,12 +111,11 @@ describe('all-traffic-experiments-tests', () => { testExperimentSet['testExperimentId'].experiment); }); it('handles multiple experiments', () => { - sandbox.win.AMP_CONFIG = {}; - const config = sandbox.win.AMP_CONFIG; - config['expt_0'] = 1.0; - config['expt_1'] = 0.0; - config['expt_2'] = 1.0; - config['expt_3'] = 1.0; + toggleExperiment(sandbox.win, 'expt_0', true, true); + toggleExperiment(sandbox.win, 'expt_1', false, true); + toggleExperiment(sandbox.win, 'expt_2', true, true); + toggleExperiment(sandbox.win, 'expt_3', true, true); + const experimentInfo = { 'expt_0': { control: '0_c', @@ -153,9 +151,7 @@ describe('all-traffic-experiments-tests', () => { }); it('handles multi-way branches', () => { dev().info(TAG_, 'Testing multi-way branches'); - sandbox.win.AMP_CONFIG = {}; - const config = sandbox.win.AMP_CONFIG; - config['expt_0'] = 1.0; + toggleExperiment(sandbox.win, 'expt_0', true, true); const experimentInfo = { 'expt_0': { b0: '0_0', @@ -173,12 +169,11 @@ describe('all-traffic-experiments-tests', () => { '0_3'); }); it('handles multiple experiments with multi-way branches', () => { - sandbox.win.AMP_CONFIG = {}; - const config = sandbox.win.AMP_CONFIG; - config['expt_0'] = 1.0; - config['expt_1'] = 0.0; - config['expt_2'] = 1.0; - config['expt_3'] = 1.0; + toggleExperiment(sandbox.win, 'expt_0', true, true); + toggleExperiment(sandbox.win, 'expt_1', false, true); + toggleExperiment(sandbox.win, 'expt_2', true, true); + toggleExperiment(sandbox.win, 'expt_3', true, true); + const experimentInfo = { 'expt_0': { b0: '0_0', @@ -235,11 +230,8 @@ describe('all-traffic-experiments-tests', () => { experiment: '108642', }, }; - sandbox.win.AMP_CONFIG = {}; - const config = sandbox.win.AMP_CONFIG; - config['fooExpt'] = 0.0; + toggleExperiment(sandbox.win, 'fooExpt', false, true); randomlySelectUnsetPageExperiments(sandbox.win, exptAInfo); - config['fooExpt'] = 1.0; randomlySelectUnsetPageExperiments(sandbox.win, exptBInfo); // Even though we tried to set up a second time, using a config // parameter that should ensure that the experiment was activated, the diff --git a/ads/google/a4a/test/test-utils.js b/ads/google/a4a/test/test-utils.js index 84f61aa381852..db1a9f92ac336 100644 --- a/ads/google/a4a/test/test-utils.js +++ b/ads/google/a4a/test/test-utils.js @@ -17,10 +17,36 @@ import { extractGoogleAdCreativeAndSignature, additionalDimensions, + extractAmpAnalyticsConfig, + injectActiveViewAmpAnalyticsElement, } from '../utils'; +import {createElementWithAttributes} from '../../../../src/dom'; import {base64UrlDecodeToBytes} from '../../../../src/utils/base64'; +import { + installExtensionsService, +} from '../../../../src/service/extensions-impl'; +import { + MockA4AImpl, +} from '../../../../extensions/amp-a4a/0.1/test/utils'; +import '../../../../extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import {createIframePromise} from '../../../../testing/iframe'; +import * as sinon from 'sinon'; + +function setupForAdTesting(fixture) { + installDocService(fixture.win, /* isSingleDoc */ true); + const doc = fixture.doc; + // TODO(a4a-cam@): This is necessary in the short term, until A4A is + // smarter about host document styling. The issue is that it needs to + // inherit the AMP runtime style element in order for shadow DOM-enclosed + // elements to behave properly. So we have to set up a minimal one here. + const ampStyle = doc.createElement('style'); + ampStyle.setAttribute('amp-runtime', 'scratch-fortesting'); + doc.head.appendChild(ampStyle); +} describe('Google A4A utils', () => { + describe('#extractGoogleAdCreativeAndSignature', () => { it('should return body and signature', () => { const creative = 'some test data'; @@ -96,4 +122,99 @@ describe('Google A4A utils', () => { '3,4,1,2,11,12,5,6,100px,101px'); }); }); + + describe('#ActiveView AmpAnalytics integration', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + it('should extract config from headers', () => { + let url = ['https://foo.com?a=b', 'https://bar.com?d=f']; + const headers = { + get: function(name) { + expect(name).to.equal('X-AmpAnalytics'); + return JSON.stringify({url}); + }, + has: function(name) { + expect(name).to.equal('X-AmpAnalytics'); + return true; + }, + }; + expect(extractAmpAnalyticsConfig(headers)).to.deep.equal({urls: url}); + url = 'not an array'; + expect(extractAmpAnalyticsConfig(headers)).to.not.be.ok; + url = ['https://foo.com?a=b', 'https://bar.com?d=f']; + headers.has = function(name) { + expect(name).to.equal('X-AmpAnalytics'); + return false; + }; + expect(extractAmpAnalyticsConfig(headers)).to.not.be.ok; + }); + + it('should not create amp-analytics element if no urls', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const doc = fixture.doc; + const element = createElementWithAttributes(doc, 'amp-a4a', { + 'width': '200', + 'height': '50', + 'type': 'adsense', + }); + const config = {urls: []}; + const extensions = installExtensionsService(fixture.win); + const loadExtensionSpy = sandbox.spy(extensions, 'loadExtension'); + injectActiveViewAmpAnalyticsElement( + new MockA4AImpl(element), extensions, config); + expect(loadExtensionSpy.withArgs('amp-analytics')).to.not.be.called; + const ampAnalyticsElements = element.querySelectorAll('amp-analytics'); + expect(ampAnalyticsElements.length).to.equal(0); + }); + }); + it('should load extension and create amp-analytics element', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const doc = fixture.doc; + const element = createElementWithAttributes(doc, 'amp-a4a', { + 'width': '200', + 'height': '50', + 'type': 'adsense', + }); + const urls = ['https://foo.com?hello=world', 'https://bar.com?a=b']; + const config = {urls}; + const extensions = installExtensionsService(fixture.win); + const loadExtensionSpy = sandbox.spy(extensions, 'loadExtension'); + injectActiveViewAmpAnalyticsElement( + new MockA4AImpl(element), extensions, config); + expect(loadExtensionSpy.withArgs('amp-analytics')).to.be.called; + const ampAnalyticsElements = element.querySelectorAll('amp-analytics'); + expect(ampAnalyticsElements.length).to.equal(1); + const ampAnalyticsElement = ampAnalyticsElements[0]; + expect(ampAnalyticsElement.getAttribute('scoped')).to.equal(''); + const scriptElements = ampAnalyticsElement.querySelectorAll('script'); + expect(scriptElements.length).to.equal(1); + const scriptElement = scriptElements[0]; + expect(scriptElement.getAttribute('type')).to.equal('application/json'); + expect(JSON.parse(scriptElement.textContent)).to.deep.equal( + { + transport: {beacon: false, xhrpost: false}, + requests: { + visibility1: urls[0], visibility2: urls[1], + }, + triggers: { + continuousVisible: { + on: 'visible', + request: ['visibility1', 'visibility2'], + visibilitySpec: { + selector: 'amp-ad', + selectionMethod: 'closest', + visiblePercentageMin: 50, + continuousTimeMin: 1000, + }, + }, + }, + }); + }); + }); + }); }); diff --git a/ads/google/a4a/utils.js b/ads/google/a4a/utils.js index 8b70507a924e1..9ecd8dee8a9cf 100644 --- a/ads/google/a4a/utils.js +++ b/ads/google/a4a/utils.js @@ -24,6 +24,7 @@ import {isProxyOrigin} from '../../../src/url'; import {viewerForDoc} from '../../../src/viewer'; import {base64UrlDecodeToBytes} from '../../../src/utils/base64'; import {domFingerprint} from '../../../src/utils/dom-fingerprint'; +import {createElementWithAttributes} from '../../../src/dom'; /** @const {string} */ const AMP_SIGNATURE_HEADER = 'X-AmpAdSignature'; @@ -31,6 +32,9 @@ const AMP_SIGNATURE_HEADER = 'X-AmpAdSignature'; /** @const {string} */ const CREATIVE_SIZE_HEADER = 'X-CreativeSize'; +/** @type {string} */ +const AMP_ANALYTICS_HEADER = 'X-AmpAnalytics'; + /** @const {number} */ const MAX_URL_LENGTH = 4096; @@ -61,6 +65,10 @@ export const QQID_HEADER = 'X-QQID'; */ export const EXPERIMENT_ATTRIBUTE = 'data-experiment-id'; +/** @typedef {{urls: !Array}} + */ +export let AmpAnalyticsConfigDef; + /** * Check whether Google Ads supports the A4A rendering pathway is valid for the * environment by ensuring native crypto support and page originated in the @@ -84,7 +92,7 @@ export function isGoogleAdsA4AValidEnvironment(win) { } /** - * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a class instance * @param {string} baseUrl * @param {number} startTime * @param {!Array} queryParams @@ -331,3 +339,74 @@ export function additionalDimensions(win, viewportSize) { innerWidth, innerHeight].join(); }; + +/** + * Extracts configuration used to build amp-analytics element for active view. + * + * @param {!../../../src/service/xhr-impl.FetchResponseHeaders} responseHeaders + * XHR service FetchResponseHeaders object containing the response + * headers. + * @return {?AmpAnalyticsConfigDef} config or null if invalid/missing. + */ +export function extractAmpAnalyticsConfig(responseHeaders) { + if (responseHeaders.has(AMP_ANALYTICS_HEADER)) { + try { + const analyticsConfig = + JSON.parse(responseHeaders.get(AMP_ANALYTICS_HEADER)); + dev().assert(Array.isArray(analyticsConfig['url'])); + return {urls: analyticsConfig.url}; + } catch (err) { + dev().error('AMP-A4A', 'Invalid analytics', err, + responseHeaders.get(AMP_ANALYTICS_HEADER)); + } + } + return null; +} + +/** + * Creates amp-analytics element within a4a element using urls specified + * with amp-ad closest selector and min 50% visible for 1 sec. + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {!../../../src/service/extensions-impl.Extensions} extensions + * @param {?AmpAnalyticsConfigDef} inputConfig + */ +export function injectActiveViewAmpAnalyticsElement( + a4a, extensions, inputConfig) { + if (!inputConfig || !inputConfig.urls.length) { + return; + } + extensions.loadExtension('amp-analytics'); + const ampAnalyticsElem = + a4a.element.ownerDocument.createElement('amp-analytics'); + ampAnalyticsElem.setAttribute('scoped', ''); + const config = { + 'transport': {'beacon': false, 'xhrpost': false}, + 'triggers': { + 'continuousVisible': { + 'on': 'visible', + 'visibilitySpec': { + 'selector': 'amp-ad', + 'selectionMethod': 'closest', + 'visiblePercentageMin': 50, + 'continuousTimeMin': 1000, + }, + }, + }, + }; + const requests = {}; + const urls = inputConfig.urls; + for (let idx = 1; idx <= urls.length; idx++) { + // TODO: Ensure url is valid and not freeform JS? + requests[`visibility${idx}`] = `${urls[idx - 1]}`; + } + // Security review needed here. + config['requests'] = requests; + config['triggers']['continuousVisible']['request'] = Object.keys(requests); + const scriptElem = createElementWithAttributes( + /** @type {!Document} */(a4a.element.ownerDocument), 'script', { + 'type': 'application/json', + }); + scriptElem.textContent = JSON.stringify(config); + ampAnalyticsElem.appendChild(scriptElem); + a4a.element.appendChild(ampAnalyticsElem); +} diff --git a/build-system/config.js b/build-system/config.js index c6b06c6d3b4c0..bf98eb589184f 100644 --- a/build-system/config.js +++ b/build-system/config.js @@ -57,6 +57,7 @@ var testPaths = commonTestPaths.concat([ var integrationTestPaths = commonTestPaths.concat([ 'test/integration/**/*.js', + 'test/functional/test-error.js', 'extensions/**/test/integration/**/*.js', ]); diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js index d9896c63c4cc7..725c835d2c771 100644 --- a/build-system/dep-check-config.js +++ b/build-system/dep-check-config.js @@ -97,6 +97,7 @@ exports.rules = [ // somewhere else at some point 'ads/google/a4a/**->src/ad-cid.js', 'ads/google/a4a/**->src/document-info.js', + 'ads/google/a4a/**->src/dom.js', 'ads/google/a4a/**->src/experiments.js', 'ads/google/a4a/**->src/timer.js', 'ads/google/a4a/**->src/viewer.js', diff --git a/build-system/global-configs/prod-config.json b/build-system/global-configs/prod-config.json index 07717ede0236f..b1187bbadd742 100644 --- a/build-system/global-configs/prod-config.json +++ b/build-system/global-configs/prod-config.json @@ -21,6 +21,7 @@ "amp-playbuzz": 1, "make-body-relative": 1, "chunked-amp": 1, + "visibility-v2": 1, "amp-selector": 1, "sentinel-name-change": 1 } diff --git a/build-system/server.js b/build-system/server.js index d02ceb147dd9e..28d9fdb6963e6 100644 --- a/build-system/server.js +++ b/build-system/server.js @@ -532,6 +532,10 @@ app.get(['/examples/*', '/test/manual/*'], function(req, res, next) { } filePath = filePath.substr(0, filePath.length - 9) + '.html'; fs.readFileAsync(process.cwd() + filePath, 'utf8').then(file => { + if (req.query['amp_js_v']) { + file = addViewerIntegrationScript(req.query['amp_js_v'], file); + } + file = replaceUrls(mode, file); // Extract amp-ad for the given 'type' specified in URL query. @@ -638,6 +642,16 @@ app.get(['/dist/cache-sw.min.html', '/dist/cache-sw.max.html'], function(req, re * End Cache SW LOCALDEV section */ +/** + * Web worker binary. + */ +app.get(['/dist/ww.js', '/dist/ww.max.js'], function(req, res) { + fs.readFileAsync(process.cwd() + req.path).then(file => { + res.setHeader('Content-Type', 'text/javascript'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(file); + }); +}); /** * @param {string} mode @@ -659,6 +673,31 @@ function replaceUrls(mode, file, hostName) { return file; } +/** + * @param {string} ampJsVersion + * @param {string} file + */ +function addViewerIntegrationScript(ampJsVersion, file) { + ampJsVersion = parseFloat(ampJsVersion); + if (!ampJsVersion) { + return file; + } + var viewerScript; + if (Number.isInteger(ampJsVersion)) { + // Viewer integration script from gws, such as + // https://cdn.ampproject.org/viewer/google/v7.js + viewerScript = ''; + } else { + // Viewer integration script from runtime, such as + // https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js + viewerScript = ''; + } + file = file.replace('', viewerScript + ''); + return file; +} + /** * @param {string} path * @return {string} diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index 7fa1f69c482eb..42249d6c80301 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -484,21 +484,29 @@ var forbiddenTerms = { message: 'Do not access AMP_CONFIG directly. Use isExperimentOn() ' + 'and getMode() to access config', whitelist: [ - 'build-system/server.js', 'build-system/amp.extern.js', - 'build-system/tasks/prepend-global/test.js', + 'build-system/server.js', 'build-system/tasks/prepend-global/index.js', - 'src/service-worker/core.js', - 'src/service-worker/error-reporting.js', - 'src/mode.js', - 'src/experiments.js', - 'src/config.js', + 'build-system/tasks/prepend-global/test.js', 'dist.3p/current/integration.js', + 'src/config.js', + 'src/experiments.js', + 'src/mode.js', + 'src/service-worker/core.js', + 'src/worker-error-reporting.js', ], }, 'data:image/svg(?!\\+xml;charset=utf-8,)[^,]*,': { message: 'SVG data images must use charset=utf-8: ' + '"data:image/svg+xml;charset=utf-8,..."', + }, + 'installWorkerErrorReporting': { + message: 'Should only be used in worker entry points', + whitelist: [ + 'src/web-worker/web-worker.js', + 'src/service-worker/shell.js', + 'src/worker-error-reporting.js', + ], } }; @@ -563,7 +571,6 @@ var forbiddenTermsSrcInclusive = { '\\.getBBox\\(': bannedTermsHelpString, '\\.getBoundingClientRect\\(': bannedTermsHelpString, '\\.getClientRects\\(': bannedTermsHelpString, - '\\.getComputedStyle\\(': bannedTermsHelpString, '\\.getMatchedCSSRules\\(': bannedTermsHelpString, '\\.postMessage\\(': bannedTermsHelpString, '\\.scrollBy\\(': bannedTermsHelpString, @@ -573,6 +580,14 @@ var forbiddenTermsSrcInclusive = { '\\.webkitConvertPointFromNodeToPage\\(': bannedTermsHelpString, '\\.webkitConvertPointFromPageToNode\\(': bannedTermsHelpString, '\\.scheduleUnlayout\\(': bannedTermsHelpString, + 'getComputedStyle\\(': { + message: 'Due to various bugs in Firefox, you must use the computedStyle ' + + 'helper in style.js.', + whitelist: [ + 'src/style.js', + 'dist.3p/current/integration.js', + ], + }, // Super complicated regex that says "find any querySelector method call that // is passed as a variable anything that is not a string, or a string that // contains a space. @@ -594,6 +609,7 @@ var forbiddenTermsSrcInclusive = { 'src/shadow-embed.js', 'extensions/amp-ad/0.1/amp-ad.js', 'extensions/amp-a4a/0.1/amp-a4a.js', + 'ads/google/a4a/utils.js', ], }, 'loadElementClass': { diff --git a/contributing/creating-great-first-issues.md b/contributing/creating-great-first-issues.md new file mode 100644 index 0000000000000..39d8c20950dcc --- /dev/null +++ b/contributing/creating-great-first-issues.md @@ -0,0 +1,24 @@ +# Creating Great First Issues + +The AMP Project welcomes new contributors and we want to make it as easy as possible for them to contribute. For many new contributors (who may not have open source/Git/AMP/etc. experience) it can be difficult to figure out how to get started. + +To help these new contributors get oriented we curate [Great First Issues](https://github.com/ampproject/amphtml/milestone/25). A Great First Issue is a starter issue that a new contributor can use to get comfortable contributing to the AMP Project. + +We depend on experienced members of the community to identify bugs/features that would provide this orientation and to then create a well-documented Great First Issue for them. + +Our approach is inspired by [Hoodie](http://hood.ie/) which collects starter issues in their [Hoodie Camp](https://github.com/hoodiehq/camp). See the [Hoodie Camp Issues](https://github.com/hoodiehq/camp/issues) for many examples of what great first issues look like. (Hoodie's [Welcoming Communities](http://hood.ie/blog/welcoming-communities.html) post is also a great read.) + +## Qualities of a Great First Issues + +Keep these qualities in mind when creating your Great First Issues: + +* **You should have a good sense of exactly what is needed to resolve the issue.** In fact it would probably be faster for you to just fix the issue than to create the Great First Issue but the benefits to the community make the cost of creating the Great First Issue worth it. +* **Even issues that require a very small/straightforward fix can be Great First Issues.** The focus of Great First Issues is not just about getting fixes in AMP; they are also a chance for new contributors to get set up with Git/GitHub, get familiar with building/making changes/testing in AMP and experience going through the Pull Request process. +* **The issue should be P3.** The issue may not get picked up right away and the person fixing it may encounter problems while working on it so it should be an issue we are okay with going unresolved for a while. +* **New contributors have a variety of backgrounds; we should have a variety of Great First Issues.** The goal isn't for each Great First Issue to be a great first issue for _every_ new contributor. Some new contributors may have extensive web development experience but haven't used Git; some may know everything about Git & Pull Requests but don't have a lot of experience with web components. You can specify "What you will need to know" in the bug so new contributors can find a Great First Issue that matches their current experience. + +## How to create a Great First Issue + +* When you identify an issue that would make a Great First Issue, create a new issue using the [Great First Issues Template](great-first-issues-template.md) (inspired by Hoodie's [template](https://github.com/hoodiehq/camp/blob/gh-pages/ISSUE_TEMPLATE.md)). Copy the [raw markdown](https://raw.githubusercontent.com/ampproject/amphtml/master/contributing/great-first-issues-template.md) into your issue and follow the guidance in the comments. +* Add the issue to the [Great First Issues milestone](https://github.com/ampproject/amphtml/milestone/25). +* If you come across a good candidate for a Great First Issue but are momentarily unable to spend the time filling out the template add the [GFI Candidate](https://github.com/ampproject/amphtml/labels/GFI%20Candidate) label to the issue. The time spent converting GFI Candidates to Great First Issues will pay off for the AMP Project so please remember to come back to issues you labeled as a GFI Candidate. diff --git a/contributing/getting-started-e2e.md b/contributing/getting-started-e2e.md index 7f0b4f7528c87..57bba52fefb22 100644 --- a/contributing/getting-started-e2e.md +++ b/contributing/getting-started-e2e.md @@ -26,7 +26,7 @@ If you're already familiar with Git/GitHub/etc. or you just want to know what co If you have a question or are unsure about something while following this end-to-end guide, you can get help from the AMP Project community in many ways: -* If you are tackling a [Great First Issue](https://github.com/ampproject/amphtml/labels/Great%20First%20Issues) or other GitHub issue you can ask a question as a comment on the issue directly. This works particularly well if the question is about how to make progress on that specific issue. +* If you are tackling a [Great First Issue](https://github.com/ampproject/amphtml/milestone/25) or other GitHub issue you can ask a question as a comment on the issue directly. This works particularly well if the question is about how to make progress on that specific issue. * The [#welcome-contributors](https://amphtml.slack.com/messages/welcome-contributors/) channel on Slack is a place for new contributors getting up to speed in the AMP Project to find help. You should feel comfortable asking any question in there no matter how basic it may seem to you (e.g. problems getting Git set up, errors during a build, etc.). If you haven't already signed up for our Slack, you'll need to [request an invitation](https://docs.google.com/forms/d/e/1FAIpQLSd83J2IZA6cdR6jPwABGsJE8YL4pkypAbKMGgUZZriU7Qu6Tg/viewform?fbzx=4406980310789882877). @@ -216,7 +216,7 @@ In the workflow we will be using you'll go to the master branch on your local re git checkout master # pull in the latest changes from the remote amphtml repository -git pull upstream master +git pull upstream master ``` If there have been any changes you'll see the details of what changed, otherwise you'll see a message like `Already up-to-date`. @@ -224,7 +224,7 @@ After running that `git pull` command your local master branch has the latest fi ``` # go to the branch you want to sync -git checkout +git checkout # bring the latest changes from your master branch into this branch git rebase master @@ -412,21 +412,36 @@ GitHub offers a convenient "Delete branch" button on the PR page after the chang ``` # go back to the master branch -git checkout master +git checkout master # delete the branch in your local repository git branch -D # delete the branch in your GitHub fork (if you didn't use the UI) -git push origin --delete +git push origin --delete ``` -# Celebrate +# See your changes in production + +**Congratulations on making your first change to AMP!** + +If your change affected internal documentation, tests, the build process, etc. you can generally see your changes right after they're merged. If your change was to the code that runs on AMP pages across the web you'll have to wait for the change to be included in a release. + +In general we cut a release of amphtml on Wednesdays during working hours (Pacific time) and push it to the AMP Dev Channel the next day. After verifying there are no issues, we push that build to 1% of AMP pages the following Monday and complete the push to all AMP pages a few days later on Thursday. That is: on Thursday we will typically push last week's build to all AMP pages and this week's build to the Dev Channel. + +**Once the push of the build that includes your change is complete all users of AMP will be using the code you contributed!** + +You can see whether your change made it into a given build on the [amphtml Releases page](https://github.com/ampproject/amphtml/releases). The build marked `Pre-release` is the version on the Dev Channel and the build marked `Latest Release` is what is running in production. Your Pull Request will be listed in the first build that includes it; if you don't see your Pull Request listed it will likely be in the next build. + +You can set your browser to use the Dev Channel build by enabling `dev-channel` on the [AMP Experiments](https://cdn.ampproject.org/experiments.html) page. This will let you see how your changes will affect any AMP page before your changes are rolled out to all AMP pages. Note that this only affects the browser in which you enable the experiment. + +You can verify the AMP version your browser is using for a given page by looking at your browser's developer console. After loading an AMP page (e.g. [https://ampproject.org](https://ampproject.org)) the console will have a message like `Powered by AMP ⚡ HTML – Version `). The `` will match one of the build numbers on the [amphtml Releases page](https://github.com/ampproject/amphtml/releases). -If you've gone through the steps above and had your first pull request approved and merged--or even if you just got the amphtml code built and played around with some local changes--congratulations! +The [Release Schedule](release-schedule.md) doc has more details on the release process. -Now that you know the process for making changes to the AMP Project and you already have most of the heavy lifting done we look forward to seeing your future contributions to the project. :) +# ⚡⚡⚡... +Now that you know the process for making changes to the AMP Project you already have most of the heavy lifting done. **We look forward to seeing your future contributions to the project.** :) # Other resources @@ -437,4 +452,4 @@ This end-to-end guide provided enough details to get a basic understanding of a * the [Git cheat sheet](https://services.github.com/on-demand/downloads/github-git-cheat-sheet.pdf) from GitHub provides a quick reference to some common commands, including many we didn't cover in this guide (such as [diff](https://www.git-tower.com/learn/git/ebook/en/command-line/advanced-topics/diffs) and [log](https://git-scm.com/book/en/v2/Git-Basics-Viewing-the-Commit-History)) * a [Training & Guides video series](https://www.youtube.com/user/GitHubGuides) * The official [Git docs](https://git-scm.com/doc) have a lot of information including the [reference docs](https://git-scm.com/docs) and an online version of [Pro Git](https://git-scm.com/book/en/v2). -* You may see discussions about the difference between rebasing and merging in Git, and we glossed over the details in this guide. If you're curious about the difference the Atlassian [Merging vs. Rebasing](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) tutorial has a good explanation. \ No newline at end of file +* You may see discussions about the difference between rebasing and merging in Git, and we glossed over the details in this guide. If you're curious about the difference the Atlassian [Merging vs. Rebasing](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) tutorial has a good explanation. diff --git a/contributing/getting-started-quick.md b/contributing/getting-started-quick.md index 5d4a4f39d5a33..871674e4091f9 100644 --- a/contributing/getting-started-quick.md +++ b/contributing/getting-started-quick.md @@ -91,3 +91,10 @@ This Quick Start guide is the TL;DR version of the longer [end-to-end guide](get * Go to the master branch: `git checkout master` * Delete your local branch: `git branch -D ` * Delete the GitHub fork branch: `git push origin --delete ` + +# See your changes in production + +* Barring any issues releases are cut on Wednesdays, pushed to Dev Channel Thursday, pushed to 1% of AMP pages on Monday and pushed to all pages a few days later on Thursday. +* The [amphtml Releases page](https://github.com/ampproject/amphtml/releases) will list your PR in the first build that contains it. `Pre-release` is the build on the Dev Channel, `Latest Release` is the build in production. +* Opt-in to using the Dev Channel in a browser by enabling `dev-channel` on the [AMP Experiments](https://cdn.ampproject.org/experiments.html) page. +* Find the AMP version being used on a page in the developer console, i.e. `Powered by AMP ⚡ HTML – Version `). diff --git a/contributing/great-first-issues-template.md b/contributing/great-first-issues-template.md new file mode 100644 index 0000000000000..721767dde9ea3 --- /dev/null +++ b/contributing/great-first-issues-template.md @@ -0,0 +1,64 @@ + + +### First Timers Only +We know figuring out the process for contributing to an open source project can be intimidating, so we created this issue as a way for you to learn the ropes. (If you feel comfortable contributing to open source projects, please leave this issue for someone else.) + + +### What you will need to know + + +### Background + + +### Motivation + + +### The bug + + +### Step by step +- [ ] Claim this issue by adding a comment below. Please only claim this bug if you plan on starting work in the next day or so. +- [ ] If you aren't too familiar with Git/GitHub, see the [Getting Started End-to-End Guide](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md) for [an intro to Git & GitHub,](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#intro-to-git-and-github) and [how to get a copy of the code](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#get-a-copy-of-the-amphtml-code). You can also refer to the [Quick Start Guide](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-quick.md) for the necessary setup steps with less explanation than the End-to-End guide. +- [ ] Follow the instructions for [building AMP](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#building-amp-and-starting-a-local-server). +- [ ] [Create a Git branch](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#create-a-git-branch) for making your changes. + +- [ ] [Commit your changes](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#edit-files-and-commit-them) frequently. +- [ ] [Push your changes to GitHub](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#push-your-changes-to-your-github-fork). + +- [ ] [Create a Pull Request](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#send-a-pull-request-ie-request-a-code-review). Mention `closes Issue ` in the description. +- [ ] [Respond to your reviewer's comments](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#respond-to-pull-request-comments) (if any). + + +Once approved, your changes will be merged. **⚡⚡⚡Congrats on making your first contribution to the AMP Project!⚡⚡⚡** You'll be able to see it [live across the web soon](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md)! + +Thanks, and we hope to see more contributions from you soon. + +### Questions? + + +If you have questions ask in this issue or on your Pull Request (if you've created one) or see the [How to get help](https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-e2e.md#how-to-get-help) section of the Getting Started guide. diff --git a/contributing/issue-priorities.md b/contributing/issue-priorities.md new file mode 100644 index 0000000000000..19a4225cc5925 --- /dev/null +++ b/contributing/issue-priorities.md @@ -0,0 +1,32 @@ + + +## AMP GitHub Issue Priorities +The AMP team is using the below priorities and guidelines to make it easier to order issues by the level of attention they need. +These guidelines can give you also a good overview of when to expect updates or closure of issues. + +If you would like to give a hint regarding the priority of the issue you can add the appropriate priority [GitHub label](https://github.com/ampproject/amphtml/labels). Note that the priority may be changed as the issue is triaged based on the project's roadmap and backlog. + +### Priorities & Guidelines + + + Priority | What it means | Guidelines for Bugs | Guidelines for FRs +--------------------| ---------------------- | ---------------------------------| --------------------------------- +P0: Drop Everything |
  • Outage
  • Critical production issue
|
  • Drop everything until this is fixed
  • Should be assigned and accepted within 24 hours of filing
  • Provide regular bug update on status and ETA of the fix, once every day after acceptance
  • Fixed in 7 days (next release or patch release, whichever is sooner)
|
  • We do not use P0 for FRs
+P1: High Priority |
  • Breakage of a critical feature or user journey
  • Highly significant feature
|
  • Should be assigned and accepted within 48 hours of filing
  • Should be updated by the team once every week after acceptance
  • Fixed within 30 days or updated with explanation and timeline
|
  • Implementation guidelines: 2 weeks
  • Feature requests are not P1, unless a situation develops where the lack of the feature is significant user problem (e.g. lack of the feature may block a significant number of AMP implementations)
+P2: Soon |
  • Breakage of a non-critical feature or user journey
  • Major usability problem (users frequently do the wrong thing)
  • Important feature
|
  • Best effort
  • Fixed in a quarter
|
  • Implementation guidelines: 1 Quarter
  • Generally higher priority feature requests
  • These are mostly features that have been specifically scheduled to meet our roadmap +P3: When Possible |
    • Minor usability problem
    • Polish
    • Minor features
    |
    • Best effort
    |
    • When possible
    + diff --git a/contributing/release-schedule.md b/contributing/release-schedule.md new file mode 100644 index 0000000000000..8d70b4e6becf4 --- /dev/null +++ b/contributing/release-schedule.md @@ -0,0 +1,26 @@ +# Release Schedule + +We push a new release of AMP to all AMP pages every week on Thursday. The more detailed schedule is as follows: + +- Every Wednesday we cut a green release at the latest commit that passed all tests. +- On Thursday this is pushed to users of AMP who opted into the [AMP Dev Channel](#amp-dev-channel). +- On Monday we check error rates for opt-in users and bug reports and if everything looks fine, we push this new release to 1% of AMP pages. +- We then continue to monitor error rates and bug reports throughout the week. +- On Thursday the "Dev Channel" release from last Thursday is then pushed to all users. + +You can always follow the current release state of AMP on our [releases page](https://github.com/ampproject/amphtml/releases). The release used by most users is marked as `Latest release` and the current Dev Channel release is marked as `Pre-release`. + +### AMP Dev Channel + +AMP Dev Channel is a way to opt a browser into using a newer version of the AMP JS libraries. + +This release **may be less stable** and it may contain features not available to all users. Opt into this option if you'd like to help test new versions of AMP, report bugs or build documents that require a new feature that is not yet available to everyone. + +Opting into Dev Channel is great to: + +- test and play with new features not yet available to all users. +- use in Q&A to ensure that your site is compatible with the next version of AMP. + +If you find an issue that appears to only occur in the Dev Channel version of AMP, please [file an issue](https://github.com/ampproject/amphtml/issues/new) with a description of the problem. Please always include a URL to a page that reproduces the issue. + +To opt your browser into the AMP Dev Channel, go to [the AMP experiments page](https://cdn.ampproject.org/experiments.html) and activate the "AMP Dev Channel" experiment. Please subscribe to our [low-volume announcements](https://groups.google.com/forum/#!forum/amphtml-announce) mailing list to get notified about important/breaking changes about AMP. diff --git a/css/Z_INDEX.md b/css/Z_INDEX.md index e7dfd9c5aa4c4..c48ea3b697a34 100644 --- a/css/Z_INDEX.md +++ b/css/Z_INDEX.md @@ -32,10 +32,7 @@ amp-lightbox | 1000 | extensions .-amp-sidebar-mask | 2147483646 | extensions/amp-sidebar/0.1/amp-sidebar.css .amp-lbv-button-prev | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css .amp-lbv-button-next | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css - -.amp-lbv-button-gallery | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css - -.amp-lbv-button-close | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css - -.amp-lbv-button-back | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css -amp-sidebar | 2147483647 | extensions/amp-sidebar/0.1/amp-sidebar.css \ No newline at end of file +.amp-lbv-button-gallery | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.amp-lbv-button-close | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.amp-lbv-button-back | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +amp-sidebar | 2147483647 | extensions/amp-sidebar/0.1/amp-sidebar.css diff --git a/css/amp.css b/css/amp.css index f7e62c1156fd3..6afb31d34f169 100644 --- a/css/amp.css +++ b/css/amp.css @@ -225,6 +225,16 @@ i-amphtml-sizer { overflow: hidden !important; } +.i-amphtml-scroll-disabled { + overflow-x: hidden !important; + overflow-y: hidden !important; +} + +#i-amphtml-wrapper.i-amphtml-scroll-disabled { + overflow-x: hidden !important; + overflow-y: hidden !important; +} + /* "notbuild" classes are set as soon as an element is created and removed as soon as the element is built. */ diff --git a/examples/ads.amp.html b/examples/ads.amp.html index 2a709a1aaf26f..3b9d0d7baaf1b 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -38,29 +38,6 @@ padding: 0; height:100px; } - amp-ad div[placeholder] { - background-color: lightgray; - } - amp-ad div[fallback] { - background-color: black; - } - amp-ad div[placeholder]::after { - content: "loading ..."; - } - amp-ad div[fallback]::after { - content: "No ad"; - color: white; - } - amp-ad div[placeholder]::after, - amp-ad div[fallback]::after { - font-size: 20px; - line-height: 30px; - text-align: center; - position: absolute; - left: 0; - right: 0; - top: calc(50% - 15px); - } @@ -199,8 +176,6 @@

    A9

    data-aax_size="300x250" data-aax_pubname="test123" data-aax_src="302"> -
    -

    AccessTrade

    @@ -208,8 +183,6 @@

    AccessTrade

    type="accesstrade" data-atops="r" data-atrotid="00000000000008c06y"> -
    -

    Adblade

    @@ -218,8 +191,6 @@

    Adblade

    data-width="300" data-height="250" data-cid="19626-3798936394"> -
    -

    AdButler

    @@ -227,8 +198,6 @@

    AdButler

    type="adbutler" data-account="167283" data-zone="212491"> -
    -

    ADITION

    @@ -236,16 +205,12 @@

    ADITION

    type="adition" data-version="1" data-wp_id="3470234"> -
    -

    Ad Generation

    -
    -

    Adman

    @@ -254,8 +219,6 @@

    Adman

    data-ws="17342" data-s="300x250" data-host="talos.adman.gr"> -
    -

    AdReactor

    @@ -264,8 +227,6 @@

    AdReactor

    data-pid=790 data-zid=9 data-custom3="No Type"> -
    -

    AdSense

    @@ -273,8 +234,6 @@

    AdSense

    type="adsense" data-ad-client="ca-pub-2005682797531342" data-ad-slot="7046626912"> -
    -

    AdsNative

    @@ -284,8 +243,6 @@

    AdsNative

    data-ancat="IAB1,IAB2" data-antid="abc" data-ankv="key:val,key:val2"> -
    -

    AdSpeed

    @@ -302,8 +259,6 @@

    AdSpirit

    type="adspirit" data-asm-params="&pid=4" data-asm-host="help.adspirit.de"> -
    -

    AdStir 320x50 banner

    @@ -311,16 +266,12 @@

    AdStir 320x50 banner

    type="adstir" data-app-id="MEDIA-343ded3e" data-ad-spot="1"> -
    -

    AdTech (1x1 fake image ad)

    -
    -

    AdThrive 320x50 banner

    @@ -329,8 +280,6 @@

    AdThrive 320x50 banner

    data-site-id="test" data-ad-unit="AdThrive_Content_1" data-sizes="320x50"> -
    -

    AdThrive 300x250 banner

    @@ -339,8 +288,6 @@

    AdThrive 300x250 banner

    data-site-id="test" data-ad-unit="AdThrive_Content_2" data-sizes="300x250"> -
    -

    Ad Up Technology

    @@ -350,8 +297,6 @@

    Ad Up Technology

    data-placementkey="ae7906d535ce47fbb29fc5f45ef910b4" data-query="reisen;mallorca;spanien" data-adtest="1"> -
    -

    Adverline

    @@ -359,8 +304,6 @@

    Adverline

    type="adverline" data-id=13625 data-plc=3> -
    -

    Adverticum

    @@ -368,8 +311,6 @@

    Adverticum

    type="adverticum" data-goa3zone="4316734" data-costumeTargetString="bWFsYWM="> -
    -

    AdvertServe

    @@ -378,8 +319,6 @@

    AdvertServe

    data-client="tester" data-pid=0 data-zid=68> -
    -

    Affiliate-B

    @@ -388,8 +327,6 @@

    Affiliate-B

    data-afb_a="l44x-y174897c" data-afb_p="g2m" data-afb_t="i"> -
    -

    AMoAd banner

    @@ -397,8 +334,6 @@

    AMoAd banner

    type="amoad" data-ad-type="banner" data-sid="62056d310111552c951d19c06bfa71e16e615e39493eceff667912488bc576a6"> -
    -

    AMoAd native

    @@ -406,8 +341,6 @@

    AMoAd native

    type="amoad" data-ad-type="native" data-sid="62056d310111552c951d19c06bfa71e1bc19eea7e94d0a6e73e46a9402dbee47"> -
    -

    AppNexus with JSON based configuration multi ad

    @@ -415,16 +348,12 @@

    AppNexus with JSON based configuration multi ad

    type="appnexus" data-target="apn_ad_1" json='{"pageOpts":{"member": 958}, "adUnits": [{"disablePsa": true, "tagId": 6063968,"sizes": [300,250],"targetId": "apn_ad_1"}, {"tagId": 6063968,"sizes": [728,90],"targetId":"apn_ad_2"}]}'> -
    -
    -
    -

    Atomx

    @@ -438,8 +367,6 @@

    brainy

    type="brainy" data-aid="10" data-slot-id="3347"> -
    -

    CA A.J.A. Infeed

    @@ -447,24 +374,18 @@

    CA A.J.A. Infeed

    type="caajainfeed" data-ad-spot="thtuegn1pAQ" data-test="true"> -
    -

    CA ProFit-X

    -
    -

    Chargeads

    -
    -

    Colombia ad

    @@ -475,8 +396,6 @@

    Colombia ad

    data-clmb_slot="129883" data-clmb_position="1" data-clmb_section="0"> -
    -

    Content.ad Banner 320x50

    @@ -486,8 +405,6 @@

    Content.ad Banner 320x50

    data-d="bm9uYmxvY2tpbmcuaW8=" data-wid="218710" data-url="nonblocking.io"> -
    -

    Content.ad Banner 300x250

    @@ -497,8 +414,6 @@

    Content.ad Banner 300x250

    data-d="bm9uYmxvY2tpbmcuaW8=" data-wid="218706" data-url="nonblocking.io"> -
    -

    Content.ad Banner 300x250 2x2

    @@ -517,8 +432,6 @@

    Content.ad Banner 300x600

    data-d="bm9uYmxvY2tpbmcuaW8=" data-wid="218708" data-url="nonblocking.io"> -
    -

    Content.ad Banner 300x600 5x2

    @@ -528,8 +441,6 @@

    Content.ad Banner 300x600 5x2

    data-d="bm9uYmxvY2tpbmcuaW8=" data-wid="218709" data-url="nonblocking.io"> -
    -

    Criteo Passback

    @@ -539,8 +450,6 @@

    Criteo Passback

    type="criteo" data-tagtype="passback" data-zone="314159"> -
    -

    Criteo RTA

    @@ -552,8 +461,6 @@

    Criteo RTA

    data-slot="/2729856/iframe_test_new_tag" data-adserver="DFP" data-networkid="1976"> -
    -

    CSA

    @@ -574,8 +481,6 @@

    Custom leaderboard

    -
    -

    Custom square

    @@ -588,8 +493,6 @@

    Custom square

    -
    -

    Custom leaderboard with no slot specified

    @@ -601,8 +504,6 @@

    Custom leaderboard with no slot specified

    -
    -

    Cxense Display

    @@ -610,8 +511,6 @@

    Cxense Display

    type="eas" data-eas-domain="stage.emediate.eu" data-eas-cu="3255"> -
    -

    DistroScale

    @@ -620,8 +519,6 @@

    DistroScale

    data-pid="1" data-zid="8710" layout="responsive"> -
    -

    DotAndAds masthead

    @@ -631,8 +528,6 @@

    DotAndAds masthead

    data-mpo="ampTest" data-mpt="amp-amp-all-all" data-sp='sn-u'> -
    -

    DotAndAds 300x250 box

    @@ -642,16 +537,12 @@

    DotAndAds 300x250 box

    data-cid="11" data-mpo="ampTest" data-mpt="amp-amp-all-all"> -
    -

    Doubleclick

    -
    -

    Doubleclick with JSON based parameters

    @@ -659,16 +550,12 @@

    Doubleclick with JSON based parameters

    type="doubleclick" data-slot="/4119129/mobile_ad_banner" json='{"targeting":{"sport":["rugby","cricket"]},"categoryExclusions":["health"],"tagForChildDirectedTreatment":0}'> -
    -

    Doubleclick no ad

    -
    -

    Doubleclick with overriden size

    @@ -677,8 +564,6 @@

    Doubleclick with overriden size

    type="doubleclick" data-slot="/4119129/mobile_ad_banner" class="red"> -
    -

    Doubleclick challenging ad

    @@ -686,8 +571,6 @@

    Doubleclick challenging ad

    type="doubleclick" layout="fixed" data-slot="/35096353/amptesting/badvideoad"> -
    -

    E-Planning 320x50

    @@ -700,8 +583,6 @@

    E-Planning 320x50

    data-epl_sec="AMP_TEST" data-epl_kvs='{"target1":"food", "target2":"cars"}' data-epl_e="AMP_TEST"> -
    -

    Ezoic

    @@ -709,8 +590,6 @@

    Ezoic

    type="ezoic" data-slot="/1254144/28607874" json='{"targeting": {"iid15":"1479509","t":"134","d":"1317","t1":"134","pvc":"0","ap":"1144","sap":"1144","a":"|0|","as":"revenue","plat":"1","bra":"mod1","ic":"1","at":"mbf","adr":"400","reft":"tf","ga":"2497208","rid":"99998","pt":"0","al":"2022","compid":"1","tap":"28607874-1479509","br1":"0","br2":"0"}}'> -
    -

    FlexOneELEPHANT

    @@ -727,8 +606,6 @@

    Felmat

    data-fmt="banner" data-fmk="U12473_n2cJD" data-fmp="0"> -
    -

    Flite

    @@ -736,8 +613,6 @@

    Flite

    type="flite" data-guid="aa7bf589-6d51-4194-91f4-d22eef8e3688" data-mixins=""> -
    -

    Fusion

    @@ -747,8 +622,6 @@

    Fusion

    data-media-zone="adtomatest.apica" data-layout="apicaping" data-space="apicaAd"> -
    -

    Geniee SSP

    @@ -756,24 +629,18 @@

    Geniee SSP

    type="genieessp" data-vid="3" data-zid="1077330"> -
    -

    GMOSSP 320x50 banner

    -
    -

    Holder 300x250 banner

    -
    -

    iBillboard 300x250 banner

    @@ -788,8 +655,6 @@

    I-Mobile 320x50 banner

    data-pid="1847" data-adtype="banner" data-asid="813689"> -
    -

    Industrybrains

    @@ -798,8 +663,6 @@

    Industrybrains

    data-width="300" data-height="250" data-cid="19626-3798936394"> -
    -

    InMobi

    @@ -807,8 +670,6 @@

    InMobi

    type="inmobi" data-siteid="a0078c4ae5a54199a8689d49f3b46d4b" data-slotid="15"> -
    -

    Index Exchange

    @@ -817,8 +678,6 @@

    Index Exchange

    data-version="2" data-ad-units="4" data-casale-i-d="1"> -
    -

    Index Exchange Header Tag

    @@ -826,8 +685,6 @@

    Index Exchange Header Tag

    type="ix" data-ix-id="1" data-slot="/62650033/AMP_Example_Ad_Unit"> -
    -
    @@ -837,48 +694,36 @@

    Kargo

    data-site="_tt9gZ3qxCc2RCg6CADfLAAFR" data-slot="_vypM8bkVCf" data-options='{"targetParams":{"AD_ID":"test-middle","ad_id":"test-middle"}}'> -
    -

    Kiosked

    -
    -

    Kixer

    -
    -

    Ligatus

    -
    -

    LOKA

    -
    -

    MADS

    -
    -

    MANTIS

    @@ -886,8 +731,6 @@

    MANTIS

    type="mantis-display" data-property = "demo" data-zone="medium-rectangle"> -
    -
    MANTIS layout=responsive heights="(min-width:1907px) 56%, (min-width:1100px) 64%, (min-width:780px) 75%, (min-width:480px) 105%, 200%" data-property="demo"> -
    -

    Media Impact

    @@ -907,8 +748,6 @@

    Media Impact

    data-format="4459" data-target="" data-slot="4459"> -
    -

    Media.Net Header Bidder Tag

    @@ -918,18 +757,14 @@

    Media.Net Header Bidder Tag

    data-cid="8CU852274" data-slot="/45361917/AMP_Header_Bidder" json='{"targeting":{"mnetAmpTest":"1","pos":"mnetSlot1"}}'> -
    -

    Media.Net Contextual Monetization Tag

    -
    -
    + type="medianet" + data-tagtype="cm" + data-cid="8CUS8O7EX" + data-crid="112682482">

    Mediavine

    @@ -945,8 +780,6 @@

    Meg

    -
    -

    MicroAd 320x50 banner

    @@ -958,8 +791,6 @@

    MicroAd 320x50 banner

    data-ifa="${COMPASS_EXT_IFA}" data-appid="${COMPASS_EXT_APPID}" data-geo="${COMPASS_EXT_GEO}"> -
    -

    Mixpo

    @@ -967,16 +798,12 @@

    Mixpo

    type = "mixpo" data-guid = "b0caf856-fd92-4adb-aaec-e91948c9ffc8" data-subdomain = "www"> -
    -

    myWidget

    -
    -

    Nativo

    @@ -985,16 +812,12 @@

    Nativo

    layout="responsive" data-premium data-request-url="http://localhost:9876"> -
    -

    Nend

    -
    -

    Nokta

    @@ -1003,8 +826,6 @@

    Nokta

    data-category="izlesene_anasayfa" data-site="izlesene:anasayfa" data-zone="152541"> -
    -

    Open AdStream single ad

    @@ -1014,8 +835,6 @@

    Open AdStream single ad

    data-sitepage="dx_tag_pvt_site" data-pos="x04" data-query="keyword=keyvalue&key2=value2" > -
    -

    OpenX

    @@ -1024,8 +843,6 @@

    OpenX

    data-auid="538289845" data-host="sademo-d.openx.net" data-nc="90577858-BidderTest"> -
    -
    See all OpenX examples @@ -1041,8 +858,6 @@

    Plista responsive widget

    data-geo="de" data-urlprefix="" data-categories="politik"> -
    -

    popIn native ad

    @@ -1051,8 +866,6 @@

    popIn native ad

    layout=responsive heights="(min-width:1907px) 39%, (min-width:1200px) 46%, (min-width:780px) 64%, (min-width:480px) 98%, (min-width:460px) 167%, 196%" data-mediaid="popin_amp"> -
    -

    Pubmine 300x250

    @@ -1062,8 +875,6 @@

    Pubmine 300x250

    data-section="1" data-siteid="37790885" data-wordads="1"> -
    -

    PulsePoint Header Bidding 300x250

    @@ -1074,8 +885,6 @@

    PulsePoint Header Bidding 300x250

    data-tagtype="hb" data-timeout="1000" data-slot="/1066621/ExchangeTech_Prebid_AdUnit"> -
    -

    PulsePoint 300x250

    @@ -1083,8 +892,6 @@

    PulsePoint 300x250

    type="pulsepoint" data-pid="512379" data-tagid="472988"> -
    -

    Purch 300x250

    @@ -1092,8 +899,6 @@

    Purch 300x250

    type="purch" data-pid="2882" data-divid="rightcol_top"> -
    -

    Rambler&Co

    @@ -1103,8 +908,6 @@

    Rambler&Co

    data-begun-auto-pad="434906118" data-begun-block-id="434908944" json='{"params":{"p1":"bvpkq","p2":"y","pct":"a"}}'> -
    -

    Relap

    @@ -1114,8 +917,6 @@

    Relap

    data-token="D3UMgQWBqleq1tPW" data-url="http://bigpicture.ru" data-anchorid="i0xMMY1MoliiZWVl"> -
    -

    Revcontent Responsive Tag

    @@ -1137,8 +938,6 @@

    Revcontent Responsive Tag

    86vw" data-wrapper="rcjsload_2ff711" data-id="203"> -
    -

    Rubicon Project Smart Tag

    @@ -1151,8 +950,6 @@

    Rubicon Project Smart Tag

    data-size="43" data-kw="amp-test, test" json='{"visitor":{"age":"18-24","gender":"male"},"inventory":{"section":"amp"}}'> -
    -

    Rubicon Project FastLane Single Slot

    @@ -1164,8 +961,6 @@

    Rubicon Project FastLane Single Slot

    data-pos="atf" data-kw="amp-test" json='{"targeting":{"kw":"amp-test","age":"18-24","gender":"male","section":"amp"},"visitor":{"age":"18-24","gender":"male"},"inventory":{"section":"amp"}}'> -
    -

    Sharethrough

    @@ -1173,16 +968,12 @@

    Sharethrough

    type="sharethrough" layout="responsive" data-pkey="c0fa8367"> -
    -

    Sklik

    -
    -

    SlimCut Media

    @@ -1190,8 +981,6 @@

    SlimCut Media

    type="slimcutmedia" data-pid="amp-3" data-ffc="SCMPROMO"> -
    -

    SmartAdServer ad

    @@ -1201,8 +990,6 @@

    SmartAdServer ad

    data-page="629154" data-format="38952" data-target="foo=bar"> -
    -

    smartclip

    @@ -1210,8 +997,6 @@

    smartclip

    type="smartclip" data-plc="84555" data-sz="400x320"> -
    -

    Sortable ad

    @@ -1219,8 +1004,6 @@

    Sortable ad

    type="sortable" data-name="medrec" data-site="ampproject.org"> -
    -

    SOVRN

    @@ -1233,8 +1016,6 @@

    SOVRN

    data-aid="affiliateIDgoeshere" data-testFlag="true" data-z="393900"> -
    -

    Swoop

    @@ -1244,8 +1025,6 @@

    Swoop

    data-publisher="SW-11122234-1AMP" data-placement="page/inline" data-slot="amp/test"> -
    -

    Taboola responsive widget

    @@ -1257,8 +1036,6 @@

    Taboola responsive widget

    data-mode="thumbnails-a" data-placement="Ads Example" data-article="auto"> -
    -

    Teads

    @@ -1266,8 +1043,6 @@

    Teads

    type="teads" data-pid="42266" layout="responsive"> -
    -

    TripleLift

    @@ -1275,8 +1050,6 @@

    TripleLift

    type="triplelift" layout="responsive" src="https://ib.3lift.com/dtj/amptest_main_feed/335430"> -
    -

    Weborama

    @@ -1287,8 +1060,6 @@

    Weborama

    data-wbo_fullhost="certification.solution.weborama.fr" data-wbo_random="[RANDOM]" data-wbo_publisherclick="[PUBLISHER_TRACKING_URL]"> -
    -

    Widespace 300x50 Ad

    @@ -1296,8 +1067,6 @@

    Widespace 300x50 Ad

    type="widespace" data-sid="93f1a996-52f5-46b4-8dc8-ccd8886a8fbf" layout="responsive"> -
    -

    Widespace 300x300 no-ad fallback

    @@ -1305,16 +1074,12 @@

    Widespace 300x300 no-ad fallback

    type="widespace" data-sid="911b4848-ef76-4ec1-9713-517d1c91eefb" layout="responsive"> -
    -

    Xlift native ad

    -
    -

    Yahoo Display

    @@ -1323,16 +1088,12 @@

    Yahoo Display

    data-sid="954014446" data-site="news" data-sa='{"LREC":"300x250","secure":"true","content":"no_expandable;"}'> -
    -

    YahooJP YDN

    -
    -

    Yieldbot 300x250

    @@ -1342,8 +1103,6 @@

    Yieldbot 300x250

    data-yb-slot="medrec" data-slot="/2476204/medium-rectangle" json='{"targeting":{"category":["food","lifestyle"]},"categoryExclusions":["health"]}'> -
    -

    YIELD ONE

    @@ -1351,16 +1110,12 @@

    YIELD ONE

    type="yieldone" data-pubid="0001" data-pid="032478_4"> -
    -

    Yieldmo

    -
    -

    ValueCommerce

    @@ -1368,8 +1123,6 @@

    ValueCommerce

    type="valuecommerce" data-sid="3008" data-pid="884466614"> -
    -

    Webediads

    @@ -1380,8 +1133,6 @@

    Webediads

    data-page="amp" data-position="middle" data-query="amptest=1"> -
    -

    ZEDO

    @@ -1393,8 +1144,6 @@

    ZEDO

    data-channel="727" data-publisher="0" data-dim="9"> -
    -

    ZergNet

    @@ -1403,16 +1152,12 @@

    ZergNet

    heights="(max-width:645px) 100%, (max-width:845px) 31%, 23%" layout="responsive" data-zergid="42658"> -
    -

    Zucks

    -
    -
    diff --git a/examples/alp.amp.html b/examples/alp.amp.html index 51c50641c4902..8e962e1837912 100644 --- a/examples/alp.amp.html +++ b/examples/alp.amp.html @@ -8,7 +8,6 @@ - diff --git a/examples/amp-fresh.amp.html b/examples/amp-fresh.amp.html index ebd360766c148..ec995b7565a65 100644 --- a/examples/amp-fresh.amp.html +++ b/examples/amp-fresh.amp.html @@ -12,7 +12,6 @@ -
    diff --git a/examples/article-access-laterpay.amp.html b/examples/article-access-laterpay.amp.html index 7458f8933752b..af33aacc9fcd0 100644 --- a/examples/article-access-laterpay.amp.html +++ b/examples/article-access-laterpay.amp.html @@ -135,7 +135,6 @@ - diff --git a/examples/article-access.amp.html b/examples/article-access.amp.html index c8a318b55c49e..0c4f399cac39d 100644 --- a/examples/article-access.amp.html +++ b/examples/article-access.amp.html @@ -154,7 +154,6 @@ - diff --git a/examples/article-fixed-header.amp.html b/examples/article-fixed-header.amp.html index 594dd5a24475b..f559ec061d706 100644 --- a/examples/article-fixed-header.amp.html +++ b/examples/article-fixed-header.amp.html @@ -239,8 +239,6 @@ - - diff --git a/examples/article.amp.html b/examples/article.amp.html index 2b95490dc09c7..677d828c43dcd 100644 --- a/examples/article.amp.html +++ b/examples/article.amp.html @@ -221,7 +221,6 @@ - diff --git a/examples/bind/basic.amp.html b/examples/bind/basic.amp.html index ff434b340a4e3..4f819b621e97b 100644 --- a/examples/bind/basic.amp.html +++ b/examples/bind/basic.amp.html @@ -34,7 +34,8 @@

    Basic example

    The image above will increase in size and change its src

    - + +
    diff --git a/examples/csp.amp.html b/examples/csp.amp.html index 484e127faf6a4..07bfd49b138e1 100644 --- a/examples/csp.amp.html +++ b/examples/csp.amp.html @@ -2,13 +2,14 @@ - AMP Analytics + AMP CSP - + + @@ -51,6 +52,9 @@

    Should show YT video

    width="240" height="135">

    Should show image

    +

    Should say "Hello World" when button is clicked

    +

    +

    Different font

    diff --git a/examples/everything.amp.html b/examples/everything.amp.html index 585f78af6c4d4..815e3da561a05 100644 --- a/examples/everything.amp.html +++ b/examples/everything.amp.html @@ -130,7 +130,6 @@ - diff --git a/examples/loads-windowcontext-creative.html b/examples/loads-windowcontext-creative.html index 50563fe45eb0a..2ae9a5c2bb100 100644 --- a/examples/loads-windowcontext-creative.html +++ b/examples/loads-windowcontext-creative.html @@ -22,9 +22,14 @@

    A4A + JS Creative

    + src="/examples/ampcontext-creative.html">
    + + diff --git a/examples/share-tracking-with-url.amp.html b/examples/share-tracking-with-url.amp.html index 8c25f14848bda..a6dc1a646c8c8 100644 --- a/examples/share-tracking-with-url.amp.html +++ b/examples/share-tracking-with-url.amp.html @@ -9,7 +9,6 @@ - diff --git a/examples/share-tracking.amp.html b/examples/share-tracking.amp.html index 6cecf6f639425..3474be3955947 100644 --- a/examples/share-tracking.amp.html +++ b/examples/share-tracking.amp.html @@ -9,7 +9,6 @@ - diff --git a/examples/sticky.ads.0.1.amp.html b/examples/sticky.ads.0.1.amp.html index 35fcad9592ca2..8b117c2b81049 100644 --- a/examples/sticky.ads.0.1.amp.html +++ b/examples/sticky.ads.0.1.amp.html @@ -150,7 +150,6 @@ -
    diff --git a/examples/sticky.ads.amp.html b/examples/sticky.ads.amp.html index 7c88bc8ac85e6..17d5e612f67d8 100644 --- a/examples/sticky.ads.amp.html +++ b/examples/sticky.ads.amp.html @@ -150,7 +150,6 @@ -
    diff --git a/examples/user-notification.amp.html b/examples/user-notification.amp.html index 5b9ed75dc9d66..e7d06e07e44f9 100644 --- a/examples/user-notification.amp.html +++ b/examples/user-notification.amp.html @@ -193,7 +193,6 @@ animation: fadeIn ease-in 1s 1 forwards; } - diff --git a/examples/viewer.html b/examples/viewer.html index c1fabbf03a963..0e8c6726463af 100644 --- a/examples/viewer.html +++ b/examples/viewer.html @@ -222,7 +222,7 @@ prerenderSize: this.prerenderSize_, origin: parseUrl(window.location.href).origin, csi: this.csi_, - cap: 'foo,a2a', + cap: 'foo,a2a,swipe', }; log('Params:' + JSON.stringify(params)); @@ -422,54 +422,46 @@ }; Viewer.prototype.processRequest_ = function(type, data, awaitResponse) { - log('Viewer.prototype.processRequest_'); - if (type == 'documentLoaded') { - return this.documentReady_(); + log('Viewer.prototype.processRequest_', type); + switch(type) { + case 'documentLoaded': + return this.documentReady_(); + case 'requestFullOverlay': + return this.requestFullOverlay_(); + case 'cancelFullOverlay': + return this.cancelFullOverlay_(); + case 'pushHistory': + return this.pushHistory_(data.stackIndex); + case 'popHistory': + return this.popHistory_(data.stackIndex); + case 'broadcast': + return this.broadcast_(data); + case 'unloaded': + log('unloaded'); + return this.handleUnload_(); + case 'tick': + log('[CSI] tick. label:', data.label); + return; + case 'a2a': + log('a2a navigation', data); + return; + case 'sendCsi': + log('[CSI] sendCsi.'); + return; + case 'touchstart': + case 'touchmove': + case 'touchend': + log('touch event!', type); + return; + case 'documentHeight': + case 'setFlushParams': + case 'prerenderComplete': + case 'scroll': + case 'replaceHistory': + return; + default: + return Promise.reject('request not supported: ' + type); } - if (type == 'requestFullOverlay') { - return this.requestFullOverlay_(); - } - if (type == 'cancelFullOverlay') { - return this.cancelFullOverlay_(); - } - if (type == 'pushHistory') { - return this.pushHistory_(data.stackIndex); - } - if (type == 'popHistory') { - return this.popHistory_(data.stackIndex); - } - if (type == 'broadcast') { - return this.broadcast_(data); - } - if (type == 'setFlushParams') { - return; - } - if (type == 'prerenderComplete') { - return; - } - if (type == 'tick') { - log('[CSI] tick. label:', data.label); - return; - } - if (type == 'sendCsi') { - log('[CSI] sendCsi.'); - return; - } - if (type == 'scroll') { - return; - } - if (type == 'a2a') { - log('a2a navigation', data); - return; - } - if (type == 'unloaded') { - log('unloaded'); - return this.handleUnload_(); - } - if (type == 'documentHeight') { - return; - } - return Promise.reject('request not supported: ' + type); }; Viewer.prototype.handleUnload_ = function(type, data, awaitResponse) { @@ -477,7 +469,7 @@ if (this.messaging_) { this.messaging_ = null; } - return goog.Promise.resolve(); + return Promise.resolve(); }; Viewer.prototype.sendRequest_ = function(type, data, awaitResponse) { @@ -533,21 +525,21 @@ new Viewer( 'container1', '1', - './everything.amp.max.html', + './everything.amp.max.html?amp_js_v=0.1', true).start(); addShowContainer('1'); new Viewer( 'container2', '2', - './article.amp.max.html', + './article.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('2'); new Viewer( 'container3', '3', - './article-access.amp.max.html', + './article-access.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('3'); @@ -561,7 +553,7 @@ new Viewer( 'container5', '5', - './article-fixed-header.amp.max.html', + './article-fixed-header.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('5'); diff --git a/examples/viewer2.html b/examples/viewer2.html index 0f3a2ebce47da..1a673b0e814fe 100644 --- a/examples/viewer2.html +++ b/examples/viewer2.html @@ -226,7 +226,7 @@ prerenderSize: this.prerenderSize_, origin: parseUrl(window.location.href).origin, csi: this.csi_, - cap: 'foo,a2a', + cap: 'foo,a2a,swipe', webview: 1, // mocking a webview }; log('Params:' + JSON.stringify(params)); @@ -550,35 +550,35 @@ new Viewer( 'container1', '1', - './everything.amp.max.html', + './everything.amp.max.html?amp_js_v=0.1', true).start(); addShowContainer('1'); new Viewer( 'container2', '2', - './article.amp.max.html', + './article.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('2'); new Viewer( 'container3', '3', - './article-access.amp.max.html', + './article-access.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('3'); new Viewer( 'container4', '4', - './alp.amp.max.html', + './alp.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('4'); new Viewer( 'container5', '5', - './article-fixed-header.amp.max.html', + './article-fixed-header.amp.max.html?amp_js_v=0.1', false).start(); addShowContainer('5'); diff --git a/extensions/amp-a4a/0.1/amp-a4a.js b/extensions/amp-a4a/0.1/amp-a4a.js index 1203edbb73fb1..7b214102a41b7 100644 --- a/extensions/amp-a4a/0.1/amp-a4a.js +++ b/extensions/amp-a4a/0.1/amp-a4a.js @@ -21,11 +21,13 @@ import { import {adConfig} from '../../../ads/_config'; import {signingServerURLs} from '../../../ads/_a4a-config'; import { - closestByTag, removeChildren, createElementWithAttributes, } from '../../../src/dom'; -import {cancellation} from '../../../src/error'; +import {cancellation, isCancellation} from '../../../src/error'; +import { + installAnchorClickInterceptor, +} from '../../../src/anchor-click-interceptor'; import { installFriendlyIframeEmbed, setFriendlyIframeEmbedVisible, @@ -35,7 +37,6 @@ import {isAdPositionAllowed} from '../../../src/ad-helper'; import {dev, user} from '../../../src/log'; import {getMode} from '../../../src/mode'; import {isArray, isObject, isEnumValue} from '../../../src/types'; -import {urlReplacementsForDoc} from '../../../src/url-replacements'; import {some} from '../../../src/utils/promise'; import {utf8Decode} from '../../../src/utils/bytes'; import {viewerForDoc} from '../../../src/viewer'; @@ -51,18 +52,15 @@ import { getDefaultBootstrapBaseUrl, generateSentinel, } from '../../../src/3p-frame'; -import {installUrlReplacementsForEmbed,} - from '../../../src/service/url-replacements-impl'; +import { + installUrlReplacementsForEmbed, +} from '../../../src/service/url-replacements-impl'; import {extensionsFor} from '../../../src/extensions'; import {A4AVariableSource} from './a4a-variable-source'; -import {rethrowAsync} from '../../../src/log'; // TODO(tdrl): Temporary. Remove when we migrate to using amp-analytics. import {getTimingDataAsync} from '../../../src/service/variable-source'; import {getContextMetadata} from '../../../src/iframe-attributes'; -/** @private @const {string} */ -const ORIGINAL_HREF_ATTRIBUTE = 'data-a4a-orig-href'; - /** @type {string} */ const METADATA_STRING = ' + + + + + + + + + + + + + + + + + + +
    + +
    + +

    AMP #0

    +Go to iframe +

    + Quisque ultricies id augue a convallis. Vivamus euismod est quis tellus laoreet lacinia. In quam tellus, mollis nec porta eget, volutpat sit amet nibh. Duis ac odio sem. Sed consequat, ante gravida fringilla suscipit, libero libero ullamcorper metus, nec porta est elit at est. Curabitur vel diam ligula. Nulla bibendum malesuada odio. +

    +

    + + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. + +

    +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. +
    +

    + +

    + Top + Image0 + Footer +

    + +

    + + + + + + + + +

    +

    + + + + + + + + +

    +

    + + + +

    + +

    Video

    + + +

    Youtube

    + + + + + +

    Audio

    + + +

    Lightbox

    + +

    + +

    +

    + +

    + +
    + + + +

    Scrollable Lightbox

    + + +

    + +

    + +

    + +

    + +

    + +

    + + + + +
    + + + +

    Images

    +

    + Responsive (w/srcset, w/image-lightbox) +

    + + +

    + +

    + Fixed +

    + +

    + +

    + None +

    + +

    +

    + Lorem ipsum dolor sit amet. +

    + + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +
    +

    Media query selection

    + + +

    +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    Twitter

    + + + + + + + +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    Instagram

    + + + +

    IFrame

    + + +
    + Go back +
    +

    + + + +

    SVG

    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + diff --git a/spec/amp-cache-modifications.everything.cache.html b/spec/amp-cache-modifications.everything.cache.html new file mode 100644 index 0000000000000..88c72ebcdffe8 --- /dev/null +++ b/spec/amp-cache-modifications.everything.cache.html @@ -0,0 +1,357 @@ +AMP #0 + +
    + +
    + +

    AMP #0

    +Go to iframe +

    + Quisque ultricies id augue a convallis. Vivamus euismod est quis tellus laoreet lacinia. In quam tellus, mollis nec porta eget, volutpat sit amet nibh. Duis ac odio sem. Sed consequat, ante gravida fringilla suscipit, libero libero ullamcorper metus, nec porta est elit at est. Curabitur vel diam ligula. Nulla bibendum malesuada odio. +

    +

    + + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. + +

    +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. +
    +

    + +

    + Top + Image0 + Footer +

    + +

    + + + + + + + + +

    +

    + + + + + + + + +

    +

    + + + +

    + +

    Video

    + + +

    Youtube

    + + + + + +

    Audio

    + + +

    Lightbox

    + +

    + +

    +

    + +

    + +
    + + + +

    Scrollable Lightbox

    + + +

    + +

    + +

    + +

    + +

    + +

    + + + + +
    + + + +

    Images

    +

    + Responsive (w/srcset, w/image-lightbox) +

    + + +

    + +

    + Fixed +

    + +

    + +

    + None +

    + +

    +

    + Lorem ipsum dolor sit amet. +

    + + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +
    +

    Media query selection

    + + + +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    Twitter

    + + + + + + + +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    Instagram

    + + + +

    IFrame

    + + +
    + Go back +
    +

    + + + +

    SVG

    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + diff --git a/spec/amp-cache-modifications.md b/spec/amp-cache-modifications.md index 4afd6a6df7a9f..355a4ff597be4 100644 --- a/spec/amp-cache-modifications.md +++ b/spec/amp-cache-modifications.md @@ -2,6 +2,8 @@ These are guidelines for what AMP cache implementations should look like. Some items are required for overall security of the platform while others are suggestions for performance improvements. All modifications are made to both AMP and AMP4ADS documents except where noted. +For example, given a [recent version](https://github.com/ampproject/amphtml/tree/master/spec/amp-cache-modifications.everything.amp.html) of [everything.amp.html](https://github.com/ampproject/amphtml/blob/master/examples/everything.amp.html), the output after modifications will be [this version](https://github.com/ampproject/amphtml/tree/master/spec/amp-cache-modifications.everything.cache.html). + ### HTML Sanitization The AMP Cache parses and re-serializes all documents to remove any ambiguities in parsing the document which might result in subtly different parses in different browsers. diff --git a/spec/amp-managing-user-state.md b/spec/amp-managing-user-state.md index d543d8c3b2e08..0197c42b98f29 100644 --- a/spec/amp-managing-user-state.md +++ b/spec/amp-managing-user-state.md @@ -1,3 +1,19 @@ + + # Managing non-authenticated user state with AMP **Table of contents** diff --git a/src/ad-helper.js b/src/ad-helper.js index bfb6c3e296f32..78e834822f6e3 100644 --- a/src/ad-helper.js +++ b/src/ad-helper.js @@ -15,6 +15,7 @@ */ import {dev} from './log'; +import {computedStyle} from './style'; /** * Tags that are allowed to have fixed positioning @@ -34,7 +35,7 @@ const CONTAINERS = { * @return {boolean} */ function isPositionFixed(el, win) { - return win./*OK*/getComputedStyle(el).position == 'fixed'; + return computedStyle(win, el).position == 'fixed'; } /** diff --git a/src/anchor-click-interceptor.js b/src/anchor-click-interceptor.js new file mode 100644 index 0000000000000..156bc9312fbb9 --- /dev/null +++ b/src/anchor-click-interceptor.js @@ -0,0 +1,83 @@ +/** + * Copyright 2017 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 { + closestByTag, +} from './dom'; +import {dev} from './log'; +import {urlReplacementsForDoc} from './url-replacements'; + +/** @private @const {string} */ +const ORIG_HREF_ATTRIBUTE = 'data-a4a-orig-href'; + +/** + * Registers a handler that performs URL replacement on the href + * of an ad click. + * @param {!./service/ampdoc-impl.AmpDoc} ampdoc + * @param {!Window} win + */ +export function installAnchorClickInterceptor(ampdoc, win) { + win.document.documentElement.addEventListener('click', + maybeExpandUrlParams.bind(null, ampdoc), /* capture */ true); +} + +/** + * Handle click on links and replace variables in the click URL. + * The function changes the actual href value and stores the + * template in the ORIGINAL_HREF_ATTRIBUTE attribute + * @param {!./service/ampdoc-impl.AmpDoc} ampdoc + * @param {!Event} e + */ +function maybeExpandUrlParams(ampdoc, e) { + const target = closestByTag(dev().assertElement(e.target), 'A'); + if (!target || !target.href) { + // Not a click on a link. + return; + } + const hrefToExpand = + target.getAttribute(ORIG_HREF_ATTRIBUTE) || target.getAttribute('href'); + if (!hrefToExpand) { + return; + } + const vars = { + 'CLICK_X': () => { + return e.pageX; + }, + 'CLICK_Y': () => { + return e.pageY; + }, + }; + const newHref = urlReplacementsForDoc(ampdoc).expandSync( + hrefToExpand, vars, undefined, /* opt_whitelist */ { + // For now we only allow to replace the click location vars + // and nothing else. + // NOTE: Addition to this whitelist requires additional review. + 'CLICK_X': true, + 'CLICK_Y': true, + }); + if (newHref != hrefToExpand) { + // Store original value so that later clicks can be processed with + // freshest values. + if (!target.getAttribute(ORIG_HREF_ATTRIBUTE)) { + target.setAttribute(ORIG_HREF_ATTRIBUTE, hrefToExpand); + } + target.setAttribute('href', newHref); + } +} + +export function maybeExpandUrlParamsForTesting(ampdoc, e) { + maybeExpandUrlParams(ampdoc, e); +} diff --git a/src/custom-element.js b/src/custom-element.js index a217c20eaff3b..b6957f9adff36 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -772,7 +772,9 @@ function createBaseCustomElementClass(win) { if (this.isUpgraded()) { this.implementation_.layoutWidth_ = this.layoutWidth_; } - this.implementation_.onLayoutMeasure(); + if (this.isBuilt()) { + this.implementation_.onLayoutMeasure(); + } if (this.isLoadingEnabled_()) { if (this.isInViewport_) { diff --git a/src/dom.js b/src/dom.js index a7ea457a2f4ea..bfd29b891545f 100644 --- a/src/dom.js +++ b/src/dom.js @@ -638,4 +638,3 @@ export function tryFocus(element) { export function isIframed(win) { return win.parent && win.parent != win; } - diff --git a/src/error.js b/src/error.js index 7e22a4e527240..c15f0e6952668 100644 --- a/src/error.js +++ b/src/error.js @@ -24,10 +24,11 @@ import { USER_ERROR_SENTINEL, isUserErrorMessage, } from './log'; -import {makeBodyVisible} from './style-installer'; -import {urls} from './config'; import {isProxyOrigin} from './url'; import {isCanary} from './experiments'; +import {makeBodyVisible} from './style-installer'; +import {startsWith} from './string'; +import {urls} from './config'; /** @@ -63,6 +64,13 @@ let reportingBackoff = function(work) { return reportingBackoff(work); }; +/** + * The true JS engine, as detected by inspecting an Error stack. This should be + * used with the userAgent to tell definitely. I.e., Chrome on iOS is really a + * Safari JS engine. + */ +let detectedJsEngine; + /** * Reports an error. If the error has an "associatedElement" property * the element is marked with the `i-amphtml-element-error` and displays @@ -90,7 +98,7 @@ export function reportError(error, opt_associatedElement) { error = new Error('Unknown error'); } // Report if error is not an expected type. - if (!isValidError && getMode().localDev) { + if (!isValidError && getMode().localDev && !getMode().test) { setTimeout(function() { const rethrow = new Error( '_reported_ Error reported incorrectly: ' + error); @@ -152,6 +160,23 @@ export function cancellation() { return new Error(CANCELLED); } +/** + * @param {*} errorOrMessage + * @return {boolean} + */ +export function isCancellation(errorOrMessage) { + if (!errorOrMessage) { + return false; + } + if (typeof errorOrMessage == 'string') { + return startsWith(errorOrMessage, CANCELLED); + } + if (typeof errorOrMessage.message == 'string') { + return startsWith(errorOrMessage.message, CANCELLED); + } + return false; +} + /** * Install handling of global unhandled exceptions. * @param {!Window} win @@ -260,6 +285,8 @@ export function getErrorReportUrl(message, filename, line, col, error, } } + const isUserError = isUserErrorMessage(message); + // This is the App Engine app in // ../tools/errortracker // It stores error reports via https://cloud.google.com/error-reporting/ @@ -268,7 +295,7 @@ export function getErrorReportUrl(message, filename, line, col, error, '?v=' + encodeURIComponent('$internalRuntimeVersion$') + '&noAmp=' + (hasNonAmpJs ? 1 : 0) + '&m=' + encodeURIComponent(message.replace(USER_ERROR_SENTINEL, '')) + - '&a=' + (isUserErrorMessage(message) ? 1 : 0); + '&a=' + (isUserError ? 1 : 0); if (expected) { // Errors are tagged with "ex" ("expected") label to allow loggers to // classify these errors as benchmarks and not exceptions. @@ -309,12 +336,24 @@ export function getErrorReportUrl(message, filename, line, col, error, } } + if (!detectedJsEngine) { + detectedJsEngine = detectJsEngineFromStack(); + } + url += `&jse=${detectJsEngineFromStack}`; + if (error) { const tagName = error && error.associatedElement - ? error.associatedElement.tagName - : 'u'; // Unknown - url += '&el=' + encodeURIComponent(tagName) + - '&s=' + encodeURIComponent(error.stack || ''); + ? error.associatedElement.tagName + : 'u'; // Unknown + url += `&el=${encodeURIComponent(tagName)}`; + if (error.args) { + url += `&args=${encodeURIComponent(JSON.stringify(error.args))}`; + } + + if (!isUserError) { + url += `&s=${encodeURIComponent(error.stack || '')}`; + } + error.message += ' _reported_'; } else { url += '&f=' + encodeURIComponent(filename || '') + @@ -334,6 +373,7 @@ export function getErrorReportUrl(message, filename, line, col, error, * Returns true if it appears like there is non-AMP JS on the * current page. * @param {!Window} win + * @return {boolean} * @visibleForTesting */ export function detectNonAmpJs(win) { @@ -349,3 +389,51 @@ export function detectNonAmpJs(win) { export function resetAccumulatedErrorMessagesForTesting() { accumulatedErrorMessages = []; } + +/** + * Does a series of checks on the stack of an thrown error to determine the + * JS engine that is currently running. This gives a bit more information than + * just the UserAgent, since browsers often allow overriding it to "emulate" + * mobile. + * @return {string} + * @visibleForTesting + */ +export function detectJsEngineFromStack() { + const object = Object.create({ + // DO NOT rename this property. + // DO NOT transform into shorthand method syntax. + t: function() { + throw new Error('message'); + }, + }); + try { + object.t(); + } catch (e) { + const stack = e.stack; + // Firefox uses a "<." to show prototype method. + if (stack.indexOf('<.t@') > -1) { + return 'Firefox'; + } + + // Safari does not show the context ("object."), just the function name. + if (stack.indexOf('t@') === 0) { + return 'Safari'; + } + + // IE looks like Chrome, but includes a context for the base stack line. + // Explicitly, we're looking for something like: + // " at Global code https://example.com/app.js:1:200" or + // " at Anonymous function https://example.com/app.js:1:200" + const last = stack.split('\n').pop(); + if (/\bat \w+ /i.test(last)) { + return 'IE'; + } + + // Finally, chrome includes the error message in the stack. + if (stack.indexOf('message') > -1) { + return 'Chrome'; + } + } + + return 'unknown'; +} diff --git a/src/inabox/amp-inabox.js b/src/inabox/amp-inabox.js index e70c543d53e02..7c29be921e01b 100644 --- a/src/inabox/amp-inabox.js +++ b/src/inabox/amp-inabox.js @@ -39,9 +39,9 @@ import { import {cssText} from '../../build/css'; import {maybeValidate} from '../validator-integration'; import {maybeTrackImpression} from '../impression'; -import {isExperimentOn} from '../experiments'; import {installViewerServiceForDoc} from '../service/viewer-impl'; import {installInaboxViewportService} from './inabox-viewport'; +import {installAnchorClickInterceptor} from '../anchor-click-interceptor'; import {getMode} from '../mode'; getMode(self).runtime = 'inabox'; @@ -77,12 +77,10 @@ startupChunk(self.document, function initial() { installRuntimeServices(self); fontStylesheetTimeout(self); - if (isExperimentOn(self, 'amp-inabox')) { - // Install inabox specific Viewport service before - // runtime tries to install the normal one. - installViewerServiceForDoc(ampdoc); - installInaboxViewportService(ampdoc); - } + // Install inabox specific Viewport service before + // runtime tries to install the normal one. + installViewerServiceForDoc(ampdoc); + installInaboxViewportService(ampdoc); installAmpdocServices(ampdoc); // We need the core services (viewer/resources) to start instrumenting @@ -102,6 +100,7 @@ startupChunk(self.document, function initial() { startupChunk(self.document, function final() { installPullToRefreshBlocker(self); installGlobalClickListenerForDoc(ampdoc); + installAnchorClickInterceptor(ampdoc, self); maybeValidate(self); makeBodyVisible(self.document, /* waitForServices */ true); diff --git a/src/inabox/inabox-viewport.js b/src/inabox/inabox-viewport.js index 401c73627ee0b..7249dde518f12 100644 --- a/src/inabox/inabox-viewport.js +++ b/src/inabox/inabox-viewport.js @@ -173,6 +173,8 @@ export class ViewportBindingInabox { /** @override */ updatePaddingTop() {/* no-op */} /** @override */ hideViewerHeader() {/* no-op */} /** @override */ showViewerHeader() {/* no-op */} + /** @override */ disableScroll() {/* no-op */} + /** @override */ resetScroll() {/* no-op */} /** @override */ ensureReadyForElements() {/* no-op */} /** @override */ updateLightboxMode() {/* no-op */} /** @override */ setScrollTop() {/* no-op */} diff --git a/src/layout.js b/src/layout.js index a40e9c03b4727..39a23be7550b3 100644 --- a/src/layout.js +++ b/src/layout.js @@ -22,10 +22,6 @@ import {dev, user} from './log'; import {isFiniteNumber} from './types'; import {setStyles} from './style'; -import {isExperimentOn} from './experiments'; - -/** @const {string} */ -export const UX_EXPERIMENT = 'amp-ad-loading-ux'; /** * @enum {string} @@ -278,10 +274,7 @@ export function getNaturalDimensions(element) { export function isLoadingAllowed(element) { const tagName = element.tagName.toUpperCase(); if (tagName == 'AMP-AD' || tagName == 'AMP-EMBED') { - const win = element.ownerDocument.defaultView; - if (isExperimentOn(win, UX_EXPERIMENT)) { - return true; - } + return true; } return LOADING_ELEMENTS_[tagName] || false; } diff --git a/src/log.js b/src/log.js index 1eebc53f898d9..89785734963a9 100644 --- a/src/log.js +++ b/src/log.js @@ -387,6 +387,8 @@ export class Log { } else if (error.message.indexOf(this.suffix_) == -1) { error.message += this.suffix_; } + } else if (isUserErrorMessage(error.message)) { + error.message = error.message.replace(USER_ERROR_SENTINEL, ''); } } } diff --git a/src/performance.js b/src/performance.js index abe97203b3a08..3e5d01a9f32c1 100644 --- a/src/performance.js +++ b/src/performance.js @@ -14,7 +14,10 @@ * limitations under the License. */ -import {getExistingServiceForWindow} from './service'; +import { + getExistingServiceForWindow, + getExistingServiceForWindowOrNull, +} from './service'; /** * @param {!Window} window @@ -23,4 +26,13 @@ import {getExistingServiceForWindow} from './service'; export function performanceFor(window) { return /** @type {!./service/performance-impl.Performance}*/ ( getExistingServiceForWindow(window, 'performance')); -}; +} + +/** + * @param {!Window} window + * @return {!./service/performance-impl.Performance} + */ +export function performanceForOrNull(window) { + return /** @type {!./service/performance-impl.Performance}*/ ( + getExistingServiceForWindowOrNull(window, 'performance')); +} diff --git a/src/service-worker/error-reporting.js b/src/service-worker/error-reporting.js deleted file mode 100644 index 8412fee9ddc32..0000000000000 --- a/src/service-worker/error-reporting.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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. - */ - -// Simplified error reporting for errors in service workers. - -import {urls} from '../config'; -import {exponentialBackoff} from '../exponential-backoff'; - -/** - * Exponential backoff for error reports to avoid any given - * ServiceWorker from generating a very large number of errors. - * @const {function(function()): number} - */ -const backoff = exponentialBackoff(1.5); - -self.addEventListener('unhandledrejection', event => { - backoff(() => report(event.reason)); -}); - -self.addEventListener('error', event => { - backoff(() => report(event.error)); -}); - -/** - * Report error to AMP's error reporting frontend. - * - * @param {*} e - */ -function report(e) { - if (urls.localhostRegex.test(self.location.origin)) { - return; - } - if (!(e instanceof Error)) { - e = new Error(e); - } - const config = self.AMP_CONFIG || {}; - const url = urls.errorReporting + - // sw=1 tags request as coming from the service worker. - '?sw=1&v=' + encodeURIComponent(config.v) + - '&m=' + encodeURIComponent(e.message) + - '&ca=' + (config.canary ? 1 : 0) + - '&s=' + encodeURIComponent(e.stack || ''); - fetch(url, /** @type {!RequestInit} */ ({ - // We don't care about the response. - mode: 'no-cors', - })).catch(reason => { - console./*OK*/error(reason); - }); -} diff --git a/src/service-worker/shell.js b/src/service-worker/shell.js index d90460ca8e684..193366b2a45d3 100644 --- a/src/service-worker/shell.js +++ b/src/service-worker/shell.js @@ -16,7 +16,9 @@ import {getMode} from '../mode'; import {calculateExtensionScriptUrl} from '../service/extension-location'; -import './error-reporting'; +import {installWorkerErrorReporting} from '../worker-error-reporting'; + +installWorkerErrorReporting('sw'); /** * Import the "core" entry point for the AMP CDN Service Worker. This shell diff --git a/src/service.js b/src/service.js index eea8f8f3a1e1e..ae1f7d92071e2 100644 --- a/src/service.js +++ b/src/service.js @@ -72,12 +72,21 @@ export class EmbeddableService { * @return {!Object} The service. */ export function getExistingServiceForWindow(win, id) { - win = getTopWindow(win); - const exists = win.services && win.services[id] && win.services[id].obj; - return dev().assert(exists, `${id} service not found. Make sure it is ` + - `installed.`); + const exists = getExistingServiceForWindowOrNull(win, id); + return dev().assert(/** @type {!Object} */ (exists), + `${id} service not found. Make sure it is installed.`); } +/** + * Returns a service or null with the given id. + * @param {!Window} win + * @param {string} id + * @return {?Object} The service. + */ +export function getExistingServiceForWindowOrNull(win, id) { + win = getTopWindow(win); + return win.services && win.services[id] && win.services[id].obj; +} /** * Returns a service with the given id. Assumes that it has been constructed diff --git a/src/service/action-impl.js b/src/service/action-impl.js index df3c156a468e1..ae13f399b5b37 100644 --- a/src/service/action-impl.js +++ b/src/service/action-impl.js @@ -22,6 +22,12 @@ import {map} from '../utils/object'; import {timerFor} from '../timer'; import {vsyncFor} from '../vsync'; +/** + * ActionInfoDef args key that maps to the an unparsed object literal string. + * @const {string} + */ +export const OBJECT_STRING_ARGS_KEY = '__AMP_OBJECT_STRING__'; + /** @const {string} */ const TAG_ = 'Action'; @@ -428,6 +434,7 @@ export function parseActionMap(s, context) { // Method: ".method". Method is optional. let method = DEFAULT_METHOD_; let args = null; + peek = toks.peek(); if (peek.type == TokenType.SEPARATOR && peek.value == '.') { toks.next(); // Skip '.' @@ -437,48 +444,8 @@ export function parseActionMap(s, context) { // Optionally, there may be arguments: "(key = value, key = value)". peek = toks.peek(); if (peek.type == TokenType.SEPARATOR && peek.value == '(') { - toks.next(); // Skip '('. - do { - tok = toks.next(); - - // Format: key = value, .... - if (tok.type == TokenType.SEPARATOR && - (tok.value == ',' || tok.value == ')')) { - // Expected: ignore. - } else if (tok.type == TokenType.LITERAL || - tok.type == TokenType.ID) { - // Key: "key = " - const argKey = tok.value; - assertToken(toks.next(), [TokenType.SEPARATOR], '='); - // Value is either a literal or a variable: "foo.bar.baz" - tok = assertToken(toks.next(/* convertValue */ true), - [TokenType.LITERAL, TokenType.ID]); - const argValueTokens = [tok]; - // Variables have one or more dereferences: ".identifier" - if (tok.type == TokenType.ID) { - for (peek = toks.peek(); - peek.type == TokenType.SEPARATOR && peek.value == '.'; - peek = toks.peek()) { - tok = toks.next(); // Skip '.'. - tok = assertToken(toks.next(false), [TokenType.ID]); - argValueTokens.push(tok); - } - } - const argValue = getActionInfoArgValue(argValueTokens); - if (!args) { - args = map(); - } - args[argKey] = argValue; - peek = toks.peek(); - assertAction( - peek.type == TokenType.SEPARATOR && - (peek.value == ',' || peek.value == ')'), - 'Expected either [,] or [)]'); - } else { - // Unexpected token. - assertAction(false, `; unexpected token [${tok.value || ''}]`); - } - } while (!(tok.type == TokenType.SEPARATOR && tok.value == ')')); + toks.next(); // Skip '(' + args = tokenizeMethodArguments(toks, assertToken, assertAction); } } @@ -511,6 +478,70 @@ export function parseActionMap(s, context) { return actionMap; } +/** + * Tokenizes and returns method arguments, e.g. target.method(arguments). + * @param {!ParserTokenizer} toks + * @param {!Function} assertToken + * @param {!Function} assertAction + * @return {ActionInfoArgsDef} + * @private + */ +function tokenizeMethodArguments(toks, assertToken, assertAction) { + let peek = toks.peek(); + let tok; + let args = null; + // Object literal. Format: {...} + if (peek.type == TokenType.OBJECT) { + // Don't parse object literals. Tokenize as a single expression + // fragment and delegate to specific action handler. + args = map(); + const value = toks.next().value; + args[OBJECT_STRING_ARGS_KEY] = () => value; + assertToken(toks.next(), [TokenType.SEPARATOR], ')'); + } else { + // Key-value pairs. Format: key = value, .... + do { + tok = toks.next(); + const type = tok.type; + const value = tok.value; + if (type == TokenType.SEPARATOR && (value == ',' || value == ')')) { + // Expected: ignore. + } else if (type == TokenType.LITERAL || type == TokenType.ID) { + // Key: "key = " + assertToken(toks.next(), [TokenType.SEPARATOR], '='); + // Value is either a literal or a variable: "foo.bar.baz" + tok = assertToken(toks.next(/* convertValue */ true), + [TokenType.LITERAL, TokenType.ID]); + const argValueTokens = [tok]; + // Variables have one or more dereferences: ".identifier" + if (tok.type == TokenType.ID) { + for (peek = toks.peek(); + peek.type == TokenType.SEPARATOR && peek.value == '.'; + peek = toks.peek()) { + tok = toks.next(); // Skip '.'. + tok = assertToken(toks.next(false), [TokenType.ID]); + argValueTokens.push(tok); + } + } + const argValue = getActionInfoArgValue(argValueTokens); + if (!args) { + args = map(); + } + args[value] = argValue; + peek = toks.peek(); + assertAction( + peek.type == TokenType.SEPARATOR && + (peek.value == ',' || peek.value == ')'), + 'Expected either [,] or [)]'); + } else { + // Unexpected token. + assertAction(false, `; unexpected token [${tok.value || ''}]`); + } + } while (!(tok.type == TokenType.SEPARATOR && tok.value == ')')); + } + return args; +} + /** * Returns a function that generates a method argument value for a given token. * The function takes a single object argument `data`. @@ -615,6 +646,7 @@ const TokenType = { SEPARATOR: 2, LITERAL: 3, ID: 4, + OBJECT: 5, }; /** @@ -632,7 +664,11 @@ const SEPARATOR_SET = ';:.()=,|!'; const STRING_SET = '"\''; /** @private @const {string} */ -const SPECIAL_SET = WHITESPACE_SET + SEPARATOR_SET + STRING_SET; +const OBJECT_SET = '{}'; + +/** @private @const {string} */ +const SPECIAL_SET = + WHITESPACE_SET + SEPARATOR_SET + STRING_SET + OBJECT_SET; /** @private */ class ParserTokenizer { @@ -737,6 +773,30 @@ class ParserTokenizer { return {type: TokenType.LITERAL, value, index: newIndex}; } + // Object literal. + if (c == '{') { + let numberOfBraces = 1; + let end = -1; + for (let i = newIndex + 1; i < this.str_.length; i++) { + const char = this.str_[i]; + if (char == '{') { + numberOfBraces++; + } else if (char == '}') { + numberOfBraces--; + } + if (numberOfBraces <= 0) { + end = i; + break; + } + } + if (end == -1) { + return {type: TokenType.INVALID, index: newIndex}; + } + const value = this.str_.substring(newIndex, end + 1); + newIndex = end; + return {type: TokenType.OBJECT, value, index: newIndex}; + } + // Advance until next special character. let end = newIndex + 1; for (; end < this.str_.length; end++) { diff --git a/src/service/fixed-layer.js b/src/service/fixed-layer.js index 2576b5266dc11..7c4b8684e5910 100644 --- a/src/service/fixed-layer.js +++ b/src/service/fixed-layer.js @@ -15,24 +15,26 @@ */ import {dev, user} from '../log'; +import {endsWith} from '../string'; import {platformFor} from '../platform'; -import {getStyle, setStyle, setStyles} from '../style'; +import {getStyle, setStyle, setStyles, computedStyle} from '../style'; const TAG = 'FixedLayer'; const DECLARED_FIXED_PROP = '__AMP_DECLFIXED'; +const DECLARED_STICKY_PROP = '__AMP_DECLSTICKY'; /** * The fixed layer is a *sibling* of the body element. I.e. it's a direct - * child of documentElement. It's used to manage the `postition:fixed` - * elements in iOS-iframe case due to the + * child of documentElement. It's used to manage the `postition:fixed` and + * `position:sticky` elements in iOS-iframe case due to the * https://bugs.webkit.org/show_bug.cgi?id=154399 bug, which is itself * a result of workaround for the issue where scrolling is not supported * in iframes (https://bugs.webkit.org/show_bug.cgi?id=149264). - * This implementation finds all elements that could be `fixed` and checks - * on major relayouts if they are indeed `fixed`. All `fixed` elements are - * moved into the fixed layer. + * This implementation finds all elements that could be `fixed` or `sticky` + * and checks on major relayouts if they are indeed `fixed`/`sticky`. + * Some `fixed` elements may be moved into the "transfer layer". */ export class FixedLayer { /** @@ -58,22 +60,22 @@ export class FixedLayer { this.transfer_ = transfer && ampdoc.isSingleDoc(); /** @private {?Element} */ - this.fixedLayer_ = null; + this.transferLayer_ = null; /** @private {number} */ this.counter_ = 0; - /** @const @private {!Array} */ - this.fixedElements_ = []; + /** @const @private {!Array} */ + this.elements_ = []; } /** * @param {boolean} visible */ setVisible(visible) { - if (this.fixedLayer_) { + if (this.transferLayer_) { this.vsync_.mutate(() => { - setStyle(this.fixedLayer_, 'visibility', + setStyle(this.transferLayer_, 'visibility', visible ? 'visible' : 'hidden'); }); } @@ -88,8 +90,9 @@ export class FixedLayer { return; } - // Find all `position:fixed` elements. + // Find all `position:fixed` and `sticky` elements. const fixedSelectors = []; + const stickySelectors = []; for (let i = 0; i < stylesheets.length; i++) { const stylesheet = stylesheets[i]; if (stylesheet.disabled || @@ -100,20 +103,20 @@ export class FixedLayer { stylesheet.ownerNode.hasAttribute('amp-extension')) { continue; } - this.discoverFixedSelectors_(stylesheet.cssRules, fixedSelectors); + this.discoverSelectors_( + stylesheet.cssRules, fixedSelectors, stickySelectors); } - this.trySetupFixedSelectorsNoInline(fixedSelectors); + this.trySetupSelectorsNoInline(fixedSelectors, stickySelectors); // Sort in document order. this.sortInDomOrder_(); const platform = platformFor(this.ampdoc.win); - if (this.fixedElements_.length > 0 && !this.transfer_ && - platform.isIos()) { + if (this.elements_.length > 0 && !this.transfer_ && platform.isIos()) { user().warn(TAG, 'Please test this page inside of an AMP Viewer such' + - ' as Google\'s because the fixed positioning might have slightly' + - ' different layout.'); + ' as Google\'s because the fixed or sticky positioning might have' + + ' slightly different layout.'); } this.update(); @@ -124,8 +127,8 @@ export class FixedLayer { * all elements. The padding update can be transient, in which case the * UI itself is not updated leaving the blank space up top, which is invisible * due to scroll position. This mode saves significant resources. However, - * eventhough layout is not updated, the fixed coordinates still need to be - * recalculated. + * eventhough layout is not updated, the fixed/sticky coordinates still need + * to be recalculated. * @param {number} paddingTop * @param {boolean} opt_transient */ @@ -143,9 +146,12 @@ export class FixedLayer { * @param {?string} transform */ transformMutate(transform) { + // Unfortunately, we can't do anything with sticky elements here. Updating + // `top` in animation frames causes reflow on all platforms and we can't + // determine whether an element is currently docked to apply transform. if (transform) { // Apply transform style to all fixed elements - this.fixedElements_.forEach(e => { + this.elements_.forEach(e => { if (e.fixedNow && e.top) { setStyle(e.element, 'transition', 'none'); if (e.transform && e.transform != 'none') { @@ -157,7 +163,7 @@ export class FixedLayer { }); } else { // Reset transform style to all fixed elements - this.fixedElements_.forEach(e => { + this.elements_.forEach(e => { if (e.fixedNow && e.top) { setStyles(e.element, { transform: '', @@ -169,26 +175,35 @@ export class FixedLayer { } /** - * Adds the element directly into the fixed layer, bypassing discovery. + * Adds the element directly into the fixed/sticky layer, bypassing discovery. * @param {!Element} element * @param {boolean=} opt_forceTransfer If set to true , then the element needs - * to be forcefully transferred to the fixed layer. + * to be forcefully transferred to the transfer layer. */ addElement(element, opt_forceTransfer) { - this.setupFixedElement_(element, /* selector */ '*', opt_forceTransfer); + this.setupElement_( + element, + /* selector */ '*', + /* position */ 'fixed', + opt_forceTransfer); this.sortInDomOrder_(); this.update(); } /** - * Removes the element from the fixed layer. + * Removes the element from the fixed/sticky layer. * @param {!Element} element */ removeElement(element) { - const fe = this.removeFixedElement_(element); - if (fe && this.fixedLayer_) { + const removed = this.removeElement_(element); + if (removed.length > 0 && this.transferLayer_) { this.vsync_.mutate(() => { - this.returnFromFixedLayer_(/** @type {FixedElementDef} */ (fe)); + for (let i = 0; i < removed.length; i++) { + const fe = removed[i]; + if (fe.position == 'fixed') { + this.returnFromTransferLayer_(fe); + } + } }); } } @@ -204,26 +219,36 @@ export class FixedLayer { } /** - * Performs fixed actions. + * Whether the element is declared as sticky in any of the user's stylesheets. + * Will include any matches, not necessarily currently sticky elements. + * @param {!Element} element + * @return {boolean} + */ + isDeclaredSticky(element) { + return !!element[DECLARED_STICKY_PROP]; + } + + /** + * Performs fixed/sticky actions. * 1. Updates `top` styling if necessary. * 2. On iOS/Iframe moves elements between fixed layer and BODY depending on * whether they are currently visible and fixed * @return {!Promise} */ update() { - if (this.fixedElements_.length == 0) { + if (this.elements_.length == 0) { return Promise.resolve(); } // Some of the elements may no longer be in DOM. - /** @type {!Array} */ - const toRemove = this.fixedElements_.filter( + /** @type {!Array} */ + const toRemove = this.elements_.filter( fe => !this.ampdoc.contains(fe.element)); - toRemove.forEach(fe => this.removeFixedElement_(fe.element)); + toRemove.forEach(fe => this.removeElement_(fe.element)); // Next, the positioning-related properties will be measured. If a - // potentially fixed element turns out to be actually fixed, it will - // be decorated and possibly move to a separate layer. + // potentially fixed/sticky element turns out to be actually fixed/sticky, + // it will be decorated and possibly move to a separate layer. let hasTransferables = false; return this.vsync_.runPromise({ measure: state => { @@ -236,37 +261,23 @@ export class FixedLayer { // `style.top = auto`. // 1. Set all style top to `auto` and calculate the auto-offset. - this.fixedElements_.forEach(fe => { + this.elements_.forEach(fe => { setStyle(fe.element, 'top', 'auto'); }); - this.fixedElements_.forEach(fe => { + this.elements_.forEach(fe => { autoTopMap[fe.id] = fe.element./*OK*/offsetTop; }); // 2. Reset style top. - this.fixedElements_.forEach(fe => { + this.elements_.forEach(fe => { setStyle(fe.element, 'top', ''); }); - // 3. Calculated fixed info. - this.fixedElements_.forEach(fe => { + // 3. Calculated fixed/sticky info. + this.elements_.forEach(fe => { const element = fe.element; - const styles = this.ampdoc.win./*OK*/getComputedStyle( - element, null); - if (!styles) { - // Notice that `styles` can be `null`, courtesy of long-standing - // Gecko bug: https://bugzilla.mozilla.org/show_bug.cgi?id=548397 - // See #3096 for more details. - state[fe.id] = { - fixed: false, - transferrable: false, - top: '', - zIndex: '', - }; - return; - } - - const position = styles.getPropertyValue('position'); + const styles = computedStyle(this.ampdoc.win, element); + const position = styles.position || ''; // Element is indeed fixed. Visibility is added to the test to // avoid moving around invisible elements. const isFixed = ( @@ -277,9 +288,12 @@ export class FixedLayer { ) ) ); - if (!isFixed) { + // Element is indeed sticky. + const isSticky = endsWith(position, 'sticky'); + if (!isFixed && !isSticky) { state[fe.id] = { fixed: false, + sticky: false, transferrable: false, top: '', zIndex: '', @@ -292,7 +306,7 @@ export class FixedLayer { // actual calculated value in all other browsers. To find out whether // or not the `top` was actually set in CSS, this method compares // `offsetTop` with `style.top = 'auto'` and without. - let top = styles.getPropertyValue('top'); + let top = styles.top; const currentOffsetTop = element./*OK*/offsetTop; const isImplicitAuto = currentOffsetTop == autoTopMap[fe.id]; if ((top == 'auto' || isImplicitAuto) && top != '0px') { @@ -302,8 +316,8 @@ export class FixedLayer { } } - const bottom = styles.getPropertyValue('bottom'); - const opacity = parseFloat(styles.getPropertyValue('opacity')); + const bottom = styles.bottom; + const opacity = parseFloat(styles.opacity); // Transferability requires element to be fixed and top or bottom to // be styled with `0`. Also, do not transfer transparent // elements - that's a lot of work for no benefit. Additionally, @@ -323,24 +337,25 @@ export class FixedLayer { } state[fe.id] = { fixed: isFixed, + sticky: isSticky, transferrable: isTransferrable, top, - zIndex: styles.getPropertyValue('z-index'), - transform: styles.getPropertyValue('transform'), + zIndex: styles.zIndex, + transform: styles.transform, }; }); }, mutate: state => { if (hasTransferables && this.transfer_) { - const fixedLayer = this.getFixedLayer_(); - if (fixedLayer.className != this.ampdoc.getBody().className) { - fixedLayer.className = this.ampdoc.getBody().className; + const transferLayer = this.getTransferLayer_(); + if (transferLayer.className != this.ampdoc.getBody().className) { + transferLayer.className = this.ampdoc.getBody().className; } } - this.fixedElements_.forEach((fe, i) => { + this.elements_.forEach((fe, i) => { const feState = state[fe.id]; if (feState) { - this.mutateFixedElement_(fe, i, feState); + this.mutateElement_(fe, i, feState); } }); }, @@ -360,17 +375,18 @@ export class FixedLayer { } /** - * Calls `setupFixedSelectors_` in a try-catch. + * Calls `setupSelectors_` in a try-catch. * Fails quietly with a dev error if call fails. * This method should not be inlined to prevent TryCatch deoptimization. * NoInline keyword at the end of function name also prevents Closure compiler * from inlining the function. * @param {!Array} fixedSelectors + * @param {!Array} stickySelectors * @private */ - trySetupFixedSelectorsNoInline(fixedSelectors) { + trySetupSelectorsNoInline(fixedSelectors, stickySelectors) { try { - this.setupFixedSelectors_(fixedSelectors); + this.setupSelectors_(fixedSelectors, stickySelectors); } catch (e) { // Fail quietly. dev().error(TAG, 'Failed to setup fixed elements:', e); @@ -378,12 +394,13 @@ export class FixedLayer { } /** - * Calls `setupFixedElement_` for up to 10 elements matching each selector - * in `fixedSelectors`. + * Calls `setupElement_` for up to 10 elements matching each selector + * in `fixedSelectors` and for all selectors in `stickySelectors`. * @param {!Array} fixedSelectors + * @param {!Array} stickySelectors * @private */ - setupFixedSelectors_(fixedSelectors) { + setupSelectors_(fixedSelectors, stickySelectors) { for (let i = 0; i < fixedSelectors.length; i++) { const fixedSelector = fixedSelectors[i]; const elements = this.ampdoc.getRootNode().querySelectorAll( @@ -393,73 +410,92 @@ export class FixedLayer { // We shouldn't have too many of `fixed` elements. break; } - this.setupFixedElement_(elements[j], fixedSelector); + this.setupElement_(elements[j], fixedSelector, 'fixed'); + } + } + for (let i = 0; i < stickySelectors.length; i++) { + const stickySelector = stickySelectors[i]; + const elements = this.ampdoc.getRootNode().querySelectorAll( + stickySelector); + for (let j = 0; j < elements.length; j++) { + this.setupElement_(elements[j], stickySelector, 'sticky'); } } } /** - * This method records the potentially fixed element. One of a more critical - * function - it records all selectors that may apply "fixed" to this element - * to check them later. + * This method records the potentially fixed or sticky element. One of a more + * critical functions - it records all selectors that may apply "fixed" + * or "sticky" to this element to check them later. * * @param {!Element} element * @param {string} selector + * @param {string} position * @param {boolean=} opt_forceTransfer If set to true , then the element needs - * to be forcefully transferred to the fixed layer. + * to be forcefully transferred to the transfer layer. * @private */ - setupFixedElement_(element, selector, opt_forceTransfer) { + setupElement_(element, selector, position, opt_forceTransfer) { let fe = null; - for (let i = 0; i < this.fixedElements_.length; i++) { - if (this.fixedElements_[i].element == element) { - fe = this.fixedElements_[i]; + for (let i = 0; i < this.elements_.length; i++) { + if (this.elements_[i].element == element && + this.elements_[i].position == position) { + fe = this.elements_[i]; break; } } + const isFixed = position == 'fixed'; if (fe) { // Already seen. fe.selectors.push(selector); } else { // A new entry. - const fixedId = 'F' + (this.counter_++); - element.setAttribute('i-amp-fixedid', fixedId); - element[DECLARED_FIXED_PROP] = true; + const id = 'F' + (this.counter_++); + element.setAttribute('i-amp-fixedid', id); + if (isFixed) { + element[DECLARED_FIXED_PROP] = true; + } else { + element[DECLARED_STICKY_PROP] = true; + } fe = { - id: fixedId, + id, element, + position, selectors: [selector], + fixedNow: false, + stickyNow: false, }; - this.fixedElements_.push(fe); + this.elements_.push(fe); } - fe.forceTransfer = !!opt_forceTransfer; + fe.forceTransfer = isFixed && !!opt_forceTransfer; } /** * Removes element from the fixed layer. * * @param {!Element} element - * @return {FixedElementDef|undefined} + * @return {!Array} * @private */ - removeFixedElement_(element) { - for (let i = 0; i < this.fixedElements_.length; i++) { - if (this.fixedElements_[i].element == element) { + removeElement_(element) { + const removed = []; + for (let i = 0; i < this.elements_.length; i++) { + if (this.elements_[i].element == element) { this.vsync_.mutate(() => { setStyle(element, 'top', ''); }); - const fe = this.fixedElements_[i]; - this.fixedElements_.splice(i, 1); - return fe; + const fe = this.elements_[i]; + this.elements_.splice(i, 1); + removed.push(fe); } } - return undefined; + return removed; } /** @private */ sortInDomOrder_() { - this.fixedElements_.sort(function(fe1, fe2) { + this.elements_.sort(function(fe1, fe2) { // 8 | 2 = 0x0A // 2 - preceeding // 8 - contains @@ -471,24 +507,39 @@ export class FixedLayer { } /** - * Mutates the fixed element. At this point it's determined that the element - * is indeed fixed. There are two main functions here: + * Mutates the fixed/sticky element. At this point it's determined that the + * element is indeed fixed/sticky. There are two main functions here: * 1. `top` has to be updated to reflect viewer's paddingTop. * 2. The element may need to be transfered to the separate fixed layer. * - * @param {!FixedElementDef} fe + * @param {!ElementDef} fe * @param {number} index - * @param {!FixedElementStateDef} state + * @param {!ElementStateDef} state * @private */ - mutateFixedElement_(fe, index, state) { + mutateElement_(fe, index, state) { const element = fe.element; const oldFixed = fe.fixedNow; + const oldSticky = fe.stickyNow; fe.fixedNow = state.fixed; - fe.top = state.fixed ? state.top : ''; + fe.stickyNow = state.sticky; + fe.top = (state.fixed || state.sticky) ? state.top : ''; fe.transform = state.transform; - if (state.fixed) { + + // Reset `top` which was assigned before. + if (oldFixed && !state.fixed || oldSticky && !state.sticky) { + if (getStyle(element, 'top')) { + setStyle(element, 'top', ''); + } + } + // Move back to the BODY layer and reset transfer z-index. + if (oldFixed && !state.fixed || !state.transferrable) { + this.returnFromTransferLayer_(fe); + } + + // Update the new fixed/sticky state. + if (state.fixed || state.sticky) { // Update `top`. This is necessary to adjust position to the viewer's // paddingTop. setStyle(element, 'top', state.top ? @@ -496,33 +547,22 @@ export class FixedLayer { ''); // Move element to the fixed layer. - if (!oldFixed && this.transfer_) { - if (state.transferrable) { - this.transferToFixedLayer_(fe, index, state); - } else { - this.returnFromFixedLayer_(fe); - } + if (this.transfer_ && + state.fixed && !oldFixed && state.transferrable) { + this.transferToTransferLayer_(fe, index, state); } - } else if (oldFixed) { - // Reset `top` which was assigned above. - if (getStyle(element, 'top')) { - setStyle(element, 'top', ''); - } - - // Move back to the BODY layer and reset transfer z-index. - this.returnFromFixedLayer_(fe); } } /** - * @param {!FixedElementDef} fe + * @param {!ElementDef} fe * @param {number} index - * @param {!FixedElementStateDef} state + * @param {!ElementStateDef} state * @private */ - transferToFixedLayer_(fe, index, state) { + transferToTransferLayer_(fe, index, state) { const element = fe.element; - if (element.parentElement == this.fixedLayer_) { + if (element.parentElement == this.transferLayer_) { return; } @@ -543,9 +583,9 @@ export class FixedLayer { `calc(${10000 + index} + ${state.zIndex || 0})`); element.parentElement.replaceChild(fe.placeholder, element); - this.getFixedLayer_().appendChild(element); + this.getTransferLayer_().appendChild(element); - // Test if the element still matches one of the `fixed ` selectors. If not + // Test if the element still matches one of the `fixed` selectors. If not // return it back to BODY. const matches = fe.selectors.some( selector => this.matches_(element, selector)); @@ -554,7 +594,7 @@ export class FixedLayer { 'Failed to move the element to the fixed position layer.' + ' This is most likely due to the compound CSS selector:', fe.element); - this.returnFromFixedLayer_(fe); + this.returnFromTransferLayer_(fe); } } @@ -581,10 +621,10 @@ export class FixedLayer { } /** - * @param {!FixedElementDef} fe + * @param {!ElementDef} fe * @private */ - returnFromFixedLayer_(fe) { + returnFromTransferLayer_(fe) { if (!fe.placeholder || !this.ampdoc.contains(fe.placeholder)) { return; } @@ -602,15 +642,15 @@ export class FixedLayer { /** * @return {?Element} */ - getFixedLayer_() { + getTransferLayer_() { // This mode is only allowed for a single-doc case. - if (!this.transfer_ || this.fixedLayer_) { - return this.fixedLayer_; + if (!this.transfer_ || this.transferLayer_) { + return this.transferLayer_; } const doc = this.ampdoc.win.document; - this.fixedLayer_ = doc.body.cloneNode(/* deep */ false); - this.fixedLayer_.removeAttribute('style'); - setStyles(this.fixedLayer_, { + this.transferLayer_ = doc.body.cloneNode(/* deep */ false); + this.transferLayer_.removeAttribute('style'); + setStyles(this.transferLayer_, { position: 'absolute', top: 0, left: 0, @@ -636,26 +676,31 @@ export class FixedLayer { transition: 'none', visibility: 'visible', }); - doc.documentElement.appendChild(this.fixedLayer_); - return this.fixedLayer_; + doc.documentElement.appendChild(this.transferLayer_); + return this.transferLayer_; } /** * @param {!Array} rules * @param {!Array} foundSelectors + * @param {!Array} stickySelectors * @private */ - discoverFixedSelectors_(rules, foundSelectors) { + discoverSelectors_(rules, foundSelectors, stickySelectors) { for (let i = 0; i < rules.length; i++) { const rule = rules[i]; if (rule.type == /* CSSStyleRule */ 1) { - if (rule.selectorText != '*' && rule.style.position == 'fixed') { - foundSelectors.push(rule.selectorText); + if (rule.selectorText != '*' && rule.style.position) { + if (rule.style.position == 'fixed') { + foundSelectors.push(rule.selectorText); + } else if (endsWith(rule.style.position, 'sticky')) { + stickySelectors.push(rule.selectorText); + } } } else if (rule.type == /* CSSMediaRule */ 4) { - this.discoverFixedSelectors_(rule.cssRules, foundSelectors); + this.discoverSelectors_(rule.cssRules, foundSelectors, stickySelectors); } else if (rule.type == /* CSSSupportsRule */ 12) { - this.discoverFixedSelectors_(rule.cssRules, foundSelectors); + this.discoverSelectors_(rule.cssRules, foundSelectors, stickySelectors); } } } @@ -667,21 +712,24 @@ export class FixedLayer { * id: string, * selectors: !Array, * element: !Element, + * position: string, * placeholder: (?Element|undefined), - * fixedNow: (boolean|undefined), + * fixedNow: boolean, + * stickyNow: boolean, * top: (string|undefined), * transform: (string|undefined), * forceTransfer: (boolean|undefined), * }} */ -let FixedElementDef; +let ElementDef; /** * @typedef {{ * fixed: boolean, + * sticky: boolean, * transferrable: boolean, * top: string, * zIndex: string, * }} */ -let FixedElementStateDef; +let ElementStateDef; diff --git a/src/service/jank-meter.js b/src/service/jank-meter.js index 9cc35b43d71de..269144f4bbdb1 100644 --- a/src/service/jank-meter.js +++ b/src/service/jank-meter.js @@ -15,8 +15,12 @@ */ import {isExperimentOn} from '../experiments'; +import {performanceForOrNull} from '../performance'; import {dev} from '../log'; +/** @const {number} */ +const NTH_FRAME = 200; + export class JankMeter { /** @@ -25,20 +29,28 @@ export class JankMeter { constructor(win) { /** @private {!Window} */ this.win_ = win; - /** @private {!Element} */ - this.jankMeterDisplay_ = this.win_.document.createElement('div'); - this.jankMeterDisplay_.classList.add('i-amphtml-jank-meter'); /** @private {number} */ this.jankCnt_ = 0; /** @private {number} */ this.totalCnt_ = 0; - this.win_.document.body.appendChild(this.jankMeterDisplay_); - this.updateMeterDisplay_(0); /** @private {?number} */ this.scheduledTime_ = null; + /** @private {?./performance-impl.Performance} */ + this.perf_ = performanceForOrNull(win); + + if (isJankMeterEnabled(win)) { + /** @private {!Element} */ + this.jankMeterDisplay_ = this.win_.document.createElement('div'); + this.jankMeterDisplay_.classList.add('i-amphtml-jank-meter'); + this.win_.document.body.appendChild(this.jankMeterDisplay_); + this.updateMeterDisplay_(0); + } } onScheduled() { + if (!this.isEnabled_()) { + return; + } // only take the first schedule for the current frame. if (this.scheduledTime_ == null) { this.scheduledTime_ = this.win_.Date.now(); @@ -46,7 +58,7 @@ export class JankMeter { } onRun() { - if (this.scheduledTime_ == null) { + if (!this.isEnabled_() || this.scheduledTime_ == null) { return; } const paintLatency = this.win_.Date.now() - this.scheduledTime_; @@ -56,7 +68,22 @@ export class JankMeter { this.jankCnt_++; dev().info('JANK', 'Paint latency: ' + paintLatency + 'ms'); } - this.updateMeterDisplay_(paintLatency); + + // Report Good Frame Probability on Nth frame. + if (this.perf_ && this.totalCnt_ == NTH_FRAME) { + this.perf_.tickDelta('gfp', this.calculateGfp_()); + this.perf_.flush(); + } + if (isJankMeterEnabled(this.win_)) { + this.updateMeterDisplay_(paintLatency); + } + } + + isEnabled_() { + return isJankMeterEnabled(this.win_) + || (this.perf_ + && this.perf_.isPerformanceTrackingOn() + && this.totalCnt_ < NTH_FRAME); } /** @@ -64,14 +91,22 @@ export class JankMeter { * @private */ updateMeterDisplay_(paintLatency) { - // Calculate Good Frame Probability - const gfp = this.win_.Math.floor( - (this.totalCnt_ - this.jankCnt_) / this.totalCnt_ * 100); + const gfp = this.calculateGfp_(); this.jankMeterDisplay_.textContent = `${gfp}%|${this.totalCnt_}|${paintLatency}ms`; } + + /** + * Calculate Good Frame Probability, which is a value range from 0 to 100. + * @returns {number} + * @private + */ + calculateGfp_() { + return this.win_.Math.floor( + (this.totalCnt_ - this.jankCnt_) / this.totalCnt_ * 100); + } } -export function isJankMeterEnabled(win) { +function isJankMeterEnabled(win) { return isExperimentOn(win, 'jank-meter'); } diff --git a/src/service/performance-impl.js b/src/service/performance-impl.js index 7b3e3a4778e2f..1addf30506bba 100644 --- a/src/service/performance-impl.js +++ b/src/service/performance-impl.js @@ -21,6 +21,7 @@ import {resourcesForDoc} from '../resources'; import {viewerForDoc} from '../viewer'; import {viewportForDoc} from '../viewport'; import {whenDocumentComplete} from '../document-ready'; +import {urls} from '../config'; /** @@ -90,6 +91,9 @@ export class Performance { /** @private {boolean} */ this.isPerformanceTrackingOn_ = false; + /** @private {?string} */ + this.enabledExperiments_ = null; + // Tick window.onload event. whenDocumentComplete(win.document).then(() => { this.tick('ol'); @@ -211,6 +215,8 @@ export class Performance { * Ticks a timing event. * * @param {string} label The variable name as it will be reported. + * See TICKEVENTS.md for available metrics, and edit this file + * when adding a new metric. * @param {?string=} opt_from The label of a previous tick to use as a * relative start for this tick. * @param {number=} opt_value The time to record the tick at. Optional, if @@ -269,11 +275,33 @@ export class Performance { */ flush() { if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) { - this.viewer_.sendMessage('sendCsi', undefined, - /* cancelUnsent */true); + const experiments = this.getEnabledExperiments_(); + const payload = experiments === '' ? undefined : { + ampexp: experiments, + }; + this.viewer_.sendMessage('sendCsi', payload, /* cancelUnsent */true); + } + } + + /** + * @returns {string} comma-separated list of experiment IDs + * @private + */ + getEnabledExperiments_() { + if (this.enabledExperiments_ !== null) { + return this.enabledExperiments_; } + const experiments = []; + // Check if it's the legacy CDN domain. + if (this.getHostname_() == urls.cdn.split('://')[1]) { + experiments.push('legacy-cdn-domain'); + } + return this.enabledExperiments_ = experiments.join(','); } + getHostname_() { + return this.win.location.hostname; + } /** * Queues the events to be flushed when tick function is set. diff --git a/src/service/resource.js b/src/service/resource.js index 261b595d78b76..0a673196f7255 100644 --- a/src/service/resource.js +++ b/src/service/resource.js @@ -20,7 +20,7 @@ import { moveLayoutRect, } from '../layout-rect'; import {dev} from '../log'; -import {toggle} from '../style'; +import {toggle, computedStyle} from '../style'; const TAG = 'Resource'; const RESOURCE_PROP_ = '__AMP__RESOURCE'; @@ -376,8 +376,8 @@ export class Resource { isFixed = true; break; } - if (viewport.isDeclaredFixed(n) && - win./*OK*/getComputedStyle(n).position == 'fixed') { + if (viewport.isDeclaredFixed(n) + && computedStyle(win, n).position == 'fixed') { isFixed = true; break; } diff --git a/src/service/resources-impl.js b/src/service/resources-impl.js index 12c074d75d652..b9d2f107fbbe6 100644 --- a/src/service/resources-impl.js +++ b/src/service/resources-impl.js @@ -36,6 +36,7 @@ import {filterSplice} from '../utils/array'; import {getSourceUrl} from '../url'; import {areMarginsChanged} from '../layout-rect'; import {documentInfoForDoc} from '../document-info'; +import {computedStyle} from '../style'; const TAG_ = 'Resources'; const READY_SCAN_SIGNAL_ = 'ready-scan'; @@ -1546,12 +1547,12 @@ export class Resources { * @private */ getLayoutMargins_(resource) { - const computedStyle = this.win./*OK*/getComputedStyle(resource.element); + const style = computedStyle(this.win, resource.element); return { - top: parseInt(computedStyle.marginTop, 10) || 0, - right: parseInt(computedStyle.marginRight, 10) || 0, - bottom: parseInt(computedStyle.marginBottom, 10) || 0, - left: parseInt(computedStyle.marginLeft, 10) || 0, + top: parseInt(style.marginTop, 10) || 0, + right: parseInt(style.marginRight, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, + left: parseInt(style.marginLeft, 10) || 0, }; } diff --git a/src/service/standard-actions-impl.js b/src/service/standard-actions-impl.js index a76ed79c728c2..26d9038a7a1bb 100644 --- a/src/service/standard-actions-impl.js +++ b/src/service/standard-actions-impl.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {OBJECT_STRING_ARGS_KEY} from '../service/action-impl'; import {actionServiceForDoc} from '../action'; import {bindForDoc} from '../bind'; import {dev, user} from '../log'; @@ -22,7 +23,6 @@ import {historyForDoc} from '../history'; import {installResourcesServiceForDoc} from './resources-impl'; import {toggle} from '../style'; - /** * This service contains implementations of some of the most typical actions, * such as hiding DOM elements. @@ -71,7 +71,20 @@ export class StandardActions { switch (invocation.method) { case 'setState': bindForDoc(this.ampdoc).then(bind => { - bind.setState(invocation.args); + const args = invocation.args; + const objectString = args[OBJECT_STRING_ARGS_KEY]; + if (objectString) { + // Object string arg. + const scope = Object.create(null); + const event = invocation.event; + if (event && event.detail) { + scope['event'] = event.detail; + } + bind.setStateWithExpression(objectString, scope); + } else { + // Key-value args. + bind.setState(args); + } }); return; case 'goBack': diff --git a/src/service/storage-impl.js b/src/service/storage-impl.js index d227b1aa40eb2..e4b842cee6cba 100644 --- a/src/service/storage-impl.js +++ b/src/service/storage-impl.js @@ -286,7 +286,7 @@ export class LocalStorageBinding { this.win = win; /** @private @const {boolean} */ - this.isLocalStorageSupported_ = 'localStorage' in this.win; + this.isLocalStorageSupported_ = this.checkIsLocalStorageSupported_(); if (!this.isLocalStorageSupported_) { const error = new Error('localStorage not supported.'); @@ -294,6 +294,28 @@ export class LocalStorageBinding { } } + /** + * Determines whether localStorage API is supported by ensuring it is declared + * and does not throw an exception when used. + * @return {boolean} + * @private + */ + checkIsLocalStorageSupported_() { + try { + if (!('localStorage' in this.win)) { + return false; + } + + // We do not care about the value fetched from local storage; we only care + // whether the call throws an exception or not. As such, we can look up + // any arbitrary key. + this.win.localStorage.getItem('test'); + return true; + } catch (e) { + return false; + } + } + /** * @param {string} origin * @return {string} diff --git a/src/service/viewport-impl.js b/src/service/viewport-impl.js index 28b08bcc33e03..f54e1f90173a4 100644 --- a/src/service/viewport-impl.js +++ b/src/service/viewport-impl.js @@ -28,7 +28,7 @@ import {dev} from '../log'; import {numeric} from '../transition'; import {onDocumentReady, whenDocumentReady} from '../document-ready'; import {platformFor} from '../platform'; -import {px, setStyle, setStyles} from '../style'; +import {px, setStyle, setStyles, computedStyle} from '../style'; import {timerFor} from '../timer'; import {installVsyncService} from './vsync-impl'; import {viewerForDoc} from '../viewer'; @@ -437,6 +437,7 @@ export class Viewport { this.viewer_.sendMessage('requestFullOverlay', {}, /* cancelUnsent */true); this.disableTouchZoom(); this.hideFixedLayer(); + this.disableScroll(); this.vsync_.mutate(() => this.binding_.updateLightboxMode(true)); } @@ -445,11 +446,31 @@ export class Viewport { */ leaveLightboxMode() { this.viewer_.sendMessage('cancelFullOverlay', {}, /* cancelUnsent */true); + this.resetScroll(); this.showFixedLayer(); this.restoreOriginalTouchZoom(); this.vsync_.mutate(() => this.binding_.updateLightboxMode(false)); } + /* + * Disable the scrolling by setting overflow: hidden. + * Should only be used for temporarily disabling scroll. + */ + disableScroll() { + this.vsync_.mutate(() => { + this.binding_.disableScroll(); + }); + } + + /* + * Reset the scrolling by removing overflow: hidden. + */ + resetScroll() { + this.vsync_.mutate(() => { + this.binding_.resetScroll(); + }); + } + /** * Resets touch zoom to initial scale of 1. */ @@ -812,6 +833,17 @@ export class ViewportBindingDef { */ showViewerHeader(unusedTransient, unusedPaddingTop) {} + /* + * Disable the scrolling by setting overflow: hidden. + * Should only be used for temporarily disabling scroll. + */ + disableScroll() {} + + /* + * Reset the scrolling by removing overflow: hidden. + */ + resetScroll() {} + /** * Updates the viewport whether it's currently in the lightbox or a normal * mode. @@ -993,6 +1025,18 @@ export class ViewportBindingNatural_ { } } + /** @override */ + disableScroll() { + this.win.document.documentElement.classList.add( + 'i-amphtml-scroll-disabled'); + } + + /** @override */ + resetScroll() { + this.win.document.documentElement.classList.remove( + 'i-amphtml-scroll-disabled'); + } + /** @override */ updateLightboxMode(unusedLightboxMode) { // The layout is always accurate. @@ -1243,9 +1287,10 @@ export class ViewportBindingNaturalIosEmbed_ { // Add extra paddingTop to make the content stay at the same position // when the hiding header operation is transient onDocumentReady(this.win.document, doc => { + const body = dev().assertElement(doc.body); const existingPaddingTop = - this.win./*OK*/getComputedStyle(doc.body)['padding-top'] || '0'; - setStyles(dev().assertElement(doc.body), { + computedStyle(this.win, body).paddingTop || '0'; + setStyles(body, { paddingTop: `calc(${existingPaddingTop} + ${lastPaddingTop}px)`, borderTop: '', }); @@ -1264,6 +1309,16 @@ export class ViewportBindingNaturalIosEmbed_ { // operation is transient } + /** @override */ + disableScroll() { + // This is not supported in ViewportBindingNaturalIosEmbed_ + } + + /** @override */ + resetScroll() { + // This is not supported in ViewportBindingNaturalIosEmbed_ + } + /** @override */ updatePaddingTop(paddingTop) { onDocumentReady(this.win.document, doc => { @@ -1548,6 +1603,16 @@ export class ViewportBindingIosEmbedWrapper_ { } } + /** @override */ + disableScroll() { + this.wrapper_.classList.add('i-amphtml-scroll-disabled'); + } + + /** @override */ + resetScroll() { + this.wrapper_.classList.remove('i-amphtml-scroll-disabled'); + } + /** @override */ updateLightboxMode(unusedLightboxMode) { // The layout is always accurate. diff --git a/src/service/vsync-impl.js b/src/service/vsync-impl.js index 54d2f6bf31d47..9b406285f53b8 100644 --- a/src/service/vsync-impl.js +++ b/src/service/vsync-impl.js @@ -23,7 +23,7 @@ import {documentStateFor} from './document-state'; import {getService} from '../service'; import {installTimerService} from './timer-impl'; import {viewerForDoc, viewerPromiseForDoc} from '../viewer'; -import {JankMeter, isJankMeterEnabled} from './jank-meter'; +import {JankMeter} from './jank-meter'; /** @const {time} */ const FRAME_TIME = 16; @@ -130,9 +130,8 @@ export class Vsync { this.docState_.onVisibilityChanged(boundOnVisibilityChanged); } - /** @private {?JankMeter} */ - this.jankMeter_ = - isJankMeterEnabled(this.win) ? new JankMeter(this.win) : null; + /** @private {!JankMeter} */ + this.jankMeter_ = new JankMeter(this.win); } /** @private */ @@ -350,9 +349,7 @@ export class Vsync { } // Schedule actual animation frame and then run tasks. this.scheduled_ = true; - if (this.jankMeter_) { - this.jankMeter_.onScheduled(); - } + this.jankMeter_.onScheduled(); this.forceSchedule_(); } @@ -373,9 +370,7 @@ export class Vsync { */ runScheduledTasks_() { this.scheduled_ = false; - if (this.jankMeter_) { - this.jankMeter_.onRun(); - } + this.jankMeter_.onRun(); const tasks = this.tasks_; const states = this.states_; @@ -456,4 +451,4 @@ export function installVsyncService(window) { installTimerService(window); return new Vsync(window); })); -}; +} diff --git a/src/string.js b/src/string.js index 4a91be7740cd6..4eda49a38afc1 100644 --- a/src/string.js +++ b/src/string.js @@ -14,15 +14,22 @@ * limitations under the License. */ +/** + * @param {string} _match + * @param {string} character + * @return {string} + */ +function toUpperCase(_match, character) { + return character.toUpperCase(); +} + /** * @param {string} name Attribute name with dashes * @return {string} Dashes removed and character after to upper case. * visibleForTesting */ export function dashToCamelCase(name) { - return name.replace(/-([a-z])/g, function(_all, character) { - return character.toUpperCase(); - }); + return name.replace(/-([a-z])/g, toUpperCase); } /** diff --git a/src/style.js b/src/style.js index abd8ebb3ae283..670b544be8894 100644 --- a/src/style.js +++ b/src/style.js @@ -15,6 +15,8 @@ */ // Note: loaded by 3p system. Cannot rely on babel polyfills. +import {map} from './utils/object.js'; + /** @type {Object} */ let propertyNameCache; @@ -36,7 +38,7 @@ export function camelCaseToTitleCase(camelCase) { * Checks the style if a prefixed version of a property exists and returns * it or returns an empty string. * @private - * @param {!CSSStyleDeclaration|!HTMLDocument} style + * @param {!Object} style * @param {string} titleCase the title case version of a css property name * @return {string} the prefixed property name or null. */ @@ -55,7 +57,7 @@ function getVendorJsPropertyName_(style, titleCase) { * (ex. WebkitTransitionDuration) given a camelCase'd version of the property * (ex. transitionDuration). * @export - * @param {!CSSStyleDeclaration|!HTMLDocument} style + * @param {!Object} style * @param {string} camelCase the camel cased version of a css property name * @param {boolean=} opt_bypassCache bypass the memoized cache of property * mapping @@ -63,7 +65,7 @@ function getVendorJsPropertyName_(style, titleCase) { */ export function getVendorJsPropertyName(style, camelCase, opt_bypassCache) { if (!propertyNameCache) { - propertyNameCache = Object.create(null); + propertyNameCache = map(); } let propertyName = propertyNameCache[camelCase]; if (!propertyName || opt_bypassCache) { @@ -207,3 +209,16 @@ export function removeAlphaFromColor(rgbaColor) { return rgbaColor.replace( /\(([^,]+),([^,]+),([^,)]+),[^)]+\)/g, '($1,$2,$3, 1)'); } + +/** + * Gets the computed style of the element. The helper is necessary to enforce + * the possible `null` value returned by a buggy Firefox. + * + * @param {!Window} win + * @param {!Element} el + * @return {!Object} + */ +export function computedStyle(win, el) { + const style = /** @type {?CSSStyleDeclaration} */(win.getComputedStyle(el)); + return /** @type {!Object} */(style) || map(); +} diff --git a/src/web-worker/amp-worker.js b/src/web-worker/amp-worker.js index 8d1e756675317..f65b8e687d05f 100644 --- a/src/web-worker/amp-worker.js +++ b/src/web-worker/amp-worker.js @@ -18,8 +18,9 @@ import {FromWorkerMessageDef, ToWorkerMessageDef} from './web-worker-defines'; import {calculateEntryPointScriptUrl} from '../service/extension-location'; import {dev} from '../log'; import {fromClass} from '../service'; -import {isExperimentOn} from '../experiments'; import {getMode} from '../mode'; +import {isExperimentOn} from '../experiments'; +import {xhrFor} from '../xhr'; const TAG = 'web-worker'; @@ -69,11 +70,25 @@ class AmpWorker { /** @const @private {!Window} */ this.win_ = win; + /** @const @private {!../service/xhr-impl.Xhr} */ + this.xhr_ = xhrFor(win); + const url = calculateEntryPointScriptUrl(location, 'ww', getMode().localDev); - /** @const @private {!Worker} */ - this.worker_ = new win.Worker(url); - this.worker_.onmessage = this.receiveMessage_.bind(this); + dev().fine(TAG, 'Fetching web worker from:', url); + + /** @private {Worker} */ + this.worker_ = null; + + /** @const @private {!Promise} */ + this.fetchPromise_ = + this.xhr_.fetchText(url, {ampCors: false}).then(text => { + // Workaround since Worker constructor only accepts same origin URLs. + const blob = new win.Blob([text], {type: 'text/javascript'}); + const blobUrl = win.URL.createObjectURL(blob); + this.worker_ = new win.Worker(blobUrl); + this.worker_.onmessage = this.receiveMessage_.bind(this); + }); /** * Array of in-flight messages pending response from worker. @@ -96,13 +111,15 @@ class AmpWorker { * @private */ sendMessage_(method, args) { - return new Promise((resolve, reject) => { - const id = this.counter_++; - this.messages_[id] = {method, resolve, reject}; - - /** @type {ToWorkerMessageDef} */ - const message = {method, args, id}; - this.worker_./*OK*/postMessage(message); + return this.fetchPromise_.then(() => { + return new Promise((resolve, reject) => { + const id = this.counter_++; + this.messages_[id] = {method, resolve, reject}; + + /** @type {ToWorkerMessageDef} */ + const message = {method, args, id}; + this.worker_./*OK*/postMessage(message); + }); }); } @@ -137,4 +154,12 @@ class AmpWorker { hasPendingMessages() { return Object.keys(this.messages_).length > 0; } + + /** + * @return {!Promise} + * @visibleForTesting + */ + fetchPromiseForTesting() { + return this.fetchPromise_; + } } diff --git a/src/web-worker/web-worker.js b/src/web-worker/web-worker.js index 44f33abe0a561..28d0c1409e49a 100644 --- a/src/web-worker/web-worker.js +++ b/src/web-worker/web-worker.js @@ -24,6 +24,9 @@ import '../../third_party/babel/custom-babel-helpers'; import {BindEvaluator} from '../../extensions/amp-bind/0.1/bind-evaluator'; import {FromWorkerMessageDef, ToWorkerMessageDef} from './web-worker-defines'; +import {installWorkerErrorReporting} from '../worker-error-reporting'; + +installWorkerErrorReporting('ww'); /** @private {BindEvaluator} */ let evaluator_; @@ -33,7 +36,6 @@ self.addEventListener('message', function(event) { let returnValue; - // TODO(choumx): Add error reporting. switch (method) { case 'bind.addBindings': evaluator_ = evaluator_ || new BindEvaluator(); @@ -47,9 +49,16 @@ self.addEventListener('message', function(event) { throw new Error(`${method}: BindEvaluator is not initialized.`); } break; - case 'bind.evaluate': + case 'bind.evaluateBindings': + if (evaluator_) { + returnValue = evaluator_.evaluateBindings.apply(evaluator_, args); + } else { + throw new Error(`${method}: BindEvaluator is not initialized.`); + } + break; + case 'bind.evaluateExpression': if (evaluator_) { - returnValue = evaluator_.evaluate.apply(evaluator_, args); + returnValue = evaluator_.evaluateExpression.apply(evaluator_, args); } else { throw new Error(`${method}: BindEvaluator is not initialized.`); } diff --git a/src/worker-error-reporting.js b/src/worker-error-reporting.js new file mode 100644 index 0000000000000..6febef07a0bf8 --- /dev/null +++ b/src/worker-error-reporting.js @@ -0,0 +1,72 @@ +/** + * 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. + */ + +/** + * @fileoverview Simplified error reporting for errors in web & service workers. + */ + +import {urls} from './config'; +import {exponentialBackoff} from './exponential-backoff'; + +/** + * Installs error reporting on the `self` global. Error requests contain a + * URL param "`tag`=1" that identifies the originating worker. + * @param {string} tag + */ +export function installWorkerErrorReporting(tag) { + /** + * Exponential backoff for error reports to avoid any given + * worker from generating a very large number of errors. + * @const {function(function()): number} + */ + const backoff = exponentialBackoff(1.5); + + self.addEventListener('unhandledrejection', event => { + backoff(() => report(event.reason)); + }); + + self.addEventListener('error', event => { + backoff(() => report(event.error)); + }); + + /** + * Report error to AMP's error reporting frontend. + * + * @param {*} e + */ + function report(e) { + // Don't report local dev errors. + if (urls.localhostRegex.test(self.location.origin)) { + return; + } + if (!(e instanceof Error)) { + e = new Error(e); + } + const config = self.AMP_CONFIG || {}; + const url = urls.errorReporting + '?' + + `${tag}=1` + // Tags request as coming from a worker. + '&v=' + encodeURIComponent(config.v) + + '&m=' + encodeURIComponent(e.message) + + '&ca=' + (config.canary ? 1 : 0) + + '&s=' + encodeURIComponent(e.stack || ''); + fetch(url, /** @type {!RequestInit} */ ({ + // We don't care about the response. + mode: 'no-cors', + })).catch(reason => { + console./*OK*/error(reason); + }); + } +} diff --git a/test/_init_tests.js b/test/_init_tests.js index d8a18a9da08df..01c4d8bb1f70e 100644 --- a/test/_init_tests.js +++ b/test/_init_tests.js @@ -83,32 +83,42 @@ class TestConfig { * @type {!Array} */ this.skipMatchers = []; + + /** + * List of predicate functions that are called before running each test + * suite to check whether the suite should be skipped or not. + * If any of the functions return 'false', the suite will be skipped. + * @type {!Array} + */ + this.ifMatchers = []; + /** * Called for each test suite (things created by `describe`). * @type {!Array} */ this.configTasks = []; - this.platform_ = platformFor(window); + + this.platform = platformFor(window); } skipChrome() { - return this.skip(this.platform_.isChrome.bind(this.platform_)); + return this.skip(this.platform.isChrome.bind(this.platform)); } skipEdge() { - return this.skip(this.platform_.isEdge.bind(this.platform_)); + return this.skip(this.platform.isEdge.bind(this.platform)); } skipFirefox() { - return this.skip(this.platform_.isFirefox.bind(this.platform_)); + return this.skip(this.platform.isFirefox.bind(this.platform)); } skipSafari() { - return this.skip(this.platform_.isSafari.bind(this.platform_)); + return this.skip(this.platform.isSafari.bind(this.platform)); } skipIos() { - return this.skip(this.platform_.isIos.bind(this.platform_)); + return this.skip(this.platform.isIos.bind(this.platform)); } /** @@ -119,6 +129,34 @@ class TestConfig { return this; } + ifChrome() { + return this.if(this.platform.isChrome.bind(this.platform)); + } + + ifEdge() { + return this.if(this.platform.isEdge.bind(this.platform)); + } + + ifFirefox() { + return this.if(this.platform.isFirefox.bind(this.platform)); + } + + ifSafari() { + return this.if(this.platform.isSafari.bind(this.platform)); + } + + ifIos() { + return this.if(this.platform.isIos.bind(this.platform)); + } + + /** + * @param {function():boolean} fn + */ + if(fn) { + this.ifMatchers.push(fn); + return this; + } + retryOnSaucelabs() { if (!window.ampTestRuntimeConfig.saucelabs) { return this; @@ -135,7 +173,14 @@ class TestConfig { */ run(desc, fn) { for (let i = 0; i < this.skipMatchers.length; i++) { - if (this.skipMatchers[i]()) { + if (this.skipMatchers[i].call(this)) { + this.runner.skip(desc, fn); + return; + } + } + + for (let i = 0; i < this.ifMatchers.length; i++) { + if (!this.ifMatchers[i].call(this)) { this.runner.skip(desc, fn); return; } diff --git a/test/functional/test-action.js b/test/functional/test-action.js index 56b869625e170..dddc1e669c857 100644 --- a/test/functional/test-action.js +++ b/test/functional/test-action.js @@ -16,6 +16,7 @@ import { ActionService, + OBJECT_STRING_ARGS_KEY, applyActionInfoArgs, parseActionMap, } from '../../src/service/action-impl'; @@ -281,6 +282,12 @@ describe('ActionService parseAction', () => { expect(parseAction('e:t.m(01=1)').args['01']()).to.equal(1); }); + it('should parse with object literal args', () => { + const a = parseAction('e:t.m({"foo": {"bar": "qux"}})'); + expect(a.args[OBJECT_STRING_ARGS_KEY]()) + .to.equal('{"foo": {"bar": "qux"}}'); + }); + it('should dereference vars in arg value identifiers', () => { const data = {foo: {bar: 'baz'}}; const a = parseAction('e:t.m(key1=foo.bar)'); diff --git a/test/functional/test-anchor-click-interceptor.js b/test/functional/test-anchor-click-interceptor.js new file mode 100644 index 0000000000000..52686b03c1026 --- /dev/null +++ b/test/functional/test-anchor-click-interceptor.js @@ -0,0 +1,60 @@ +/** + * Copyright 2017 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 { + maybeExpandUrlParamsForTesting, +} from '../../src/anchor-click-interceptor'; +import { + installUrlReplacementsServiceForDoc, +} from '../../src/service/url-replacements-impl'; +import {createElementWithAttributes} from '../../src/dom'; + +describes.realWin('anchor-click-interceptor', {amp: true}, env => { + + let doc; + let ampdoc; + + beforeEach(() => { + ampdoc = env.ampdoc; + doc = ampdoc.win.document; + installUrlReplacementsServiceForDoc(ampdoc); + }); + + it('should replace CLICK_X and CLICK_Y in href', () => { + const a = createElementWithAttributes(doc, 'a', { + href: 'http://example.com/?x=CLICK_X&y=CLICK_Y', + }); + const div = createElementWithAttributes(doc, 'div', {}); + a.appendChild(div); + doc.body.appendChild(a); + + // first click + maybeExpandUrlParamsForTesting(ampdoc, { + target: div, + pageX: 12, + pageY: 34, + }); + expect(a.href).to.equal('http://example.com/?x=12&y=34'); + + // second click + maybeExpandUrlParamsForTesting(ampdoc, { + target: div, + pageX: 23, + pageY: 45, + }); + expect(a.href).to.equal('http://example.com/?x=23&y=45'); + }); +}); diff --git a/test/functional/test-error.js b/test/functional/test-error.js index 1da86479dd7a7..faeb4f09ae1ea 100644 --- a/test/functional/test-error.js +++ b/test/functional/test-error.js @@ -19,7 +19,9 @@ import { detectNonAmpJs, getErrorReportUrl, installErrorReporting, + isCancellation, reportError, + detectJsEngineFromStack, } from '../../src/error'; import {parseUrl, parseQueryString} from '../../src/url'; import {user} from '../../src/log'; @@ -86,7 +88,6 @@ describe('reportErrorToServer', () => { beforeEach(() => { onError = window.onerror; sandbox = sinon.sandbox.create(); - sandbox.spy(window, 'Image'); }); afterEach(() => { @@ -107,14 +108,28 @@ describe('reportErrorToServer', () => { expect(query.m).to.equal('XYZ'); expect(query.el).to.equal('u'); expect(query.a).to.equal('0'); - expect(query.s).to.equal(e.stack); + expect(query.s).to.equal(e.stack.trim()); expect(query['3p']).to.equal(undefined); expect(e.message).to.contain('_reported_'); - expect(query.or).to.contain('http://localhost'); + if (location.ancestorOrigins) { + expect(query.or).to.contain('http://localhost'); + } expect(query.vs).to.be.undefined; expect(query.ae).to.equal(''); expect(query.r).to.contain('http://localhost'); expect(query.noAmp).to.equal('1'); + expect(query.args).to.be.undefined; + }); + + it('reportError with error object w/args', () => { + const e = new Error('XYZ'); + e.args = {x: 1}; + const url = parseUrl( + getErrorReportUrl(undefined, undefined, undefined, undefined, e, + true)); + const query = parseQueryString(url.search); + + expect(query.args).to.equal(JSON.stringify({x: 1})); }); it('reportError with a string instead of error', () => { @@ -248,6 +263,27 @@ describe('reportErrorToServer', () => { expect(url).to.be.undefined; }); + it('should construct cancellation', () => { + const e = cancellation(); + expect(isCancellation(e)).to.be.true; + expect(isCancellation(e.message)).to.be.true; + + // Suffix is tollerated. + e.message += '___'; + expect(isCancellation(e)).to.be.true; + expect(isCancellation(e.message)).to.be.true; + + // Prefix is not tollerated. + e.message = '___' + e.message; + expect(isCancellation(e)).to.be.false; + expect(isCancellation(e.message)).to.be.false; + + expect(isCancellation('')).to.be.false; + expect(isCancellation(null)).to.be.false; + expect(isCancellation(1)).to.be.false; + expect(isCancellation({})).to.be.false; + }); + it('reportError with error object', () => { const e = cancellation(); const url = @@ -299,6 +335,15 @@ describe('reportErrorToServer', () => { expect(url).to.contain('&ex=1'); }); + it('should omit the error stack for user errors', () => { + const e = user().createError('123'); + const url = parseUrl( + getErrorReportUrl(undefined, undefined, undefined, undefined, e, + true)); + const query = parseQueryString(url.search); + expect(query.s).to.be.undefined; + }); + describe('detectNonAmpJs', () => { let win; let scripts; @@ -368,6 +413,7 @@ describes.sandboxed('reportError', {}, () => { }); it('should accept string and report incorrect use', () => { + window.AMP_MODE = {localDev: true, test: false}; const result = reportError('error'); expect(result).to.be.instanceOf(Error); expect(result.message).to.be.equal('error'); @@ -379,6 +425,7 @@ describes.sandboxed('reportError', {}, () => { }); it('should accept number and report incorrect use', () => { + window.AMP_MODE = {localDev: true, test: false}; const result = reportError(101); expect(result).to.be.instanceOf(Error); expect(result.message).to.be.equal('101'); @@ -390,6 +437,7 @@ describes.sandboxed('reportError', {}, () => { }); it('should accept null and report incorrect use', () => { + window.AMP_MODE = {localDev: true, test: false}; const result = reportError(null); expect(result).to.be.instanceOf(Error); expect(result.message).to.be.equal('Unknown error'); @@ -400,3 +448,40 @@ describes.sandboxed('reportError', {}, () => { }).to.throw(/_reported_ Error reported incorrectly/); }); }); + +describe('detectJsEngineFromStack', () => { + // Note that these are not true of every case. You can emulate iOS Safari + // on Desktop Chrome and break this. These tests are explicitly for + // SauceLabs, which runs does not masquerade with UserAgent. + describe.configure().ifIos().run('on iOS', () => { + it.configure().ifSafari().run('detects safari as safari', () => { + expect(detectJsEngineFromStack()).to.equal('Safari'); + }); + + it.configure().ifChrome().run('detects chrome as safari', () => { + expect(detectJsEngineFromStack()).to.equal('Safari'); + }); + + it.configure().ifFirefox().run('detects firefox as safari', () => { + expect(detectJsEngineFromStack()).to.equal('Safari'); + }); + }); + + describe.configure().skipIos().run('on other OSs', () => { + it.configure().ifSafari().run('detects safari as safari', () => { + expect(detectJsEngineFromStack()).to.equal('Safari'); + }); + + it.configure().ifChrome().run('detects chrome as chrome', () => { + expect(detectJsEngineFromStack()).to.equal('Chrome'); + }); + + it.configure().ifFirefox().run('detects firefox as firefox', () => { + expect(detectJsEngineFromStack()).to.equal('Firefox'); + }); + + it.configure().ifEdge().run('detects edge as IE', () => { + expect(detectJsEngineFromStack()).to.equal('IE'); + }); + }); +}); diff --git a/test/functional/test-fixed-layer.js b/test/functional/test-fixed-layer.js index 902bb2bbfa3fe..cd6d353439db7 100644 --- a/test/functional/test-fixed-layer.js +++ b/test/functional/test-fixed-layer.js @@ -30,6 +30,8 @@ describe('FixedLayer', () => { let element1; let element2; let element3; + let element4; + let element5; let allRules; beforeEach(() => { @@ -44,11 +46,16 @@ describe('FixedLayer', () => { element1 = createElement('element1'); element2 = createElement('element2'); element3 = createElement('element3'); + element4 = createElement('element4'); + element5 = createElement('element4'); docBody.appendChild(element1); docBody.appendChild(element2); docBody.appendChild(element3); + docBody.appendChild(element4); + docBody.appendChild(element5); - const invalidRule = createValidRule('#invalid', [element1, element3]); + const invalidRule = createValidRule('#invalid', 'fixed', + [element1, element3]); documentApi = { styleSheets: [ // Will be ignored due to being a link. @@ -81,20 +88,25 @@ describe('FixedLayer', () => { { ownerNode: createStyleNode('amp-custom'), cssRules: [ - createValidRule('#amp-custom-rule1', [element1]), - createValidRule('#doc-body-id #amp-custom-rule1', [element1]), - createValidRule('#amp-custom-rule2', [element1, element2]), + createValidRule('#amp-custom-rule1', 'fixed', [element1]), + createValidRule('#doc-body-id #amp-custom-rule1', 'fixed', + [element1]), + createValidRule('#amp-custom-rule2', 'fixed', [element1, element2]), createUnrelatedRule('#amp-custom-rule3', [element3]), + createValidRule('#amp-custom-rule4', 'sticky', + [element2, element4]), + createValidRule('#amp-custom-rule5', '-webkit-sticky', [element5]), { type: 4, cssRules: [ - createValidRule('#amp-custom-media-rule1', [element1]), + createValidRule('#amp-custom-media-rule1', 'fixed', [element1]), ], }, { type: 12, cssRules: [ - createValidRule('#amp-custom-supports-rule1', [element2]), + createValidRule('#amp-custom-supports-rule1', 'fixed', + [element2]), ], }, // Uknown rule. @@ -107,8 +119,8 @@ describe('FixedLayer', () => { { ownerNode: createStyleNode(), cssRules: [ - createValidRule('#other-rule1', [element1]), - createValidRule('#other-rule2', [element2]), + createValidRule('#other-rule1', 'fixed', [element1]), + createValidRule('#other-rule2', 'fixed', [element2]), createUnrelatedRule('#other-rule3', [element1, element3]), ], }, @@ -124,11 +136,7 @@ describe('FixedLayer', () => { }, defaultView: { getComputedStyle: elem => { - return { - getPropertyValue: prop => { - return elem.computedStyle[prop] || ''; - }, - }; + return elem.computedStyle; }, navigator: window.navigator, }, @@ -177,6 +185,10 @@ describe('FixedLayer', () => { computedStyle: { opacity: '0.9', visibility: 'visible', + top: '', + bottom: '', + zIndex: '', + transform: '', }, matches: () => true, compareDocumentPosition: other => { @@ -237,11 +249,11 @@ describe('FixedLayer', () => { return node; } - function createValidRule(selector, elements) { + function createValidRule(selector, position, elements) { const rule = { type: 1, selectorText: selector, - style: {position: 'fixed'}, + style: {position}, elements, }; if (allRules[selector]) { @@ -276,7 +288,7 @@ describe('FixedLayer', () => { }); it('should initiale fixed layer to null', () => { - expect(fixedLayer.fixedLayer_).to.be.null; + expect(fixedLayer.transferLayer_).to.be.null; }); it('should discover all potentials', () => { @@ -284,15 +296,18 @@ describe('FixedLayer', () => { expect(actual.id).to.equal(expected.id, `${expected.id} wrong`); expect(actual.element).to.equal(expected.element, `${expected.id}: wrong element`); + expect(actual.position).to.equal(expected.position, + `${expected.id}: wrong position`); expect(JSON.stringify(actual.selectors)) .to.equal(JSON.stringify(expected.selectors), `${expected.id}: wrong selectors`); } - expect(fixedLayer.fixedElements_).to.have.length(2); - expectFe(fixedLayer.fixedElements_[0], { + expect(fixedLayer.elements_).to.have.length(5); + expectFe(fixedLayer.elements_[0], { id: 'F0', element: element1, + position: 'fixed', selectors: [ '#amp-custom-rule1', '#doc-body-id #amp-custom-rule1', @@ -301,99 +316,174 @@ describe('FixedLayer', () => { '#other-rule1', ], }); - expectFe(fixedLayer.fixedElements_[1], { + expectFe(fixedLayer.elements_[1], { id: 'F1', element: element2, + position: 'fixed', selectors: [ '#amp-custom-rule2', '#amp-custom-supports-rule1', '#other-rule2', ], }); + expectFe(fixedLayer.elements_[2], { + id: 'F2', + element: element2, + position: 'sticky', + selectors: [ + '#amp-custom-rule4', + ], + }); + expectFe(fixedLayer.elements_[3], { + id: 'F3', + element: element4, + position: 'sticky', + selectors: [ + '#amp-custom-rule4', + ], + }); + expectFe(fixedLayer.elements_[4], { + id: 'F4', + element: element5, + position: 'sticky', + selectors: [ + '#amp-custom-rule5', + ], + }); expect(fixedLayer.isDeclaredFixed(element1)).to.be.true; + expect(fixedLayer.isDeclaredSticky(element1)).to.be.false; expect(fixedLayer.isDeclaredFixed(element2)).to.be.true; + expect(fixedLayer.isDeclaredSticky(element2)).to.be.true; expect(fixedLayer.isDeclaredFixed(element3)).to.be.false; + expect(fixedLayer.isDeclaredSticky(element3)).to.be.false; + expect(fixedLayer.isDeclaredSticky(element4)).to.be.true; + expect(fixedLayer.isDeclaredSticky(element5)).to.be.true; }); it('should add and remove element directly', () => { const updateStub = sandbox.stub(fixedLayer, 'update'); - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); // Add. fixedLayer.addElement(element3, '*'); expect(updateStub).to.be.calledOnce; - expect(fixedLayer.fixedElements_).to.have.length(3); - const fe = fixedLayer.fixedElements_[2]; - expect(fe.id).to.equal('F2'); + expect(fixedLayer.elements_).to.have.length(6); + const fe = fixedLayer.elements_[5]; + expect(fe.id).to.equal('F5'); expect(fe.element).to.equal(element3); expect(fe.selectors).to.deep.equal(['*']); // Remove. fixedLayer.removeElement(element3); - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); - //Add with forceTransfer + // Add with forceTransfer. fixedLayer.addElement(element3, '*', true); expect(updateStub).to.have.callCount(2); - expect(fixedLayer.fixedElements_).to.have.length(3); - const fe1 = fixedLayer.fixedElements_[2]; - expect(fe1.id).to.equal('F3'); + expect(fixedLayer.elements_).to.have.length(6); + const fe1 = fixedLayer.elements_[5]; + expect(fe1.id).to.equal('F6'); expect(fe1.element).to.equal(element3); expect(fe1.selectors).to.deep.equal(['*']); expect(fe1.forceTransfer).to.be.true; // Remove. fixedLayer.removeElement(element3); - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); }); it('should remove node when disappeared from DOM', () => { docBody.removeChild(element1); - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); fixedLayer.update(); - expect(fixedLayer.fixedElements_).to.have.length(1); + expect(fixedLayer.elements_).to.have.length(4); + }); + + it('should remove all candidates', () => { + // element2 is both fixed and sticky. + docBody.removeChild(element2); + expect(fixedLayer.elements_).to.have.length(5); + fixedLayer.update(); + expect(fixedLayer.elements_).to.have.length(3); }); it('should collect updates', () => { element1.computedStyle['position'] = 'fixed'; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + // F0: element1 + expect(state['F0'].fixed).to.be.true; + expect(state['F0'].sticky).to.be.false; expect(state['F0'].top).to.equal(''); expect(state['F0'].zIndex).to.equal(''); + // F1: element2 expect(state['F1'].fixed).to.equal(false); + expect(state['F1'].sticky).to.equal(false); + + // F2: element3 + expect(state['F2'].fixed).to.equal(false); + expect(state['F2'].sticky).to.equal(false); + + // F3: element4 + expect(state['F3'].fixed).to.be.false; + expect(state['F3'].sticky).to.be.false; + expect(state['F3'].top).to.equal(''); + expect(state['F3'].zIndex).to.equal(''); + + // F4: element5 + expect(state['F4'].fixed).to.be.false; + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal(''); + expect(state['F4'].zIndex).to.equal(''); + }); + + it('should support vendor-based sticky', () => { + element5.computedStyle['position'] = '-webkit-sticky'; + + expect(vsyncTasks).to.have.length(1); + const state = {}; + vsyncTasks[0].measure(state); + + expect(state['F4'].sticky).to.be.true; }); it('should disregard non-fixed position', () => { element1.computedStyle['position'] = 'static'; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'static'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(false); - expect(state['F1'].fixed).to.equal(false); + expect(state['F0'].fixed).to.be.false; + expect(state['F1'].fixed).to.be.false; + expect(state['F4'].fixed).to.be.false; }); - it('should disregard invisible element', () => { + it('should disregard invisible element, but for fixed only', () => { element1.computedStyle['position'] = 'fixed'; element1.offsetWidth = 0; element1.offsetHeight = 0; + element5.computedStyle['position'] = 'sticky'; + element5.offsetWidth = 0; + element5.offsetHeight = 0; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(false); - expect(state['F1'].fixed).to.equal(false); + expect(state['F0'].fixed).to.be.false; + expect(state['F1'].fixed).to.be.false; + expect(state['F4'].sticky).to.be.true; }); it('should tolerate getComputedStyle = null', () => { @@ -408,12 +498,14 @@ describe('FixedLayer', () => { const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(false); - expect(state['F0'].transferrable).to.equal(false); + expect(state['F0'].fixed).to.be.false; + expect(state['F0'].sticky).to.be.false; + expect(state['F0'].transferrable).to.be.false; expect(state['F0'].top).to.equal(''); expect(state['F0'].zIndex).to.equal(''); - expect(state['F1'].fixed).to.equal(false); + expect(state['F1'].fixed).to.be.false; + expect(state['F1'].sticky).to.be.false; }); it('should collect for top != auto', () => { @@ -421,13 +513,20 @@ describe('FixedLayer', () => { element1.computedStyle['top'] = '11px'; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '11px'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; + expect(state['F0'].sticky).to.be.false; expect(state['F0'].top).to.equal('11px'); + + expect(state['F4'].fixed).to.be.false; + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal('11px'); }); it('should collect for top = auto, but not update top', () => { @@ -435,13 +534,20 @@ describe('FixedLayer', () => { element1.computedStyle['top'] = 'auto'; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = 'auto'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; + expect(state['F0'].sticky).to.be.false; expect(state['F0'].top).to.equal(''); + + expect(state['F4'].fixed).to.be.false; + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal(''); }); it('should collect for implicit top = auto, but not update top', () => { @@ -450,13 +556,19 @@ describe('FixedLayer', () => { element1.autoOffsetTop = 12; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '12px'; + element5.autoOffsetTop = 12; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; expect(state['F0'].top).to.equal(''); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal(''); }); it('should override implicit top = auto to 0 when equals padding', () => { @@ -465,13 +577,19 @@ describe('FixedLayer', () => { element1.autoOffsetTop = 11; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '11px'; + element5.autoOffsetTop = 11; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; expect(state['F0'].top).to.equal('0px'); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal('0px'); }); it('should override implicit top = auto to 0 w/transient padding', () => { @@ -480,6 +598,9 @@ describe('FixedLayer', () => { element1.autoOffsetTop = 11; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '11px'; + element5.autoOffsetTop = 11; expect(vsyncTasks).to.have.length(1); const state = {}; @@ -492,16 +613,20 @@ describe('FixedLayer', () => { sandbox.stub(fixedLayer, 'update', () => {}); fixedLayer.updatePaddingTop(22, /* transient */ true); vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; expect(state['F0'].top).to.equal('0px'); + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal('0px'); expect(fixedLayer.paddingTop_).to.equal(22); expect(fixedLayer.committedPaddingTop_).to.equal(11); // Update to non-transient padding. fixedLayer.updatePaddingTop(22, /* transient */ false); vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; expect(state['F0'].top).to.equal(''); // Reset completely. + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal(''); // Reset completely. expect(fixedLayer.paddingTop_).to.equal(22); expect(fixedLayer.committedPaddingTop_).to.equal(22); }); @@ -512,30 +637,52 @@ describe('FixedLayer', () => { element1.autoOffsetTop = 0; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '0px'; + element5.autoOffsetTop = 0; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); - expect(state['F0'].fixed).to.equal(true); + expect(state['F0'].fixed).to.be.true; expect(state['F0'].top).to.equal('0px'); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].top).to.equal('0px'); }); it('should mutate element to fixed without top', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, + sticky: false, top: '', }); expect(fe.fixedNow).to.be.true; + expect(fe.stickyNow).to.be.false; + expect(fe.element.style.top).to.equal(''); + expect(fixedLayer.transferLayer_).to.be.null; + }); + + it('should mutate element to sticky without top', () => { + const fe = fixedLayer.elements_[4]; + fixedLayer.mutateElement_(fe, 1, { + fixed: false, + sticky: true, + top: '', + }); + + expect(fe.fixedNow).to.be.false; + expect(fe.stickyNow).to.be.true; expect(fe.element.style.top).to.equal(''); - expect(fixedLayer.fixedLayer_).to.be.null; + expect(fixedLayer.transferLayer_).to.be.null; }); it('should mutate element to fixed with top', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, top: '17px', }); @@ -544,17 +691,28 @@ describe('FixedLayer', () => { expect(fe.element.style.top).to.equal('calc(17px + 11px)'); }); + it('should mutate element to sticky with top', () => { + const fe = fixedLayer.elements_[4]; + fixedLayer.mutateElement_(fe, 1, { + sticky: true, + top: '17px', + }); + + expect(fe.stickyNow).to.be.true; + expect(fe.element.style.top).to.equal('calc(17px + 11px)'); + }); + it('should reset top upon being removed from fixedlayer', () => { - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); // Add. fixedLayer.addElement(element3, '*'); - expect(fixedLayer.fixedElements_).to.have.length(3); - const fe = fixedLayer.fixedElements_[2]; - expect(fe.id).to.equal('F2'); + expect(fixedLayer.elements_).to.have.length(6); + const fe = fixedLayer.elements_[5]; + expect(fe.id).to.equal('F5'); expect(fe.element).to.equal(element3); expect(fe.selectors).to.deep.equal(['*']); - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: true, top: '17px', }); @@ -568,15 +726,38 @@ describe('FixedLayer', () => { }, }; fixedLayer.removeElement(element3); - expect(fixedLayer.fixedElements_).to.have.length(2); + expect(fixedLayer.elements_).to.have.length(5); expect(element3.style.top).to.equal(''); }); + it('should reset sticky top upon being removed from fixedlayer', () => { + expect(fixedLayer.elements_).to.have.length(5); + + const fe = fixedLayer.elements_[4]; + fixedLayer.mutateElement_(fe, 1, { + sticky: true, + top: '17px', + }); + + expect(fe.stickyNow).to.be.true; + expect(fe.element.style.top).to.equal('calc(17px + 11px)'); + + // Remove. + fixedLayer.vsync_ = { + mutate: function(callback) { + callback(); + }, + }; + fixedLayer.removeElement(element5); + expect(fixedLayer.elements_).to.have.length(4); + expect(element5.style.top).to.equal(''); + }); + it('should mutate element to non-fixed', () => { - const fe = fixedLayer.fixedElements_[0]; + const fe = fixedLayer.elements_[0]; fe.fixedNow = true; fe.element.style.top = '27px'; - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: false, top: '17px', }); @@ -585,9 +766,22 @@ describe('FixedLayer', () => { expect(fe.element.style.top).to.equal(''); }); + it('should mutate element to non-sticky', () => { + const fe = fixedLayer.elements_[4]; + fe.stickyNow = true; + fe.element.style.top = '27px'; + fixedLayer.mutateElement_(fe, 1, { + sticky: false, + top: '17px', + }); + + expect(fe.stickyNow).to.be.false; + expect(fe.element.style.top).to.equal(''); + }); + it('should transform fixed elements with anchored top', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, top: '17px', }); @@ -604,9 +798,28 @@ describe('FixedLayer', () => { expect(fe.element.style.transition).to.equal(''); }); + it('should NOT transform sticky elements with anchored top', () => { + const fe = fixedLayer.elements_[4]; + fixedLayer.mutateElement_(fe, 1, { + sticky: true, + top: '17px', + }); + expect(fe.stickyNow).to.be.true; + expect(fe.element.style.top).to.equal('calc(17px + 11px)'); + + fixedLayer.transformMutate('translateY(-10px)'); + expect(fe.element.style.transform).to.be.undefined; + expect(fe.element.style.transition).to.be.undefined; + + // Reset back. + fixedLayer.transformMutate(null); + expect(fe.element.style.transform).to.be.undefined; + expect(fe.element.style.transition).to.be.undefined; + }); + it('should compound transform with anchored top', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, top: '17px', transform: 'scale(2)', @@ -617,9 +830,9 @@ describe('FixedLayer', () => { }); it('should NOT transform fixed elements w/o anchored top', () => { - const fe = fixedLayer.fixedElements_[0]; + const fe = fixedLayer.elements_[0]; fe.element.style.transform = ''; - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: true, top: '', }); @@ -641,22 +854,26 @@ describe('FixedLayer', () => { it('should initiale fixed layer to null', () => { expect(fixedLayer.transfer_).to.be.true; - expect(fixedLayer.fixedLayer_).to.be.null; + expect(fixedLayer.transferLayer_).to.be.null; }); it('should collect turn off transferrable', () => { element1.computedStyle['position'] = 'fixed'; element1.offsetWidth = 10; element1.offsetHeight = 10; + element5.computedStyle['position'] = 'sticky'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(false); + expect(state['F0'].transferrable).to.be.false; expect(state['F1'].fixed).to.equal(false); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; }); it('should collect turn on transferrable with top = 0', () => { @@ -664,14 +881,20 @@ describe('FixedLayer', () => { element1.offsetWidth = 10; element1.offsetHeight = 10; element1.computedStyle['top'] = '0px'; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '0px'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(true); + expect(state['F0'].transferrable).to.be.true; expect(state['F0'].top).to.equal('0px'); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; + expect(state['F4'].top).to.equal('0px'); }); it('should collect turn off transferrable with top != 0', () => { @@ -679,14 +902,20 @@ describe('FixedLayer', () => { element1.offsetWidth = 10; element1.offsetHeight = 10; element1.computedStyle['top'] = '2px'; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['top'] = '2px'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(false); + expect(state['F0'].transferrable).to.be.false; expect(state['F0'].top).to.equal('2px'); + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; + expect(state['F4'].top).to.equal('2px'); }); it('should collect turn on transferrable with bottom = 0', () => { @@ -694,34 +923,42 @@ describe('FixedLayer', () => { element1.offsetWidth = 10; element1.offsetHeight = 10; element1.computedStyle['bottom'] = '0px'; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['bottom'] = '0px'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(true); + expect(state['F0'].transferrable).to.be.true; + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; }); it('should not disregard invisible element if it has forceTransfer', () => { element1.computedStyle['position'] = 'fixed'; element1.offsetWidth = 0; element1.offsetHeight = 0; + element5.computedStyle['position'] = 'sticky'; expect(vsyncTasks).to.have.length(1); let state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.false; - expect(state['F0'].transferrable).to.equal(false); + expect(state['F0'].transferrable).to.be.false; + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; // Add. state = {}; - fixedLayer.setupFixedElement_(element1, '*', true); + fixedLayer.setupElement_(element1, '*', 'fixed', true); expect(vsyncTasks).to.have.length(1); vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(true); + expect(state['F0'].transferrable).to.be.true; }); it('should collect turn off transferrable with bottom != 0', () => { @@ -729,20 +966,25 @@ describe('FixedLayer', () => { element1.offsetWidth = 10; element1.offsetHeight = 10; element1.computedStyle['bottom'] = '2px'; + element5.computedStyle['position'] = 'sticky'; + element5.computedStyle['bottom'] = '2px'; expect(vsyncTasks).to.have.length(1); const state = {}; vsyncTasks[0].measure(state); expect(state['F0'].fixed).to.be.true; - expect(state['F0'].transferrable).to.equal(false); + expect(state['F0'].transferrable).to.be.false; + + expect(state['F4'].sticky).to.be.true; + expect(state['F4'].transferrable).to.be.false; }); it('should collect z-index', () => { element1.computedStyle['position'] = 'fixed'; element1.offsetWidth = 10; element1.offsetHeight = 10; - element1.computedStyle['z-index'] = '101'; + element1.computedStyle['zIndex'] = '101'; expect(vsyncTasks).to.have.length(1); const state = {}; @@ -753,8 +995,8 @@ describe('FixedLayer', () => { }); it('should transfer element', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, transferrable: true, zIndex: '11', @@ -764,31 +1006,31 @@ describe('FixedLayer', () => { expect(fe.placeholder).to.exist; expect(fe.placeholder.style['display']).to.equal('none'); - expect(fe.element.parentElement).to.equal(fixedLayer.fixedLayer_); + expect(fe.element.parentElement).to.equal(fixedLayer.transferLayer_); expect(fe.element.style['pointer-events']).to.equal('initial'); expect(fe.element.style['zIndex']).to.equal('calc(10001 + 11)'); - expect(fixedLayer.fixedLayer_).to.exist; - expect(fixedLayer.fixedLayer_.style['pointerEvents']).to.equal('none'); + expect(fixedLayer.transferLayer_).to.exist; + expect(fixedLayer.transferLayer_.style['pointerEvents']).to.equal('none'); }); it('should ignore transfer when non-transferrable', () => { - const fe = fixedLayer.fixedElements_[0]; - fixedLayer.mutateFixedElement_(fe, 1, { + const fe = fixedLayer.elements_[0]; + fixedLayer.mutateElement_(fe, 1, { fixed: true, transferrable: false, }); expect(fe.fixedNow).to.be.true; expect(fe.placeholder).to.not.exist; - expect(fixedLayer.fixedLayer_).to.not.exist; - expect(fe.element.parentElement).to.not.equal(fixedLayer.fixedLayer_); + expect(fixedLayer.transferLayer_).to.not.exist; + expect(fe.element.parentElement).to.not.equal(fixedLayer.transferLayer_); }); it('should return transfered element if it no longer matches', () => { - const fe = fixedLayer.fixedElements_[0]; + const fe = fixedLayer.elements_[0]; fe.element.matches = () => false; - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: true, transferrable: true, zIndex: '11', @@ -796,30 +1038,30 @@ describe('FixedLayer', () => { expect(fe.fixedNow).to.be.true; expect(fe.placeholder).to.exist; - expect(fixedLayer.fixedLayer_).to.exist; - expect(fe.element.parentElement).to.not.equal(fixedLayer.fixedLayer_); + expect(fixedLayer.transferLayer_).to.exist; + expect(fe.element.parentElement).to.not.equal(fixedLayer.transferLayer_); expect(fe.placeholder.parentElement).to.be.null; expect(fe.element.style.zIndex).to.equal(''); }); it('should remove transfered element if it no longer exists', () => { - const fe = fixedLayer.fixedElements_[0]; + const fe = fixedLayer.elements_[0]; // Add. - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: true, transferrable: true, zIndex: '11', }); expect(fe.fixedNow).to.be.true; expect(fe.placeholder).to.exist; - expect(fe.element.parentElement).to.equal(fixedLayer.fixedLayer_); - expect(fixedLayer.fixedLayer_).to.exist; - expect(fixedLayer.fixedLayer_.id).to.equal('doc-body-id'); + expect(fe.element.parentElement).to.equal(fixedLayer.transferLayer_); + expect(fixedLayer.transferLayer_).to.exist; + expect(fixedLayer.transferLayer_.id).to.equal('doc-body-id'); // Remove from DOM. fe.element.parentElement.removeChild(fe.element); - fixedLayer.mutateFixedElement_(fe, 1, { + fixedLayer.mutateElement_(fe, 1, { fixed: false, transferrable: false, }); diff --git a/test/functional/test-log.js b/test/functional/test-log.js index 9afbc7247269d..1185a2dd22d6f 100644 --- a/test/functional/test-log.js +++ b/test/functional/test-log.js @@ -404,6 +404,15 @@ describe('Logging', () => { expect(message.indexOf(USER_ERROR_SENTINEL)).to.equal(-1); }); + it('should strip suffix if not available', () => { + const error = log.createError(new Error('test')); + expect(isUserErrorMessage(error.message)).to.be.true; + + const noSuffixLog = new Log(win, RETURNS_FINE); + noSuffixLog.createError(error); + expect(isUserErrorMessage(error.message)).to.be.false; + }); + it('should create other-suffixed errors', () => { log = new Log(win, RETURNS_FINE, '-other'); const error = log.createError('test'); diff --git a/test/functional/test-performance.js b/test/functional/test-performance.js index 76b0e000d6768..785f209111961 100644 --- a/test/functional/test-performance.js +++ b/test/functional/test-performance.js @@ -643,6 +643,38 @@ describe('performance', () => { }); }); }); +}); + +describes.fakeWin('performance with experiment', {amp: true}, env => { + + let win; + let perf; + let viewerSendMessageStub; + beforeEach(() => { + win = env.win; + const viewer = viewerForDoc(win.document); + viewerSendMessageStub = sandbox.stub(viewer, 'sendMessage'); + sandbox.stub(viewer, 'whenMessagingReady').returns(Promise.resolve()); + sandbox.stub(viewer, 'getParam').withArgs('csi').returns('1'); + sandbox.stub(viewer, 'isEmbedded').returns(true); + perf = installPerformanceService(win); + }); + it('legacy-cdn-domain experiment enabled', () => { + sandbox.stub(perf, 'getHostname_', () => 'cdn.ampproject.org'); + return perf.coreServicesAvailable().then(() => { + perf.flush(); + expect(viewerSendMessageStub) + .to.be.calledWith('sendCsi', {ampexp: 'legacy-cdn-domain'}); + }); + }); + + it('no experiment', () => { + sandbox.stub(perf, 'getHostname_', () => 'curls.cdn.ampproject.org'); + return perf.coreServicesAvailable().then(() => { + perf.flush(); + expect(viewerSendMessageStub).to.be.calledWith('sendCsi', undefined); + }); + }); }); diff --git a/test/functional/test-storage.js b/test/functional/test-storage.js index a7adb40902b7c..62e1f7e0323bd 100644 --- a/test/functional/test-storage.js +++ b/test/functional/test-storage.js @@ -478,7 +478,8 @@ describe('LocalStorageBinding', () => { }); }); - it('should reject on local storage failure', () => { + it('should reject on local storage failure w/ localStorage support', () => { + binding.isLocalStorageSupported_ = true; localStorageMock.expects('getItem') .withExactArgs('amp-store:https://acme.com') .throws(new Error('unknown')) @@ -502,6 +503,18 @@ describe('LocalStorageBinding', () => { }); }); + it('should bypass loading from localStorage if getItem throws', () => { + localStorageMock.expects('getItem') + .throws(new Error('unknown')) + .once(); + binding = new LocalStorageBinding(windowApi); + localStorageMock.expects('getItem').never(); + return binding.loadBlob('https://acme.com') + .then(() => 'SUCCESS', () => 'ERROR').then(res => { + expect(res).to.equal('SUCCESS'); + }); + }); + it('should save store', () => { localStorageMock.expects('setItem') .withExactArgs('amp-store:https://acme.com', 'BLOB1') @@ -532,6 +545,21 @@ describe('LocalStorageBinding', () => { expect(res).to.equal('SUCCESS'); }); }); + + it('should bypass saving to localStorage if getItem throws', () => { + const setItemSpy = sandbox.spy(windowApi.localStorage, 'setItem'); + + localStorageMock.expects('getItem') + .throws(new Error('unknown')) + .once(); + binding = new LocalStorageBinding(windowApi); + // Never reaches setItem + return binding.saveBlob('https://acme.com', 'BLOB1') + .then(() => 'SUCCESS', () => `ERROR`).then(res => { + expect(setItemSpy).to.have.not.been.called; + expect(res).to.equal('SUCCESS'); + }); + }); }); diff --git a/test/functional/test-viewport.js b/test/functional/test-viewport.js index eca4813fe885b..48eac6dd7cccf 100644 --- a/test/functional/test-viewport.js +++ b/test/functional/test-viewport.js @@ -27,10 +27,10 @@ import { updateViewportMetaString, } from '../../src/service/viewport-impl'; import {getMode} from '../../src/mode'; -import {getStyle} from '../../src/style'; import {installPlatformService} from '../../src/service/platform-impl'; import {installTimerService} from '../../src/service/timer-impl'; import {installViewerServiceForDoc} from '../../src/service/viewer-impl'; +import {installVsyncService} from '../../src/service/vsync-impl'; import {loadPromise} from '../../src/event-helper'; import {setParentWindow} from '../../src/service'; import {toggleExperiment} from '../../src/experiments'; @@ -990,188 +990,104 @@ describe('Viewport META', () => { }); -describe('ViewportBindingNatural', () => { - let sandbox; - let windowMock; +describes.realWin('ViewportBindingNatural', {ampCss: true}, env => { let binding; - let windowApi; - let documentElement; - let documentBody; - let windowEventHandlers; + let win; let viewer; - let viewerMock; beforeEach(() => { - sandbox = sinon.sandbox.create(); - const WindowApi = function() {}; - windowEventHandlers = {}; - WindowApi.prototype.addEventListener = function(eventType, handler) { - windowEventHandlers[eventType] = handler; - }; - WindowApi.prototype.removeEventListener = function(eventType, handler) { - if (windowEventHandlers[eventType] == handler) { - delete windowEventHandlers[eventType]; - } - }; - windowApi = new WindowApi(); + env.iframe.style.width = '100px'; + env.iframe.style.height = '200px'; + win = env.win; + const child = win.document.createElement('div'); + child.style.width = '200px'; + child.style.height = '300px'; + win.document.body.appendChild(child); - documentElement = { - style: {}, - }; - documentBody = { - nodeType: 1, - style: {}, - }; - windowApi.document = { - documentElement, - body: documentBody, - defaultView: windowApi, - }; - windowApi.navigator = {userAgent: ''}; - windowApi.location = {}; - windowMock = sandbox.mock(windowApi); - installPlatformService(windowApi); - viewer = { - isEmbedded: () => false, - getParam: param => { - if (param == 'paddingTop') { - return 19; - } - return undefined; - }, - onMessage: () => {}, - }; - viewerMock = sandbox.mock(viewer); - binding = new ViewportBindingNatural_(windowApi, viewer); + viewer = {}; + + installPlatformService(win); + + binding = new ViewportBindingNatural_(win, viewer); binding.connect(); }); afterEach(() => { - windowMock.verify(); - viewerMock.verify(); - sandbox.restore(); - toggleExperiment(windowApi, 'make-body-relative', false); + toggleExperiment(win, 'make-body-relative', false); + }); + + it('should setup overflow:visible on body', () => { + expect(win.document.body.style.overflow).to.equal('visible'); }); it('should configure make-body-relative', () => { - toggleExperiment(windowApi, 'make-body-relative', true); - binding = new ViewportBindingNatural_(windowApi, viewer); - expect(documentBody.style.display).to.be.undefined; - expect(documentBody.style.position).to.equal('relative'); + toggleExperiment(win, 'make-body-relative', true); + binding = new ViewportBindingNatural_(win, viewer); + expect(win.document.body.style.display).to.not.be.ok; + expect(win.document.body.style.position).to.equal('relative'); // It's important that this experiment does NOT override the previously // set `overflow`. - expect(documentBody.style.overflow).to.equal('visible'); - expect(documentBody.style.overflowY).to.not.be.ok; - expect(documentBody.style.overflowX).to.not.be.ok; - }); - - it('should setup overflow:visible on body', () => { - expect(documentBody.style.overflow).to.equal('visible'); + expect(win.document.body.style.overflow).to.equal('visible'); }); it('should NOT require fixed layer transferring', () => { expect(binding.requiresFixedLayerTransfer()).to.be.false; }); - it('should subscribe to scroll and resize events', () => { - expect(windowEventHandlers['scroll']).to.not.equal(undefined); - expect(windowEventHandlers['resize']).to.not.equal(undefined); + it('should connect events: subscribe to scroll and resize events', () => { + expect(win.eventListeners.count('resize')).to.equal(1); + expect(win.eventListeners.count('scroll')).to.equal(1); }); - it('should connect/disconnect events', () => { - windowEventHandlers = {}; - binding = new ViewportBindingNatural_(windowApi, viewer); - expect(Object.keys(windowEventHandlers)).to.have.length(0); - - binding.connect(); - expect(windowEventHandlers['scroll']).to.not.equal(undefined); - expect(windowEventHandlers['resize']).to.not.equal(undefined); - + it('should disconnect events', () => { // After disconnect, there are no more listeners on window. binding.disconnect(); - expect(Object.keys(windowEventHandlers)).to.have.length(0); + expect(win.eventListeners.count('resize')).to.equal(0); + expect(win.eventListeners.count('scroll')).to.equal(0); }); it('should update padding', () => { - windowApi.document = { - documentElement: {style: {}}, - }; binding.updatePaddingTop(31); - expect(windowApi.document.documentElement.style.paddingTop).to - .equal('31px'); + expect(win.document.documentElement.style.paddingTop).to.equal('31px'); }); it('should calculate size', () => { - windowApi.innerWidth = 111; - windowApi.innerHeight = 222; - windowApi.document = { - documentElement: { - clientWidth: 333, - clientHeight: 444, - }, - }; - let size = binding.getSize(); - expect(size.width).to.equal(111); - expect(size.height).to.equal(222); - - delete windowApi.innerWidth; - delete windowApi.innerHeight; - size = binding.getSize(); - expect(size.width).to.equal(333); - expect(size.height).to.equal(444); + const size = binding.getSize(); + expect(size.width).to.equal(100); + expect(size.height).to.equal(200); }); it('should calculate scrollTop from scrollElement', () => { - windowApi.pageYOffset = 11; - windowApi.document = { - scrollingElement: { - scrollTop: 17, - }, - }; + win.pageYOffset = 11; + win.document.scrollingElement.scrollTop = 17; expect(binding.getScrollTop()).to.equal(17); }); it('should calculate scrollWidth from scrollElement', () => { - windowApi.pageYOffset = 11; - windowApi.document = { - scrollingElement: { - scrollWidth: 117, - }, - }; - expect(binding.getScrollWidth()).to.equal(117); + expect(binding.getScrollWidth()).to.equal(200); }); it('should calculate scrollHeight from scrollElement', () => { - windowApi.pageYOffset = 11; - windowApi.document = { - scrollingElement: { - scrollHeight: 119, - }, - }; - expect(binding.getScrollHeight()).to.equal(119); + expect(binding.getScrollHeight()).to.equal(300); }); it('should update scrollTop on scrollElement', () => { - windowApi.pageYOffset = 11; - windowApi.document = { - scrollingElement: { - scrollTop: 17, - }, - }; + win.pageYOffset = 11; + win.document.scrollingElement.scrollTop = 17; binding.setScrollTop(21); - expect(windowApi.document.scrollingElement./*OK*/scrollTop).to.equal(21); + expect(win.document.scrollingElement./*OK*/scrollTop).to.equal(21); }); it('should fallback scrollTop to pageYOffset', () => { - windowApi.pageYOffset = 11; - windowApi.document = {scrollingElement: {}}; + win.pageYOffset = 11; + delete win.document.scrollingElement.scrollTop; expect(binding.getScrollTop()).to.equal(11); }); it('should offset client rect for layout', () => { - windowApi.pageXOffset = 0; - windowApi.pageYOffset = 200; - windowApi.document = {scrollingElement: {}}; + win.pageXOffset = 0; + win.pageYOffset = 200; + delete win.document.scrollingElement; const el = { getBoundingClientRect: () => { return {left: 11.5, top: 12.5, width: 13.5, height: 14.5}; @@ -1185,9 +1101,9 @@ describe('ViewportBindingNatural', () => { }); it('should offset client rect for layout and position passed in', () => { - windowApi.pageXOffset = 0; - windowApi.pageYOffset = 2000; - windowApi.document = {scrollingElement: {}}; + win.pageXOffset = 0; + win.pageYOffset = 2000; + delete win.document.scrollingElement; const el = { getBoundingClientRect: () => { return {left: 11.5, top: 12.5, width: 13.5, height: 14.5}; @@ -1199,64 +1115,53 @@ describe('ViewportBindingNatural', () => { expect(rect.width).to.equal(14); // round(13.5) expect(rect.height).to.equal(15); // round(14.5) }); -}); -describe('ViewportBindingNaturalIosEmbed', () => { - let sandbox; - let windowMock; + it('should disable scroll temporarily and reset scroll', () => { + let htmlCss = win.getComputedStyle(win.document.documentElement); + expect(htmlCss.overflowX).to.equal('hidden'); + expect(htmlCss.overflowY).to.equal('auto'); + + binding.disableScroll(); + + expect(win.document.documentElement).to.have.class( + 'i-amphtml-scroll-disabled'); + htmlCss = win.getComputedStyle(win.document.documentElement); + expect(htmlCss.overflowX).to.equal('hidden'); + expect(htmlCss.overflowY).to.equal('hidden'); + + binding.resetScroll(); + + expect(win.document.documentElement).to.not.have.class( + 'i-amphtml-scroll-disabled'); + htmlCss = win.getComputedStyle(win.document.documentElement); + expect(htmlCss.overflowX).to.equal('hidden'); + expect(htmlCss.overflowY).to.equal('auto'); + }); +}); + +describes.realWin('ViewportBindingNaturalIosEmbed', {}, env => { let binding; - let windowApi; - let windowEventHandlers; - let bodyEventListeners; - let bodyChildren; + let win; beforeEach(() => { - sandbox = sinon.sandbox.create(); - const WindowApi = function() {}; - windowEventHandlers = {}; - bodyEventListeners = {}; - bodyChildren = []; - WindowApi.prototype.addEventListener = function(eventType, handler) { - windowEventHandlers[eventType] = handler; - }; - windowApi = new WindowApi(); - windowApi.innerWidth = 555; - windowApi.document = { - nodeType: /* DOCUMENT */ 9, - readyState: 'complete', - documentElement: {style: {}}, - body: { - nodeType: 1, - scrollWidth: 777, - scrollHeight: 999, - style: {}, - appendChild: child => { - bodyChildren.push(child); - }, - addEventListener: (eventType, handler) => { - bodyEventListeners[eventType] = handler; - }, - }, - createElement: tagName => { - return { - tagName, - id: '', - style: {}, - scrollIntoView: sandbox.spy(), - }; - }, - }; - windowApi.document.defaultView = windowApi; - windowMock = sandbox.mock(windowApi); - binding = new ViewportBindingNaturalIosEmbed_(windowApi, - new AmpDocSingle(windowApi)); - return Promise.resolve(); - }); + env.iframe.style.width = '100px'; + env.iframe.style.height = '200px'; + win = env.win; + const child = win.document.createElement('div'); + child.id = 'child'; + child.style.width = '200px'; + child.style.height = '300px'; + win.document.body.appendChild(child); + const ampdocService = installDocService(win, /* isSingleDoc */ true); + const ampdoc = ampdocService.getAmpDoc(); - afterEach(() => { - windowMock.verify(); - sandbox.restore(); + installPlatformService(win); + installViewerServiceForDoc(ampdoc); + installVsyncService(win); + + binding = new ViewportBindingNaturalIosEmbed_(win, ampdoc); + return Promise.resolve(); }); it('should require fixed layer transferring', () => { @@ -1264,131 +1169,119 @@ describe('ViewportBindingNaturalIosEmbed', () => { }); it('should subscribe to resize events on window, scroll on body', () => { - expect(windowEventHandlers['resize']).to.not.equal(undefined); - expect(windowEventHandlers['scroll']).to.equal(undefined); - expect(bodyEventListeners['scroll']).to.not.equal(undefined); - }); - - it('should always have scrollWidth equal window.innerWidth', () => { - expect(binding.getScrollWidth()).to.equal(555); + expect(win.eventListeners.count('resize')).to.equal(1); + expect(win.eventListeners.count('scroll')).to.equal(0); + expect(win.document.body.eventListeners.count('scroll')).to.equal(1); }); it('should setup document for embed scrolling', () => { - const documentElement = windowApi.document.documentElement; - const body = windowApi.document.body; + const documentElement = win.document.documentElement; + const body = win.document.body; expect(documentElement.style.overflowY).to.equal('auto'); expect(documentElement.style.webkitOverflowScrolling).to.equal('touch'); expect(body.style.overflowX).to.equal('hidden'); expect(body.style.overflowY).to.equal('auto'); expect(body.style.webkitOverflowScrolling).to.equal('touch'); expect(body.style.position).to.equal('absolute'); - expect(body.style.top).to.equal(0); - expect(body.style.left).to.equal(0); - expect(body.style.right).to.equal(0); - expect(body.style.bottom).to.equal(0); - - expect(bodyChildren.length).to.equal(3); - - expect(bodyChildren[0].id).to.equal('i-amphtml-scrollpos'); - expect(bodyChildren[0].style.position).to.equal('absolute'); - expect(bodyChildren[0].style.top).to.equal(0); - expect(bodyChildren[0].style.left).to.equal(0); - expect(bodyChildren[0].style.width).to.equal(0); - expect(bodyChildren[0].style.height).to.equal(0); - expect(bodyChildren[0].style.visibility).to.equal('hidden'); - - expect(bodyChildren[1].id).to.equal('i-amphtml-scrollmove'); - expect(bodyChildren[1].style.position).to.equal('absolute'); - expect(bodyChildren[1].style.top).to.equal(0); - expect(bodyChildren[1].style.left).to.equal(0); - expect(bodyChildren[1].style.width).to.equal(0); - expect(bodyChildren[1].style.height).to.equal(0); - expect(bodyChildren[1].style.visibility).to.equal('hidden'); - - expect(bodyChildren[2].id).to.equal('i-amphtml-endpos'); - expect(bodyChildren[2].style.position).to.be.undefined; - expect(bodyChildren[2].style.top).to.be.undefined; - expect(bodyChildren[2].style.width).to.equal(0); - expect(bodyChildren[2].style.height).to.equal(0); - expect(bodyChildren[2].style.visibility).to.equal('hidden'); - }); - - it('should update border on BODY', () => { - windowApi.document = { - body: { - nodeType: 1, - style: {}, - }, - }; + expect(body.style.top).to.equal('0px'); + expect(body.style.left).to.equal('0px'); + expect(body.style.right).to.equal('0px'); + expect(body.style.bottom).to.equal('0px'); + + const scrollpos = body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos).to.be.ok; + expect(scrollpos.style.position).to.equal('absolute'); + expect(scrollpos.style.top).to.equal('0px'); + expect(scrollpos.style.left).to.equal('0px'); + expect(scrollpos.style.width).to.equal('0px'); + expect(scrollpos.style.height).to.equal('0px'); + expect(scrollpos.style.visibility).to.equal('hidden'); + + const scrollmove = body.querySelector('#i-amphtml-scrollmove'); + expect(scrollmove).to.be.ok; + expect(scrollmove.style.position).to.equal('absolute'); + expect(scrollmove.style.top).to.equal('0px'); + expect(scrollmove.style.left).to.equal('0px'); + expect(scrollmove.style.width).to.equal('0px'); + expect(scrollmove.style.height).to.equal('0px'); + expect(scrollmove.style.visibility).to.equal('hidden'); + + const endpos = body.querySelector('#i-amphtml-endpos'); + expect(endpos).to.be.ok; + expect(endpos.style.position).to.not.be.ok; + expect(endpos.style.top).to.not.be.ok; + expect(endpos.style.width).to.equal('0px'); + expect(endpos.style.height).to.equal('0px'); + expect(endpos.style.visibility).to.equal('hidden'); + expect(endpos.getBoundingClientRect().top).to.equal(300); + }); + + it('should always have scrollWidth equal window.innerWidth', () => { + expect(binding.getScrollWidth()).to.equal(100); + }); + + it('should update border on BODY when updatePaddingTop', () => { binding.updatePaddingTop(31); - expect(windowApi.document.body.style.borderTop).to + expect(win.document.body.style.borderTop).to .equal('31px solid transparent'); + expect(win.document.body.style.paddingTop).to.not.be.ok; }); - it('should update border in lightbox mode', () => { - windowApi.document = { - body: { - nodeType: 1, - style: {}, - }, - }; + it('should update border in lightbox mode when updateLightboxMode', () => { binding.updatePaddingTop(31); - expect(windowApi.document.body.style.borderTop).to + expect(win.document.body.style.borderTop).to .equal('31px solid transparent'); - expect(windowApi.document.body.style.borderTopStyle).to.be.undefined; + expect(win.document.body.style.borderTopStyle).to.equal('solid'); binding.updateLightboxMode(true); - expect(windowApi.document.body.style.borderTopStyle).to.equal('none'); + expect(win.document.body.style.borderTopStyle).to.equal('none'); binding.updateLightboxMode(false); - expect(windowApi.document.body.style.borderTopStyle).to.equal('solid'); - expect(windowApi.document.body.style.borderBottomStyle).to.not.equal( - 'solid'); - expect(windowApi.document.body.style.borderLeftStyle).to.not.equal('solid'); - expect(windowApi.document.body.style.borderRightStyle).to.not.equal( - 'solid'); + expect(win.document.body.style.borderTopStyle).to.equal('solid'); + expect(win.document.body.style.borderBottomStyle).to.not.equal('solid'); + expect(win.document.body.style.borderLeftStyle).to.not.equal('solid'); + expect(win.document.body.style.borderRightStyle).to.not.equal('solid'); }); it('should calculate size', () => { - windowApi.innerWidth = 111; - windowApi.innerHeight = 222; const size = binding.getSize(); - expect(size.width).to.equal(111); - expect(size.height).to.equal(222); + expect(size.width).to.equal(100); + expect(size.height).to.equal(200); }); it('should calculate scrollTop from scrollpos element', () => { - bodyChildren[0].getBoundingClientRect = () => { - return {top: -17, left: -11}; - }; + binding.setScrollTop(17); + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-17); binding.onScrolled_(); expect(binding.getScrollTop()).to.equal(17); }); it('should calculate scrollTop from scrollpos element with padding', () => { - bodyChildren[0].getBoundingClientRect = () => { - return {top: 0, left: -11}; - }; + binding.setScrollTop(17); + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-17); binding.updatePaddingTop(10); binding.onScrolled_(); // scrollTop = - BCR.top + paddingTop - expect(binding.getScrollTop()).to.equal(10); + expect(binding.getScrollTop()).to.equal(27); }); it('should calculate scrollHeight from scrollpos/endpos elements', () => { - bodyChildren[0].getBoundingClientRect = () => { - return {top: -17, left: -11}; - }; - bodyChildren[2].getBoundingClientRect = () => { - return {top: 100, left: -11}; - }; - expect(binding.getScrollHeight()).to.equal(117); + binding.setScrollTop(17); + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-17); + const endpos = win.document.body.querySelector('#i-amphtml-endpos'); + expect(endpos.getBoundingClientRect().top).to.equal(283); // 300 - 17 + expect(binding.getScrollHeight()).to.equal(300); // 283 - (-17) }); + it('should offset client rect for layout', () => { - bodyChildren[0].getBoundingClientRect = () => { - return {top: -200, left: -100}; - }; + binding.setScrollTop(100); + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-100); + expect(scrollpos.getBoundingClientRect().left).to.equal(0); binding.onScrolled_(); const el = { getBoundingClientRect: () => { @@ -1396,81 +1289,74 @@ describe('ViewportBindingNaturalIosEmbed', () => { }, }; const rect = binding.getLayoutRect(el); - expect(rect.left).to.equal(112); // round(100 + 11.5) - expect(rect.top).to.equal(213); // round(200 + 12.5) + expect(rect.left).to.equal(12); // round(0 + 11.5) + expect(rect.top).to.equal(113); // round(100 + 12.5) expect(rect.width).to.equal(14); // round(13.5) expect(rect.height).to.equal(15); // round(14.5) }); it('should set scroll position via moving element', () => { - const moveEl = bodyChildren[1]; binding.setScrollTop(10); - expect(getStyle(moveEl, 'transform')).to.equal('translateY(10px)'); - expect(moveEl.scrollIntoView).to.be.calledOnce; - expect(moveEl.scrollIntoView.firstCall.args[0]).to.equal(true); + const scrollmove = win.document.body.querySelector('#i-amphtml-scrollmove'); + expect(scrollmove.style.transform).to.equal('translateY(10px)'); }); it('should set scroll position via moving element with padding', () => { binding.updatePaddingTop(19); - const moveEl = bodyChildren[1]; binding.setScrollTop(10); + const scrollmove = win.document.body.querySelector('#i-amphtml-scrollmove'); // transform = scrollTop - paddingTop - expect(getStyle(moveEl, 'transform')).to.equal('translateY(-9px)'); - expect(moveEl.scrollIntoView).to.be.calledOnce; - expect(moveEl.scrollIntoView.firstCall.args[0]).to.equal(true); + expect(scrollmove.style.transform).to.equal('translateY(-9px)'); }); it('should adjust scroll position when scrolled to 0', () => { - const posEl = bodyChildren[0]; - posEl.getBoundingClientRect = () => {return {top: 0, left: 0};}; - const moveEl = bodyChildren[1]; + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(0); + const scrollmove = win.document.body.querySelector('#i-amphtml-scrollmove'); const event = {preventDefault: sandbox.spy()}; binding.adjustScrollPos_(event); - expect(getStyle(moveEl, 'transform')).to.equal('translateY(1px)'); - expect(moveEl.scrollIntoView).to.be.calledOnce; - expect(moveEl.scrollIntoView.firstCall.args[0]).to.equal(true); + expect(scrollmove.style.transform).to.equal('translateY(1px)'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-1); expect(event.preventDefault).to.be.calledOnce; }); it('should adjust scroll position when scrolled to 0 w/padding', () => { binding.updatePaddingTop(10); - const posEl = bodyChildren[0]; - posEl.getBoundingClientRect = () => {return {top: 10, left: 0};}; - const moveEl = bodyChildren[1]; + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(10); + const scrollmove = win.document.body.querySelector('#i-amphtml-scrollmove'); const event = {preventDefault: sandbox.spy()}; binding.adjustScrollPos_(event); // transform = 1 - updatePadding - expect(getStyle(moveEl, 'transform')).to.equal('translateY(-9px)'); - expect(moveEl.scrollIntoView).to.be.calledOnce; - expect(moveEl.scrollIntoView.firstCall.args[0]).to.equal(true); + expect(scrollmove.style.transform).to.equal('translateY(-9px)'); + // scroll pos should not change + expect(scrollpos.getBoundingClientRect().top).to.equal(10); expect(event.preventDefault).to.be.calledOnce; }); it('should adjust scroll position when scrolled to 0; w/o event', () => { - const posEl = bodyChildren[0]; - posEl.getBoundingClientRect = () => {return {top: 0, left: 0};}; - const moveEl = bodyChildren[1]; + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(0); + const scrollmove = win.document.body.querySelector('#i-amphtml-scrollmove'); binding.adjustScrollPos_(); - expect(moveEl.scrollIntoView).to.be.calledOnce; + expect(scrollmove.style.transform).to.equal('translateY(1px)'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-1); }); it('should NOT adjust scroll position when scrolled away from 0', () => { - const posEl = bodyChildren[0]; - posEl.getBoundingClientRect = () => {return {top: -10, left: 0};}; - const moveEl = bodyChildren[1]; + binding.setScrollTop(10); + const scrollpos = win.document.body.querySelector('#i-amphtml-scrollpos'); + expect(scrollpos.getBoundingClientRect().top).to.equal(-10); const event = {preventDefault: sandbox.spy()}; binding.adjustScrollPos_(event); - expect(moveEl.scrollIntoView).to.have.not.been.called; + expect(scrollpos.getBoundingClientRect().top).to.equal(-10); expect(event.preventDefault).to.have.not.been.called; }); it('should NOT adjust scroll position when overscrolled', () => { - const posEl = bodyChildren[0]; - posEl.getBoundingClientRect = () => {return {top: 10, left: 0};}; - const moveEl = bodyChildren[1]; + binding.setScrollTop(310); const event = {preventDefault: sandbox.spy()}; binding.adjustScrollPos_(event); - expect(moveEl.scrollIntoView).to.have.not.been.called; expect(event.preventDefault).to.have.not.been.called; }); }); @@ -1566,7 +1452,7 @@ describes.realWin('ViewportBindingIosEmbedWrapper', {ampCss: true}, env => { expect(binding.wrapper_.scrollTop).to.equal(1); }); - it('should subscribe to scroll and resize events', () => { + it('should connect events: subscribe to scroll and resize events', () => { expect(win.eventListeners.count('resize')).to.equal(1); // Note that scroll event is on the wrapper, and NOT on root or body. expect(win.eventListeners.count('scroll')).to.equal(0); @@ -1577,7 +1463,7 @@ describes.realWin('ViewportBindingIosEmbedWrapper', {ampCss: true}, env => { .to.equal(0); }); - it('should connect/disconnect events', () => { + it('should disconnect events', () => { // After disconnect, there are no more listeners on window. binding.disconnect(); expect(win.eventListeners.count('resize')).to.equal(0); @@ -1658,36 +1544,58 @@ describes.realWin('ViewportBindingIosEmbedWrapper', {ampCss: true}, env => { expect(binding.getScrollTop()).to.equal(11); }); }); + + it('should disable scroll temporarily and reset scroll', () => { + let wrapperCss = win.getComputedStyle(binding.wrapper_); + expect(wrapperCss.overflowX).to.equal('hidden'); + expect(wrapperCss.overflowY).to.equal('auto'); + + binding.disableScroll(); + + expect(binding.wrapper_).to.have.class('i-amphtml-scroll-disabled'); + wrapperCss = win.getComputedStyle(binding.wrapper_); + expect(wrapperCss.overflowX).to.equal('hidden'); + expect(wrapperCss.overflowY).to.equal('hidden'); + + binding.resetScroll(); + + expect(binding.wrapper_).to.not.have.class( + 'i-amphtml-scroll-disabled'); + wrapperCss = win.getComputedStyle(binding.wrapper_); + expect(wrapperCss.overflowX).to.equal('hidden'); + expect(wrapperCss.overflowY).to.equal('auto'); + }); }); describe('createViewport', () => { - describes.fakeWin('in Android', {win: {navigator: {userAgent: 'Android'}}}, - env => { - let win; - - beforeEach(() => { - win = env.win; - installPlatformService(win); - installTimerService(win); - }); - - it('should bind to "natural" when not iframed', () => { - win.parent = win; - const ampDoc = installDocService(win, true).getAmpDoc(); - installViewerServiceForDoc(ampDoc); - const viewport = installViewportServiceForDoc(ampDoc); - expect(viewport.binding_).to.be.instanceof(ViewportBindingNatural_); - }); - - it('should bind to "naturual" when iframed', () => { - win.parent = {}; - const ampDoc = installDocService(win, true).getAmpDoc(); - installViewerServiceForDoc(ampDoc); - const viewport = installViewportServiceForDoc(ampDoc); - expect(viewport.binding_).to.be.instanceof(ViewportBindingNatural_); - }); - }); + describes.fakeWin('in Android', { + win: {navigator: {userAgent: 'Android'}}, + }, env => { + let win; + + beforeEach(() => { + win = env.win; + installPlatformService(win); + installTimerService(win); + }); + + it('should bind to "natural" when not iframed', () => { + win.parent = win; + const ampDoc = installDocService(win, true).getAmpDoc(); + installViewerServiceForDoc(ampDoc); + const viewport = installViewportServiceForDoc(ampDoc); + expect(viewport.binding_).to.be.instanceof(ViewportBindingNatural_); + }); + + it('should bind to "naturual" when iframed', () => { + win.parent = {}; + const ampDoc = installDocService(win, true).getAmpDoc(); + installViewerServiceForDoc(ampDoc); + const viewport = installViewportServiceForDoc(ampDoc); + expect(viewport.binding_).to.be.instanceof(ViewportBindingNatural_); + }); + }); describes.fakeWin('in iOS', { win: {navigator: {userAgent: 'iPhone'}}, diff --git a/test/functional/web-worker/test-amp-worker.js b/test/functional/web-worker/test-amp-worker.js index c9a7b9e16e8d3..f6b59e219e72f 100644 --- a/test/functional/web-worker/test-amp-worker.js +++ b/test/functional/web-worker/test-amp-worker.js @@ -19,6 +19,8 @@ import { invokeWebWorker, ampWorkerForTesting, } from '../../../src/web-worker/amp-worker'; +import {installXhrService} from '../../../src/service/xhr-impl'; +import {xhrFor} from '../../../src/xhr'; import {toggleExperiment} from '../../../src/experiments'; import * as sinon from 'sinon'; @@ -27,6 +29,7 @@ describe('invokeWebWorker', () => { let fakeWin; let postMessageStub; let fakeWorker; + let workerReadyPromise; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -36,9 +39,21 @@ describe('invokeWebWorker', () => { fakeWorker = {}; fakeWorker.postMessage = postMessageStub; - const fakeWorkerClass = () => fakeWorker; - fakeWin = {Worker: fakeWorkerClass}; + // Fake Worker constructor just returns our `fakeWorker` instance. + fakeWin = { + Worker: () => fakeWorker, + Blob: sandbox.stub(), + URL: {createObjectURL: sandbox.stub()}, + }; + + // Stub xhr.fetchText() to return a resolved promise. + installXhrService(fakeWin); + sandbox.stub(xhrFor(fakeWin), 'fetchText', () => Promise.resolve()); + + const ampWorker = ampWorkerForTesting(fakeWin); + workerReadyPromise = ampWorker.fetchPromiseForTesting(); + // Enable 'web-worker' experiment on `fakeWin`. toggleExperiment( fakeWin, 'web-worker', @@ -68,21 +83,26 @@ describe('invokeWebWorker', () => { it('should send and receive a message', () => { // Sending. - const promise = invokeWebWorker(fakeWin, 'foo', ['bar', 123]); - expect(postMessageStub).to.have.been.calledWithMatch({ - method: 'foo', - args: sinon.match(['bar', 123]), - id: 0, - }); - // Receiving. - const data = { - method: 'foo', - returnValue: {'qux': 456}, - id: 0, - }; - fakeWorker.onmessage({data}); - return promise.then(returnValue => { - expect(returnValue).to.deep.equals({'qux': 456}); + const invokePromise = invokeWebWorker(fakeWin, 'foo', ['bar', 123]); + + return workerReadyPromise.then(() => { + expect(postMessageStub).to.have.been.calledWithMatch({ + method: 'foo', + args: sinon.match(['bar', 123]), + id: 0, + }); + + // Receiving. + const data = { + method: 'foo', + returnValue: {'qux': 456}, + id: 0, + }; + fakeWorker.onmessage({data}); + + return invokePromise.then(returnValue => { + expect(returnValue).to.deep.equals({'qux': 456}); + }); }); }); @@ -91,91 +111,100 @@ describe('invokeWebWorker', () => { const bar = invokeWebWorker(fakeWin, 'bar', ['bar-arg']); const qux = invokeWebWorker(fakeWin, 'qux', ['qux-arg']); - fakeWorker.onmessage({data: { - method: 'bar', - returnValue: 'bar-retVal', - id: 1, - }}); - fakeWorker.onmessage({data: { - method: 'qux', - returnValue: 'qux-retVal', - id: 2, - }}); - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: 'foo-retVal', - id: 0, - }}); - - return Promise.all([foo, bar, qux]).then(values => { - expect(values[0]).to.equal('foo-retVal'); - expect(values[1]).to.equal('bar-retVal'); - expect(values[2]).to.equal('qux-retVal'); + return workerReadyPromise.then(() => { + fakeWorker.onmessage({data: { + method: 'bar', + returnValue: 'bar-retVal', + id: 1, + }}); + fakeWorker.onmessage({data: { + method: 'qux', + returnValue: 'qux-retVal', + id: 2, + }}); + fakeWorker.onmessage({data: { + method: 'foo', + returnValue: 'foo-retVal', + id: 0, + }}); + + return Promise.all([foo, bar, qux]).then(values => { + expect(values[0]).to.equal('foo-retVal'); + expect(values[1]).to.equal('bar-retVal'); + expect(values[2]).to.equal('qux-retVal'); + }); }); }); it('should differentiate messages of same method with different ids', () => { const one = invokeWebWorker(fakeWin, 'foo', ['one']); - expect(postMessageStub).to.have.been.calledWithMatch({ - method: 'foo', - id: 0, - }); const two = invokeWebWorker(fakeWin, 'foo', ['two']); - expect(postMessageStub).to.have.been.calledWithMatch({ - method: 'foo', - id: 1, - }); const three = invokeWebWorker(fakeWin, 'foo', ['three']); - expect(postMessageStub).to.have.been.calledWithMatch({ - method: 'foo', - id: 2, - }); - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: 'three', - id: 2, - }}); - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: 'one', - id: 0, - }}); - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: 'two', - id: 1, - }}); - - return Promise.all([one, two, three]).then(values => { - expect(values[0]).to.equal('one'); - expect(values[1]).to.equal('two'); - expect(values[2]).to.equal('three'); + return workerReadyPromise.then(() => { + expect(postMessageStub.firstCall).to.have.been.calledWithMatch({ + method: 'foo', + id: 0, + }); + expect(postMessageStub.secondCall).to.have.been.calledWithMatch({ + method: 'foo', + id: 1, + }); + expect(postMessageStub.thirdCall).to.have.been.calledWithMatch({ + method: 'foo', + id: 2, + }); + + fakeWorker.onmessage({data: { + method: 'foo', + returnValue: 'three', + id: 2, + }}); + fakeWorker.onmessage({data: { + method: 'foo', + returnValue: 'one', + id: 0, + }}); + fakeWorker.onmessage({data: { + method: 'foo', + returnValue: 'two', + id: 1, + }}); + + return Promise.all([one, two, three]).then(values => { + expect(values[0]).to.equal('one'); + expect(values[1]).to.equal('two'); + expect(values[2]).to.equal('three'); + }); }); }); it('should log error when unexpected message is received', () => { const errorStub = sandbox.stub(dev(), 'error'); + invokeWebWorker(fakeWin, 'foo'); - expect(errorStub.callCount).to.equal(0); - - // Unexpected `id` value. - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: undefined, - id: 3, - }}); - expect(errorStub.callCount).to.equal(1); - expect(errorStub).to.have.been.calledWith('web-worker'); - - // Unexpected method at valid `id`. - expect(() => { + + return workerReadyPromise.then(() => { + expect(errorStub.callCount).to.equal(0); + + // Unexpected `id` value. fakeWorker.onmessage({data: { - method: 'bar', + method: 'foo', returnValue: undefined, - id: 0, + id: 3, }}); - }).to.throw('mismatched method'); + expect(errorStub.callCount).to.equal(1); + expect(errorStub).to.have.been.calledWith('web-worker'); + + // Unexpected method at valid `id`. + expect(() => { + fakeWorker.onmessage({data: { + method: 'bar', + returnValue: undefined, + id: 0, + }}); + }).to.throw('mismatched method'); + }); }); it('should clean up storage after message completion', () => { @@ -183,14 +212,16 @@ describe('invokeWebWorker', () => { invokeWebWorker(fakeWin, 'foo'); - expect(ampWorker.hasPendingMessages()).to.be.true; + return workerReadyPromise.then(() => { + expect(ampWorker.hasPendingMessages()).to.be.true; - fakeWorker.onmessage({data: { - method: 'foo', - returnValue: 'abc', - id: 0, - }}); + fakeWorker.onmessage({data: { + method: 'foo', + returnValue: 'abc', + id: 0, + }}); - expect(ampWorker.hasPendingMessages()).to.be.false; + expect(ampWorker.hasPendingMessages()).to.be.false; + }); }); }); diff --git a/test/integration/test-example-validation.js b/test/integration/test-example-validation.js index 23ea2bb60d8be..b17601438b73c 100644 --- a/test/integration/test-example-validation.js +++ b/test/integration/test-example-validation.js @@ -74,7 +74,6 @@ describe.configure().retryOnSaucelabs().run('example', function() { * @constructor {!Array} */ const errorWhitelist = [ - /GENERAL_DISALLOWED_TAG script viewer-integr.js/, /DISALLOWED_TAG content/, // Experiments with shadow slots ]; diff --git a/tools/errortracker/app.yaml b/tools/errortracker/app.yaml index 8ecc61141db62..effab55bef2da 100644 --- a/tools/errortracker/app.yaml +++ b/tools/errortracker/app.yaml @@ -1,5 +1,5 @@ application: amp-error-reporting -version: 14 +version: 15 runtime: go api_version: go1 diff --git a/tools/errortracker/errortracker.go b/tools/errortracker/errortracker.go index f8c6497d2b03a..6ddad209c9d9b 100644 --- a/tools/errortracker/errortracker.go +++ b/tools/errortracker/errortracker.go @@ -149,6 +149,9 @@ func handle(w http.ResponseWriter, r *http.Request) { errorType += "-canary" isCanary = true; } + if r.URL.Query().Get("ex") == "1" { + errorType += "-expected" + } sample := rand.Float64() throttleRate := 0.01 diff --git a/tools/experiments/experiments.js b/tools/experiments/experiments.js index 5a2d04605504a..35eabb507612d 100644 --- a/tools/experiments/experiments.js +++ b/tools/experiments/experiments.js @@ -92,12 +92,6 @@ const EXPERIMENTS = [ spec: 'https://github.com/ampproject/amphtml/issues/6196', cleanupIssue: 'https://github.com/ampproject/amphtml/issues/6217', }, - { - id: 'amp-inabox', - name: 'AMP inabox', - spec: 'https://github.com/ampproject/amphtml/issues/5700', - cleanupIssue: 'https://github.com/ampproject/amphtml/issues/6156', - }, { id: 'amp-form-var-sub', name: 'Variable Substitutions in AMP Form inputs for POST/GET submits', @@ -211,11 +205,6 @@ const EXPERIMENTS = [ 'amp-animation/amp-animation.md', cleanupIssue: 'https://github.com/ampproject/amphtml/issues/5888', }, - { - id: 'amp-ad-loading-ux', - name: 'New default loading UX to amp-ad', - cleanupIssue: 'https://github.com/ampproject/amphtml/issues/6009', - }, { id: 'visibility-v2', name: 'New visibility tracking using native IntersectionObserver', diff --git a/validator/validator-main.protoascii b/validator/validator-main.protoascii index 1aca6891c1385..dd40109090d06 100644 --- a/validator/validator-main.protoascii +++ b/validator/validator-main.protoascii @@ -2944,6 +2944,10 @@ attr_lists: { } attrs: { name: "dir" } attrs: { name: "draggable" } + attrs: { + name: "hidden" + value: "" + } attrs: { name: "id" blacklisted_value_regex: "(^|\\s)(" # Values are space separated