Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better A4A/FIE testing, add FIE services regression test #19943

Merged
merged 14 commits into from
Dec 19, 2018
239 changes: 168 additions & 71 deletions build-system/amp4test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

const app = module.exports = require('express').Router();

/*eslint "max-len": 0*/
/* eslint-disable max-len */

/**
* Logs the given messages to the console in local dev mode, but not while
Expand All @@ -31,59 +31,45 @@ function log(...messages) {
}

app.use('/compose-doc', function(req, res) {
const sourceOrigin = req.query['__amp_source_origin'];
if (sourceOrigin) {
res.setHeader('AMP-Access-Control-Allow-Source-Origin', sourceOrigin);
}
res.setHeader('X-XSS-Protection', '0');
const mode = process.env.SERVE_MODE == 'compiled' ? '' : 'max.';
const frameHtml = process.env.SERVE_MODE == 'compiled'

const {body, css, experiments, extensions, spec} = req.query;

const compiled = process.env.SERVE_MODE == 'compiled';
const frameHtml = (compiled)
? 'dist.3p/current-min/frame.html'
: 'dist.3p/current/frame.max.html';
const {extensions} = req.query;
let extensionScripts = '';
if (!!extensions) {
extensionScripts = extensions.split(',').map(function(extension) {
return '<script async custom-element="'
+ extension + '" src=/dist/v0/'
+ extension + '-0.1.' + mode + 'js></script>';
}).join('\n');
}

const {experiments} = req.query;
let metaTag = '';
let experimentString = '';
let experimentsBlock = '';
if (experiments) {
metaTag = '<meta name="amp-experiments-opt-in" content="' +
experiments + '">';
experimentString = '"' + experiments.split(',').join('","') + '"';
const string = `"${experiments.split(',').join('","')}"`;
// TODO: Why is setting localDev necessary?
// `allow-doc-opt-in` enables any experiment to be enabled via doc opt-in.
experimentsBlock =
`<script>
window.AMP_CONFIG = window.AMP_CONFIG || {"localDev": true};
window.AMP_CONFIG['allow-doc-opt-in'] = (window.AMP_CONFIG['allow-doc-opt-in'] || []).concat([${string}]);
</script>
<meta name="amp-experiments-opt-in" content="${experiments}">`;
}
const {css} = req.query;
const cssTag = css ? `<style amp-custom>${css}</style>` : '';

res.send(`
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<link rel="canonical" href="http://nonblocking.io/" >
<title>AMP TEST</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
${metaTag}
<script>
window.AMP_CONFIG = window.AMP_CONFIG || {
"localDev": true
};
window.AMP_CONFIG['allow-doc-opt-in'] =
(window.AMP_CONFIG['allow-doc-opt-in'] || []).concat([${experimentString}]);
</script>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="/dist/${process.env.SERVE_MODE == 'compiled' ? 'v0' : 'amp'}.js"></script>
<meta name="amp-3p-iframe-src" content="http://localhost:9876/${frameHtml}">
${extensionScripts}
${cssTag}
</head>
<body>
${req.query.body}
</body>
</html>
`);
// TODO: Do we need to inject amp-3p-iframe-src for non-ad tests?
const head =
`${experimentsBlock}
<meta name="amp-3p-iframe-src" content="http://localhost:9876/${frameHtml}">`;

const doc = composeDocument({
body,
css,
extensions: extensions ? extensions.split(',') : '',
head,
spec,
});
res.send(doc);
});

/**
Expand Down Expand Up @@ -158,12 +144,22 @@ app.get('/a4a/:bid', (req, res) => {
res.setHeader('AMP-Access-Control-Allow-Source-Origin', sourceOrigin);
}
const {bid} = req.params;
res.send(`
<!doctype html><html amp4ads><head><meta charset=utf-8><meta content=width=device-width,minimum-scale=1,initial-scale=1 name=viewport><script async src=https://cdn.ampproject.org/amp4ads-v0.js></script><script async custom-element=amp-accordion src=https://cdn.ampproject.org/v0/amp-accordion-0.1.js></script><script async custom-element=amp-analytics src=https://cdn.ampproject.org/v0/amp-analytics-0.1.js></script><script async custom-element=amp-anim src=https://cdn.ampproject.org/v0/amp-anim-0.1.js></script><script async custom-element=amp-audio src=https://cdn.ampproject.org/v0/amp-audio-0.1.js></script><script async custom-element=amp-carousel src=https://cdn.ampproject.org/v0/amp-carousel-0.1.js></script><script async custom-element=amp-fit-text src=https://cdn.ampproject.org/v0/amp-fit-text-0.1.js></script><script async custom-element=amp-font src=https://cdn.ampproject.org/v0/amp-font-0.1.js></script><script async custom-element=amp-form src=https://cdn.ampproject.org/v0/amp-form-0.1.js></script><script async custom-element=amp-social-share src=https://cdn.ampproject.org/v0/amp-social-share-0.1.js></script><style amp-custom>body {
background-color: #f4f4f4;
}
</style><style amp4ads-boilerplate>body{visibility:hidden}</style></head>
<body>

const extensions = [
'amp-accordion',
'amp-analytics',
'amp-anim',
'amp-audio',
'amp-carousel',
'amp-fit-text',
'amp-font',
'amp-form',
'amp-social-share',
];

const css = 'body { background-color: #f4f4f4; }';

const body = `
<a href=https://ampbyexample.com target=_blank>
<amp-img alt="AMP Ad" height=250 src=//localhost:9876/amp4test/request-bank/${bid}/deposit/image width=300></amp-img>
</a>
Expand Down Expand Up @@ -192,22 +188,123 @@ app.get('/a4a/:bid', (req, res) => {
}
}
</script>
</amp-analytics>
<script amp-ad-metadata type=application/json>
{
"ampRuntimeUtf16CharOffsets" : [ 134, 1129 ],
"customElementExtensions" : [
"amp-analytics"
],
"extensions" : [
{
"custom-element" : "amp-analytics",
"src" : "https://cdn.ampproject.org/v0/amp-analytics-0.1.js"
}
]
}
</script>
</body>
</html>
`);
</amp-analytics>`;

const doc = composeDocument({
spec: 'amp4ads',
body,
css,
extensions,
mode: 'cdn',
});
res.send(doc);
});

/**
* @param {{body: string, css: string|undefined, extensions: Array<string>|undefined, head: string|undefined, spec: string|undefined}} config
*/
function composeDocument(config) {
const {body, css, extensions, head, spec, mode} = config;

const m = (mode || process.env.SERVE_MODE);
const cdn = (m === 'cdn');
const compiled = (m === 'compiled');

const cssTag = css ? `<style amp-custom>${css}</style>` : '';

// Set link[rel=canonical], CSS boilerplate and runtime <script> depending
// on the AMP spec.
let canonical, boilerplate, runtime;
const amp = spec || 'amp';
switch (amp) {
case 'amp':
canonical = '<link rel="canonical" href="http://nonblocking.io" />';
boilerplate = '<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
runtime = (cdn)
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${compiled ? 'v0' : 'amp'}.js`;
break;
case 'amp4ads':
canonical = '';
boilerplate = '<style amp4ads-boilerplate>body{visibility:hidden}</style>';
runtime = (cdn)
? 'https://cdn.ampproject.org/amp4ads-v0.js'
: `/dist/${compiled ? 'amp4ads-v0' : 'amp-inabox'}.js`;
break;
case 'amp4email':
canonical = '';
boilerplate = '<style amp4email-boilerplate>body{visibility:hidden}</style>';
runtime = (cdn)
? 'https://cdn.ampproject.org/v0.js'
: `/dist/${compiled ? 'v0' : 'amp'}.js`;
break;
default:
throw new Error('Unrecognized AMP spec: ' + spec);
}
const runtimeScript = `<script async src="${runtime}"></script>`;

// Generate extension <script> markup.
let extensionScripts = '';
if (extensions) {
extensionScripts = extensions.map(extension => {
const src = (cdn)
? `https://cdn.ampproject.org/v0/${extension}-0.1.js`
// TODO: Version 0.1 is hard-coded. Use '-latest'?
: `/dist/v0/${extension}-0.1.${compiled ? '' : 'max.'}js`;
return `<script async custom-element="${extension}" src="${src}"></script>`;
}).join('\n');
}

