diff --git a/amp.js b/amp.js
index 727c5fae82e7..a164240f0c04 100755
--- a/amp.js
+++ b/amp.js
@@ -58,7 +58,6 @@ createTask('coverage-map', 'coverageMap');
createTask('css');
createTask('default', 'defaultTask', 'default-task');
createTask('dep-check', 'depCheck');
-createTask('dev-dashboard-tests', 'devDashboardTests');
createTask('dist');
createTask('e2e');
createTask('firebase');
diff --git a/build-system/pr-check/build-targets.js b/build-system/pr-check/build-targets.js
index ee6e2fa04565..54cb812e0efb 100644
--- a/build-system/pr-check/build-targets.js
+++ b/build-system/pr-check/build-targets.js
@@ -173,16 +173,6 @@ const targetMatchers = {
file == 'build-system/global-configs/caches.json'
);
},
- [Targets.DEV_DASHBOARD]: (file) => {
- if (isOwnersFile(file)) {
- return false;
- }
- return (
- file == 'build-system/tasks/dev-dashboard-tests.js' ||
- file == 'build-system/server/app.js' ||
- file.startsWith('build-system/server/app-index/')
- );
- },
[Targets.DOCS]: (file) => {
if (isOwnersFile(file)) {
return false;
diff --git a/build-system/pr-check/checks.js b/build-system/pr-check/checks.js
index c5f935168b8b..c8267fb3570c 100644
--- a/build-system/pr-check/checks.js
+++ b/build-system/pr-check/checks.js
@@ -39,7 +39,6 @@ function pushBuildWorkflow() {
timedExecOrDie('amp check-build-system');
timedExecOrDie('amp babel-plugin-tests');
timedExecOrDie('amp caches-json');
- timedExecOrDie('amp dev-dashboard-tests');
timedExecOrDie('amp check-exact-versions');
timedExecOrDie('amp check-renovate-config');
timedExecOrDie('amp server-tests');
@@ -105,10 +104,6 @@ async function prBuildWorkflow() {
timedExecOrDie('amp markdown-toc');
}
- if (buildTargetsInclude(Targets.DEV_DASHBOARD)) {
- timedExecOrDie('amp dev-dashboard-tests');
- }
-
if (buildTargetsInclude(Targets.OWNERS)) {
timedExecOrDie('amp check-owners --local_changes'); // only for PR builds
}
diff --git a/build-system/server/app-index/test/helpers.js b/build-system/server/app-index/test/helpers.js
index 6a5d56b7f4fc..29e2f16ae3c1 100644
--- a/build-system/server/app-index/test/helpers.js
+++ b/build-system/server/app-index/test/helpers.js
@@ -14,44 +14,45 @@
* limitations under the License.
*/
-const {expect} = require('chai');
-const {JSDOM} = require('jsdom');
+const posthtml = require('posthtml');
-const parseHtmlChunk = (htmlStr) => {
- const {body} = new JSDOM(htmlStr).window.document;
- expect(body.children).to.have.length(1);
- return body.firstElementChild;
-};
+function getElementChildren(content) {
+ return content?.filter?.((node) => typeof node !== 'string') || [];
+}
-const boundAttrRe = (attr) =>
- new RegExp(`\\[${attr}\\]=(("[^"]+")|('[^']+')|([^\\s\\>]+))`);
+async function posthtmlGetTextContent(document, matcher) {
+ let textContent;
-// JSDom doesn't parse attributes whose names don't follow the spec, so
-// our only way to test [attr] values is via regex.
-const getBoundAttr = ({outerHTML}, attr) => {
- const match = outerHTML.match(boundAttrRe(attr));
- if (!match) {
- return;
- }
- const [, valuePart] = match;
- if (valuePart.charAt(0) == '"' || valuePart.charAt(0) == "'") {
- return valuePart.substring(1, valuePart.length - 1);
- }
- return valuePart;
-};
+ await posthtml([
+ (tree) => {
+ tree.match(matcher, (node) => {
+ if (node.content) {
+ textContent = node.content.join('');
+ }
+ return node;
+ });
+ return tree;
+ },
+ ]).process(document);
-const expectValidAmphtml = (validator, string) => {
- const {errors: errorsAndWarnings, status} = validator.validateString(string);
- const errors = errorsAndWarnings.filter(({severity}) => severity == 'ERROR');
+ return textContent;
+}
- // Compare with empty array instead of checking `to.be.empty` so
- // validation errors are output as AssertionErrors.
- expect(errors).to.deep.equal([]);
- expect(status).to.equal('PASS');
-};
+async function extractMatching(document, matcher) {
+ const {html} = await posthtml([
+ (tree) => {
+ let matching = '';
+ tree.match(matcher, (node) => {
+ matching = node;
+ });
+ return [matching];
+ },
+ ]).process(document);
+ return html.trim() || null;
+}
module.exports = {
- expectValidAmphtml,
- getBoundAttr,
- parseHtmlChunk,
+ extractMatching,
+ getElementChildren,
+ posthtmlGetTextContent,
};
diff --git a/build-system/server/app-index/test/test-amphtml-helpers.js b/build-system/server/app-index/test/test-amphtml-helpers.js
index 3e568aa91440..03f4c1a73766 100644
--- a/build-system/server/app-index/test/test-amphtml-helpers.js
+++ b/build-system/server/app-index/test/test-amphtml-helpers.js
@@ -14,13 +14,8 @@
* limitations under the License.
*/
-const amphtmlValidator = require('amphtml-validator');
-
-const {expectValidAmphtml, parseHtmlChunk} = require('./helpers');
-const {expect} = require('chai');
-const {html} = require('../html');
-const {JSDOM} = require('jsdom');
-
+const posthtml = require('posthtml');
+const test = require('ava');
const {
AmpDoc,
AmpState,
@@ -29,395 +24,318 @@ const {
containsExpr,
ternaryExpr,
} = require('../amphtml-helpers');
+const {getElementChildren, posthtmlGetTextContent} = require('./helpers');
+const {html} = require('../html');
-describe('devdash', () => {
- describe('AMPHTML helpers', () => {
- describe('AmpDoc', () => {
- it('fails without args', () => {
- expect(() => AmpDoc()).to.throw();
- });
-
- it('fails without min required fields', () => {
- expect(() => AmpDoc({})).to.throw();
- });
-
- it('creates valid doc with min required fields', async () => {
- expectValidAmphtml(
- await amphtmlValidator.getInstance(),
- AmpDoc({
- canonical: '/',
- })
- );
- }).timeout(5000);
-
- it('creates valid doc with set fields', async () => {
- expectValidAmphtml(
- await amphtmlValidator.getInstance(),
- AmpDoc({
- canonical: '/',
- css: 'body { font-family:sans-serif; } ',
- head: html` `,
- body: html`
Hola
`,
- })
- );
- }).timeout(5000);
- });
-
- describe('ampStateKey', () => {
- it('concats arguments', () => {
- expect(ampStateKey('foo', 'bar')).to.equal('foo.bar');
- expect(ampStateKey('tacos', 'al', 'pastor')).to.equal(
- 'tacos.al.pastor'
- );
- });
- });
-
- describe('ternaryExpr', () => {
- it('creates expression', () => {
- expect(ternaryExpr('a', 'b', 'c')).to.equal('a ? b : c');
- });
- });
-
- describe('containsExpr', () => {
- it('creates expression with literals', () => {
- expect(containsExpr("'a'", "'b'", "'c'", "'d'")).to.equal(
- "'a'.indexOf('b') > -1 ? 'c' : 'd'"
- );
- });
-
- it('creates expression with vars', () => {
- expect(containsExpr('a', 'b', 'c', 'd')).to.equal(
- 'a.indexOf(b) > -1 ? c : d'
- );
- });
- });
-
- describe('AmpState', () => {
- it('generates tree', () => {
- const id = 'foo';
- const state = 'bar';
- const root = parseHtmlChunk(AmpState(id, state));
+test('AmpDoc fails without args', (t) => {
+ t.throws(() => AmpDoc());
+});
- expect(root.tagName).to.equal('AMP-STATE');
- expect(root.getAttribute('id')).to.equal(id);
+test('AmpDoc fails without min required fields', (t) => {
+ t.throws(() => AmpDoc({}));
+});
- expect(root.children).to.have.length(1);
+test('ampStateKey concats arguments', (t) => {
+ t.is(ampStateKey('foo', 'bar'), 'foo.bar');
+ t.is(ampStateKey('tacos', 'al', 'pastor'), 'tacos.al.pastor');
+});
- const {firstElementChild} = root;
- expect(firstElementChild.tagName).to.equal('SCRIPT');
- expect(firstElementChild.getAttribute('type')).to.equal(
- 'application/json'
- );
- });
+test('ternaryExpr creates expression', (t) => {
+ t.is(ternaryExpr('a', 'b', 'c'), 'a ? b : c');
+});
- it('renders json object', () => {
- const id = 'whatever';
- const state = {foo: 'bar', baz: {yes: 'no'}};
+test('containsExpr creates expression with literals', (t) => {
+ t.is(
+ containsExpr("'a'", "'b'", "'c'", "'d'"),
+ "'a'.indexOf('b') > -1 ? 'c' : 'd'"
+ );
+});
- const {textContent} = parseHtmlChunk(
- AmpState(id, state)
- ).firstElementChild;
+test('containsExpr creates expression with vars', (t) => {
+ t.is(containsExpr('a', 'b', 'c', 'd'), 'a.indexOf(b) > -1 ? c : d');
+});
- expect(JSON.parse(textContent)).to.deep.equal(state);
+function containsExtension(scripts, expectedExtension) {
+ return scripts.some(
+ (s) =>
+ s.attrs?.['custom-element'] == expectedExtension &&
+ s.attrs?.['custom-template'] == null
+ );
+}
+
+function containsTemplate(scripts, expectedTemplate) {
+ return scripts.some(
+ (s) =>
+ s.attrs?.['custom-template'] == expectedTemplate &&
+ s.attrs?.['custom-extension'] == null
+ );
+}
+
+async function getScriptNodes(document) {
+ const scripts = [];
+
+ await posthtml([
+ (tree) => {
+ tree.match({tag: 'script'}, (node) => {
+ scripts.push(node);
+ return node;
});
+ return tree;
+ },
+ ]).process(document);
+
+ return scripts;
+}
+
+test('AmpState generates tree', async (t) => {
+ const id = 'foo';
+ const state = 'bar';
+ const document = AmpState(id, state);
+
+ await posthtml([
+ (tree) => {
+ const nodes = getElementChildren(tree);
+ t.is(nodes.length, 1);
+ const [root] = nodes;
+ t.is(root.tag, 'amp-state');
+ const rootChildren = getElementChildren(root.content);
+ t.is(rootChildren.length, 1);
+ const [firstElementChild] = rootChildren;
+ t.is(firstElementChild.tag, 'script');
+ t.is(firstElementChild.attrs?.type, 'application/json');
+ },
+ ]).process(document);
+});
- it('renders string literal', () => {
- const id = 'whatever';
- const state = 'foo';
+test('AmpState renders json object', async (t) => {
+ const id = 'whatever';
+ const state = {foo: 'bar', baz: {yes: 'no'}};
- const {textContent} = parseHtmlChunk(
- AmpState(id, state)
- ).firstElementChild;
+ const document = AmpState(id, state);
- expect(JSON.parse(textContent)).to.equal(state);
- });
+ const textContent = await posthtmlGetTextContent(document, {
+ tag: 'script',
+ });
- it('renders array', () => {
- const id = 'whatever';
- const state = ['foo', 'bar', 'baz'];
+ t.deepEqual(JSON.parse(textContent), state);
+});
- const {textContent} = parseHtmlChunk(
- AmpState(id, state)
- ).firstElementChild;
+test('AmpState renders string literal', async (t) => {
+ const id = 'whatever';
+ const state = 'foo';
- expect(JSON.parse(textContent)).to.deep.equal(state);
- });
- });
-
- describe('addRequiredExtensionsToHead', () => {
- function containsExtension(scripts, expectedExtension) {
- return scripts.some(
- (s) =>
- s.getAttribute('custom-element') == expectedExtension &&
- s.getAttribute('custom-template') == null
- );
- }
-
- function containsTemplate(scripts, expectedTemplate) {
- return scripts.some(
- (s) =>
- s.getAttribute('custom-template') == expectedTemplate &&
- s.getAttribute('custom-extension') == null
- );
- }
-
- it('renders ok', () => {
- // eslint-disable-next-line local/html-template
- const rawStr = html`
-
-
-
-
- `;
-
- expect(new JSDOM(addRequiredExtensionsToHead(rawStr))).to.be.ok;
- });
+ const document = AmpState(id, state);
- it('adds mixed', () => {
- const expectedExtensions = [
- 'amp-foo',
- 'amp-bar',
- 'amp-foo-bar-baz',
- 'amp-bind',
- 'amp-form',
- ];
-
- const expectedTemplates = ['amp-mustache'];
-
- // eslint-disable-next-line local/html-template
- const rawStr = html`
-
-
-
-
-
-
-
- `;
-
- const {document} = new JSDOM(addRequiredExtensionsToHead(rawStr))
- .window;
-
- const scripts = Array.from(
- document.head.getElementsByTagName('script')
- );
-
- expect(scripts).to.have.length(
- expectedExtensions.length + expectedTemplates.length
- );
-
- scripts.forEach((script) => {
- expect(script.getAttribute('src')).to.be.ok;
- expect(script.getAttribute('async')).to.equal('');
- });
-
- expectedExtensions.forEach((expectedScript) => {
- expect(scripts).to.satisfy((scripts) =>
- containsExtension(scripts, expectedScript)
- );
- });
-
- expectedTemplates.forEach((expectedScript) => {
- expect(scripts).to.satisfy((scripts) =>
- containsTemplate(scripts, expectedScript)
- );
- });
- });
+ const textContent = await posthtmlGetTextContent(document, {
+ tag: 'script',
+ });
- it('adds extensions', () => {
- const expected = ['amp-foo', 'amp-bar', 'amp-foo-bar-baz'];
-
- // eslint-disable-next-line local/html-template
- const rawStr = html`
-
-
-
-
-
-
-
- `;
-
- const {document} = new JSDOM(addRequiredExtensionsToHead(rawStr))
- .window;
-
- const scripts = Array.from(
- document.head.getElementsByTagName('script')
- );
-
- expect(scripts).to.have.length(expected.length);
-
- scripts.forEach((script) => {
- expect(script.getAttribute('src')).to.be.ok;
- expect(script.getAttribute('async')).to.equal('');
- expect(script.getAttribute('custom-template')).to.be.null;
- });
-
- expected.forEach((expectedScript) => {
- expect(scripts).to.satisfy((scripts) =>
- containsExtension(scripts, expectedScript)
- );
- });
- });
+ t.is(JSON.parse(textContent), state);
+});
- it('adds template', () => {
- const expected = 'amp-mustache';
+test('AmpState renders array', async (t) => {
+ const id = 'whatever';
+ const state = ['foo', 'bar', 'baz'];
- // eslint-disable-next-line local/html-template
- const rawStr = html`
-
-
-
-
-
-
-
-
-
-
- `;
+ const document = AmpState(id, state);
- const {document} = new JSDOM(addRequiredExtensionsToHead(rawStr))
- .window;
+ const textContent = await posthtmlGetTextContent(document, {
+ tag: 'script',
+ });
- const scripts = document.head.getElementsByTagName('script');
+ t.deepEqual(JSON.parse(textContent), state);
+});
- expect(scripts).to.have.length(1);
+test('addRequiredExtensionsToHead adds mixed', async (t) => {
+ const expectedExtensions = [
+ 'amp-foo',
+ 'amp-bar',
+ 'amp-foo-bar-baz',
+ 'amp-bind',
+ 'amp-form',
+ ];
+
+ const expectedTemplates = ['amp-mustache'];
+
+ // eslint-disable-next-line local/html-template
+ const rawStr = html`
+
+
+
+
+
+
+
+
+
+ `;
+
+ const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr));
+
+ t.is(scripts.length, expectedExtensions.length + expectedTemplates.length);
+
+ scripts.forEach((script) => {
+ t.truthy(script.attrs?.src);
+ t.is(script.attrs?.async, '');
+ });
- const [script] = scripts;
+ expectedExtensions.forEach((expectedScript) => {
+ t.assert(containsExtension(scripts, expectedScript));
+ });
- expect(script.getAttribute('custom-element')).to.be.null;
- expect(script.getAttribute('src')).to.be.ok;
- expect(script.getAttribute('async')).to.equal('');
- expect(script.getAttribute('custom-template')).to.equal(expected);
- });
+ expectedTemplates.forEach((expectedScript) => {
+ t.assert(containsTemplate(scripts, expectedScript));
+ });
+});
- it('adds per
-
- `;
+ expected.forEach((expectedScript) => {
+ t.assert(containsExtension(scripts, expectedScript));
+ });
+});
- const {document} = new JSDOM(addRequiredExtensionsToHead(rawStr))
- .window;
+test('addRequiredExtensionsToHead adds template', async (t) => {
+ const expected = 'amp-mustache';
+
+ // eslint-disable-next-line local/html-template
+ const rawStr = html`
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr));
+
+ t.is(scripts.length, 1);
+
+ const [script] = scripts;
+
+ t.truthy(script.attrs?.src);
+ t.is(script.attrs?.async, '');
+ t.falsy(script.attrs?.['custom-element']);
+ t.is(script.attrs?.['custom-template'], expected);
+});
- const scripts = document.head.getElementsByTagName('script');
+test('addRequiredExtensionsToHead adds per
+
+ `;
- const [script] = scripts;
+ const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr));
- expect(script.getAttribute('custom-template')).to.be.null;
- expect(script.getAttribute('src')).to.be.ok;
- expect(script.getAttribute('async')).to.equal('');
- expect(script.getAttribute('custom-element')).to.equal(expected);
- });
+ t.is(scripts.length, 1);
- it('adds per ', () => {
- const expected = 'amp-form';
+ const [script] = scripts;
- // eslint-disable-next-line local/html-template
- const rawStr = html`
-
-
-
-
-
-
- `;
+ t.truthy(script.attrs?.src);
+ t.is(script.attrs?.async, '');
+ t.falsy(script.attrs?.['custom-template']);
+ t.is(script.attrs?.['custom-element'], expected);
+});
- const {document} = new JSDOM(addRequiredExtensionsToHead(rawStr))
- .window;
+test('addRequiredExtensionsToHead adds per ', async (t) => {
+ const expected = 'amp-form';
- const scripts = document.head.getElementsByTagName('script');
+ // eslint-disable-next-line local/html-template
+ const rawStr = html`
+
+
+
+
+
+
+
+
+ `;
- expect(scripts).to.have.length(1);
+ const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr));
- const [script] = scripts;
+ t.is(scripts.length, 1);
- expect(script.getAttribute('custom-template')).to.be.null;
- expect(script.getAttribute('src')).to.be.ok;
- expect(script.getAttribute('async')).to.equal('');
- expect(script.getAttribute('custom-element')).to.equal(expected);
- });
+ const [script] = scripts;
- it('adds per