const topHalfOfHtml =
`<!doctype html>
<html ${amp}>
<head>
<title>AMP TEST</title>
<meta charset="utf-8">
${canonical}
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
${head || ''}
${boilerplate}
${runtimeScript}
${extensionScripts}
${cssTag}
</head>`;

// To enable A4A FIE, a <script amp-ad-metadata> tag must exist.
let ampAdMeta = '';
if (amp === 'amp4ads') {
// `ampRuntimeUtf16CharOffsets` is used to cut out all runtime scripts,
// which are not needed in FIE mode.
const start = topHalfOfHtml.indexOf(runtimeScript);
let end = start + runtimeScript.length;

let customElements = [], extensionsMap = [];
if (extensions) {
end = topHalfOfHtml.indexOf(extensionScripts) + extensionScripts.length;
// Filter out extensions that are not custom elements, e.g. amp-mustache.
customElements = extensions.filter(e => e !== 'amp-mustache');
extensionsMap = customElements.map(ce => {
return {
'custom-element': ce,
// TODO: Should this be a local URL i.e. /dist/v0/...?
'src': `https://cdn.ampproject.org/v0/${ce}-0.1.js`,
};
});
}
ampAdMeta =
`<script amp-ad-metadata type=application/json>
{
"ampRuntimeUtf16CharOffsets": [ ${start}, ${end} ],
"customElementExtensions": ${JSON.stringify(customElements)},
"extensions": ${JSON.stringify(extensionsMap)}
}
</script>`;
}

return `${topHalfOfHtml}
<body>
${body}
${ampAdMeta}
</body>
</html>`;
}
44 changes: 43 additions & 1 deletion test/integration/test-amp-ad-fake.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

import {RequestBank} from '../../testing/test-helper';
import {parseQueryString} from '../../src/url';
import {addParamsToUrl, parseQueryString} from '../../src/url';
import {poll} from '../../testing/iframe';

describe.configure().skipIfPropertiesObfuscated().run('A4A', function() {
this.timeout(15000);
Expand Down Expand Up @@ -60,4 +61,45 @@ describe.configure().skipIfPropertiesObfuscated().run('A4A', function() {
});
});
});

const src = addParamsToUrl('/amp4test/compose-doc', {
body: `
<p [text]="foo">123</p>
<button on="tap:AMP.setState({foo: 456})"></button>
`,
extensions: 'amp-bind',
spec: 'amp4ads',
});
describes.integration('amp-bind in A4A', {
body: `
<amp-ad width="300" height="400"
id="i-amphtml-demo-id"
type="fake"
src="${src}">
<div placeholder>Loading...</div>
<div fallback>Could not display the fake ad :(</div>
</amp-ad>
`,
}, env => {
it('p[text]', function*() {
// Wait for the amp-ad to construct its child iframe.
const ad = env.win.document.getElementById('i-amphtml-demo-id');
yield poll('amp-ad > iframe', () => ad.querySelector('iframe'));

// Wait for the iframe contents to load.
const fie = ad.querySelector('iframe').contentWindow;
yield poll('iframe > button', () => fie.document.querySelector('button'));

const text = fie.document.querySelector('p');
expect(text.textContent).to.equal('123');

const button = fie.document.querySelector('button');
return poll('[text]', () => {
// We click this too many times but there's no good way to tell whether
// amp-bind is initialized yet.
button.click();
return text.textContent === '456';
}, /* onError */ undefined, 5000);
});
});
});