From 47bacd84d4078e736ae34d0e8a2a342f55ce910f Mon Sep 17 00:00:00 2001 From: Alan Orozco Date: Wed, 16 Jun 2021 09:47:11 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=E2=99=BB=EF=B8=8F=F0=9F=9A=80=20Up?= =?UTF-8?q?dates=20to=20`app-index`=20tests=20(#34887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In general, makes the dev dashboard tests faster and more consistent with other `build-system` tests. 1. **Removes AMP validation of output**. This was the slowest process in tests. It wasn't providing much value since we'd never fail validation anyway. We could potentially use Bento components later so might as well remove. 2. **Uses `ava`**. This is faster and can be listed along other `ava` tests instead of requiring a custom runner. Also removed special runner, task, and related configuration. 3. **Uses `posthtml` instead of `JSDOM`**. This reduces the elapsed time of remaining tests from `~1s` to `~200ms`. --- amp.js | 1 - build-system/pr-check/build-targets.js | 10 - build-system/pr-check/checks.js | 5 - build-system/server/app-index/test/helpers.js | 67 +- .../app-index/test/test-amphtml-helpers.js | 626 ++++++++---------- .../server/app-index/test/test-file-list.js | 246 ++++--- .../server/app-index/test/test-html.js | 40 +- .../server/app-index/test/test-self.js | 130 ---- .../server/app-index/test/test-template.js | 45 -- build-system/server/app-index/test/test.js | 12 +- build-system/tasks/ava.js | 1 + build-system/tasks/dev-dashboard-tests.js | 58 -- build-system/tasks/pr-check.js | 4 - 13 files changed, 472 insertions(+), 773 deletions(-) delete mode 100644 build-system/server/app-index/test/test-self.js delete mode 100644 build-system/server/app-index/test/test-template.js delete mode 100644 build-system/tasks/dev-dashboard-tests.js 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` - - - - - -
- -
- - Text content - -
- - - -
- - `; - - 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` - - - - - -
- -
- - Text content - -
-
- - `; - - 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` + + + + + + +
+ +
+ + Text content + +
+ + + +
+ + + `; + + 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
', () => { - const expected = 'amp-form'; +test('addRequiredExtensionsToHead adds extensions', async (t) => { + const expected = ['amp-foo', 'amp-bar', 'amp-foo-bar-baz']; + + // eslint-disable-next-line local/html-template + const rawStr = html` + + + + + + +
+ +
+ + Text content + +
+
+ + + `; + + const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr)); + + t.is(scripts.length, expected.length); + + scripts.forEach((script) => { + t.truthy(script.attrs?.src); + t.is(script.attrs?.async, ''); + t.falsy(script.attrs?.['custom-template']); + }); - // eslint-disable-next-line local/html-template - const rawStr = html` - - -
- - `; + 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
', async (t) => { + const expected = 'amp-form'; - expect(scripts).to.have.length(1); + // eslint-disable-next-line local/html-template + const rawStr = html` + + +
+ + `; - 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 - - `; +test('addRequiredExtensionsToHead adds per + + + `; - const scripts = document.head.getElementsByTagName('script'); + const scripts = await getScriptNodes(addRequiredExtensionsToHead(rawStr)); - expect(scripts).to.have.length(1); + t.is(scripts.length, 1); - const [script] = scripts; + const [script] = scripts; - 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.truthy(script.attrs?.src); + t.is(script.attrs?.async, ''); + t.falsy(script.attrs?.['custom-template']); + t.is(script.attrs?.['custom-element'], expected); }); diff --git a/build-system/server/app-index/test/test-file-list.js b/build-system/server/app-index/test/test-file-list.js index b5c04fe6f381..01aee9344b8c 100644 --- a/build-system/server/app-index/test/test-file-list.js +++ b/build-system/server/app-index/test/test-file-list.js @@ -14,138 +14,178 @@ * limitations under the License. */ -const {expect} = require('chai'); +const posthtml = require('posthtml'); +const test = require('ava'); +const {extractMatching, getElementChildren} = require('./helpers'); const {FileList} = require('../file-list'); -const {getBoundAttr, parseHtmlChunk} = require('./helpers'); -describe('devdash', () => { - describe('FileList', () => { - it('wraps', () => { - const root = parseHtmlChunk( - FileList({ - basepath: 'basepath', - fileSet: [], - selectModePrefix: '/', - }) - ); - - expect(root.className).to.equal('file-list-container'); +test('wraps', async (t) => { + const document = FileList({ + basepath: 'basepath', + fileSet: [], + selectModePrefix: '/', + }); - const {firstElementChild} = root; + await posthtml([ + (tree) => { + const elements = getElementChildren(tree); + t.is(elements.length, 1); - expect(firstElementChild.className).to.equal('wrap'); - }); + const [root] = elements; + t.is(root.attrs.class, 'file-list-container'); - it('creates amp-list', () => { - const root = parseHtmlChunk( - FileList({ - basepath: 'basepath', - fileSet: [], - selectModePrefix: '/', - }) - ); + const [firstElementChild] = getElementChildren(root.content); + t.is(firstElementChild.attrs.class, 'wrap'); + }, + ]).process(document); +}); - const {length} = root.getElementsByTagName('amp-list'); - expect(length).to.equal(1); - }); +test('creates amp-list', async (t) => { + const document = FileList({ + basepath: 'basepath', + fileSet: [], + selectModePrefix: '/', + }); - it('creates placeholder inside amp-list with rendered data', () => { - const fileSet = ['foo.bar', 'tacos.al.pastor']; + const elements = []; + await posthtml([ + (tree) => + tree.match({tag: 'amp-list'}, (node) => { + elements.push(node); + return node; + }), + ]).process(document); + + const {length} = elements; + t.is(length, 1); +}); - const root = parseHtmlChunk( - FileList({ - fileSet, - basepath: 'basepath', - selectModePrefix: '/', - }) - ); +test('creates placeholder inside amp-list with rendered data', async (t) => { + const fileSet = ['foo.bar', 'tacos.al.pastor']; - const els = root.querySelectorAll('amp-list > [placeholder]'); + const document = FileList({ + fileSet, + basepath: 'basepath', + selectModePrefix: '/', + }); - expect(els).to.have.length(1); + const extractedAmplist = await extractMatching(document, { + tag: 'amp-list', + }); + t.truthy(extractedAmplist); - const [placeholder] = els; - const {firstElementChild} = placeholder; + const extractedPlaceholder = await extractMatching(extractedAmplist, { + attrs: {placeholder: ''}, + }); + t.truthy(extractedPlaceholder); + + let placeholders = []; + const fileLinkContainers = []; + await posthtml([ + (tree) => { + placeholders = getElementChildren(tree); + tree.match({attrs: {class: /file-link-container/}}, (node) => { + const [a] = getElementChildren(node.content); + if (a && a.tag === 'a' && !a.attrs?.['[href]']) { + fileLinkContainers.push(node); + } + }); - expect(firstElementChild.getAttribute('role')).to.equal('list'); + return tree; + }, + ]).process(extractedPlaceholder); - const items = firstElementChild.querySelectorAll('.file-link-container'); + const [placeholder] = placeholders; + const [firstElementChild] = getElementChildren(placeholder.content || []); - expect(items).to.have.length(fileSet.length); - }); + t.is(firstElementChild.attrs?.role, 'list'); + t.is(fileLinkContainers.length, fileSet.length); +}); - it('binds /examples hrefs', () => { - const fileSet = ['asada.html', 'adobada.html', 'pastor.html']; - const basepath = '/examples/'; +async function getListitemAElements(document) { + const extractedList = await extractMatching(document, { + attrs: {role: 'list'}, + }); - const root = parseHtmlChunk( - FileList({ - fileSet, - basepath, - selectModePrefix: '/', - }) - ); + const elements = []; + await posthtml([ + (tree) => + tree.match({attrs: {role: 'listitem'}}, (node) => { + const [a] = getElementChildren(node.content); + if (a && a.tag === 'a' && a.attrs?.href) { + elements.push(a); + } + return node; + }), + ]).process(extractedList); + + return elements; +} + +test('binds /examples hrefs', async (t) => { + const fileSet = ['asada.html', 'adobada.html', 'pastor.html']; + const basepath = '/examples/'; + + const document = FileList({ + fileSet, + basepath, + selectModePrefix: '/', + }); - const els = root.querySelectorAll('amp-list [role=listitem] > a[href]'); + const elements = await getListitemAElements(document); - expect(els).to.have.length(fileSet.length); + t.is(elements.length, fileSet.length); - Array.from(els).forEach((el, i) => { - expect(getBoundAttr(el, 'href')).to.be.ok; - expect(el.getAttribute('href')).to.equal(basepath + fileSet[i]); - }); - }); + for (const [i, element] of elements.entries()) { + t.truthy(element.attrs?.['[href]']); + t.is(element.attrs?.href, basepath + fileSet[i]); + } +}); - it('does not bind non-/examples hrefs', () => { - const fileSet = ['asada.html', 'adobada.html', 'pastor.html']; - const basepath = '/potato/'; +test('does not bind non-/examples hrefs', async (t) => { + const fileSet = ['asada.html', 'adobada.html', 'pastor.html']; + const basepath = '/potato/'; - const root = parseHtmlChunk( - FileList({ - fileSet, - basepath, - selectModePrefix: '/', - }) - ); + const document = FileList({ + fileSet, + basepath, + selectModePrefix: '/', + }); - const els = root.querySelectorAll('amp-list [role=listitem] > a[href]'); + const elements = await getListitemAElements(document); - expect(els).to.have.length(fileSet.length); + t.is(elements.length, fileSet.length); - Array.from(els).forEach((el, i) => { - expect(getBoundAttr(el, 'href')).to.be.undefined; - expect(el.getAttribute('href')).to.equal(basepath + fileSet[i]); - }); - }); + for (const [i, element] of elements.entries()) { + t.falsy(element.attrs?.['[href]']); + t.is(element.attrs?.href, basepath + fileSet[i]); + } +}); - it('binds/does not bind mixed', () => { - const bound = ['asada.html', 'adobada.html', 'pastor.html']; - const notBound = ['chabbuddy.g', 'dj.beats', 'mc.grindah']; - const basepath = '/examples/'; +test('binds/does not bind mixed', async (t) => { + const bound = ['asada.html', 'adobada.html', 'pastor.html']; + const notBound = ['chabbuddy.g', 'dj.beats', 'mc.grindah']; + const basepath = '/examples/'; - const root = parseHtmlChunk( - FileList({ - fileSet: [...bound, ...notBound], - basepath, - selectModePrefix: '/', - }) - ); + const document = FileList({ + fileSet: [...bound, ...notBound], + basepath, + selectModePrefix: '/', + }); - const els = root.querySelectorAll('amp-list [role=listitem] > a[href]'); + const elements = await getListitemAElements(document); - expect(els).to.have.length(bound.length + notBound.length); + t.is(elements.length, bound.length + notBound.length); - bound.forEach((expectedHref, i) => { - const el = els[i]; - expect(getBoundAttr(el, 'href')).to.be.ok; - expect(el.getAttribute('href')).to.equal(basepath + expectedHref); - }); + bound.forEach((expectedHref, i) => { + const element = elements[i]; + t.truthy(element.attrs?.['[href]']); + t.is(element.attrs?.href, basepath + expectedHref); + }); - notBound.forEach((expectedHref, i) => { - const el = els[bound.length + i]; - expect(getBoundAttr(el, 'href')).to.be.undefined; - expect(el.getAttribute('href')).to.equal(basepath + expectedHref); - }); - }); + notBound.forEach((expectedHref, i) => { + const element = elements[bound.length + i]; + t.falsy(element.attrs?.['[href]']); + t.is(element.attrs?.href, basepath + expectedHref); }); }); diff --git a/build-system/server/app-index/test/test-html.js b/build-system/server/app-index/test/test-html.js index fa1a3d671055..180aa2276e37 100644 --- a/build-system/server/app-index/test/test-html.js +++ b/build-system/server/app-index/test/test-html.js @@ -14,30 +14,26 @@ * limitations under the License. */ -const {expect} = require('chai'); +const test = require('ava'); const {html, joinFragments} = require('../html'); -describe('devdash', () => { - describe('html helpers', () => { - describe('joinFragments', () => { - it('joins simple fragments', () => { - expect(joinFragments(['a', 'b', 'c'])).to.equal('abc'); - }); - it('joins mapped fragments', () => { - expect(joinFragments([1, 2, 3], (a) => a + 1)).to.equal('234'); - }); - }); +test('joinFragments joins simple fragments', (t) => { + t.is(joinFragments(['a', 'b', 'c']), 'abc'); +}); + +test('joinFragments joins mapped fragments', (t) => { + t.is( + joinFragments([1, 2, 3], (a) => a + 1), + '234' + ); +}); - describe('html', () => { - it('passes through simple string', () => { - expect(html`foo`).to.equal('foo'); - }); +test('tagged literal passes through simple string', (t) => { + t.is(html`foo`, 'foo'); +}); - it('concatenates interpolated args', () => { - // eslint-disable-next-line local/html-template - const interpolated = html`quesadilla ${'de'} chicharrón ${'con'} queso`; - expect(interpolated).to.equal('quesadilla de chicharrón con queso'); - }); - }); - }); +test('tagged literal concatenates interpolated args', (t) => { + // eslint-disable-next-line local/html-template + const interpolated = html`quesadilla ${'de'} chicharrón ${'con'} queso`; + t.is(interpolated, 'quesadilla de chicharrón con queso'); }); diff --git a/build-system/server/app-index/test/test-self.js b/build-system/server/app-index/test/test-self.js deleted file mode 100644 index 937d5191356b..000000000000 --- a/build-system/server/app-index/test/test-self.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright 2019 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const amphtmlValidator = require('amphtml-validator'); -const fs = require('fs'); -const path = require('path'); -const {expectValidAmphtml, getBoundAttr, parseHtmlChunk} = require('./helpers'); -const {expect} = require('chai'); - -describe('devdash', () => { - describe('Test helpers', () => { - describe('parseHtmlChunk', () => { - it('returns firstElementChild', () => { - const {tagName} = parseHtmlChunk(''); - expect(tagName).to.equal(tagName); - }); - - it('fails with multiple children', () => { - expect(() => parseHtmlChunk('')).to.throw(); - }); - - it('fails with text node as content', () => { - expect(() => parseHtmlChunk('text content')).to.throw(); - }); - - it('fails on empty string', () => { - expect(() => parseHtmlChunk('')).to.throw(); - }); - }); - - describe('getBoundAttr', () => { - it('returns bound attr set with other bound attrs', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'myHref')).to.equal('a'); - }); - - it('returns bound attr set without quotes with unbound attr', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'myHref')).to.equal('a'); - }); - - it('returns bound attr set with quotes with unbound attr', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'foo')).to.equal('blah'); - }); - - it('returns bound attr set with quotes', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'myHref')).to.equal('b'); - }); - - it('returns bound attr set without quotes', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'foo')).to.equal('baz'); - }); - - it('returns undefined when not found', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'whatever')).to.be.undefined; - }); - - it('returns value with whitespace', () => { - const valueWithWhitespace = ` ^..^ / - /_/\_____/ - /\ /\ - / \ / \ `; - const fakeEl = { - outerHTML: `
`, - }; - expect(getBoundAttr(fakeEl, 'foo')).to.equal(valueWithWhitespace); - }); - - it('returns value with double quote', () => { - const fakeEl = {outerHTML: "
"}; - expect(getBoundAttr(fakeEl, 'foo')).to.equal('ab"cd ef'); - }); - - it('returns value with single quote', () => { - const fakeEl = {outerHTML: '
'}; - expect(getBoundAttr(fakeEl, 'foo')).to.equal("ab'cd ef"); - }); - }); - - describe('expectValidAmphtml', () => { - it('passes with minimum valid doc', async () => { - const validDocPath = path.join( - __dirname, - '../../../../validator/testdata/feature_tests/minimum_valid_amp.html' - ); - - const validDoc = fs.readFileSync(validDocPath).toString(); - - expectValidAmphtml(await amphtmlValidator.getInstance(), validDoc); - }); - - it('fails with invalid doc', async () => { - const invalidDoc = ''; - - const validator = await amphtmlValidator.getInstance(); - - expect(() => { - expectValidAmphtml(validator, invalidDoc); - }).to.throw(); - }); - - it('ignores errors with severity ≠ ERROR', () => { - const mockValidator = { - validateString: () => ({ - status: 'PASS', - errors: [{severity: 'FOO'}], - }), - }; - expectValidAmphtml(mockValidator, 'any string'); - }); - }); - }); -}); diff --git a/build-system/server/app-index/test/test-template.js b/build-system/server/app-index/test/test-template.js deleted file mode 100644 index 2974931b84d9..000000000000 --- a/build-system/server/app-index/test/test-template.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2019 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const amphtmlValidator = require('amphtml-validator'); - -const {expectValidAmphtml} = require('./helpers'); -const {renderTemplate} = require('../template'); - -describe('template', () => { - describe('renderTemplate', () => { - it('renders valid doc', async () => { - expectValidAmphtml( - await amphtmlValidator.getInstance(), - renderTemplate({ - basepath: '/examples/', - css: 'body{}', - isMainPage: true, - fileSet: ['tacos.al.pastor'], - serveMode: 'default', - selectModePrefix: '/', - }) - ); - }); - - it('renders valid doc with empty/default values', async () => { - expectValidAmphtml( - await amphtmlValidator.getInstance(), - renderTemplate() - ); - }); - }); -}); diff --git a/build-system/server/app-index/test/test.js b/build-system/server/app-index/test/test.js index a2b8a3c682a1..d01ed7598291 100644 --- a/build-system/server/app-index/test/test.js +++ b/build-system/server/app-index/test/test.js @@ -14,16 +14,12 @@ * limitations under the License. */ -const {expect} = require('chai'); +const test = require('ava'); const {serveIndexForTesting} = require('../index'); const NOOP = () => {}; -describe('devdash', () => { - describe('express middleware', () => { - it('renders HTML', async () => { - const renderedHtml = await serveIndexForTesting({url: '/'}, {end: NOOP}); - expect(renderedHtml).to.be.ok; - }); - }); +test('renders HTML', async (t) => { + const renderedHtml = await serveIndexForTesting({url: '/'}, {end: NOOP}); + t.truthy(renderedHtml); }); diff --git a/build-system/tasks/ava.js b/build-system/tasks/ava.js index e0554cc98b01..6930952780a4 100644 --- a/build-system/tasks/ava.js +++ b/build-system/tasks/ava.js @@ -25,6 +25,7 @@ async function ava() { // These need equivalents for CI in build-system/pr-check/build-targets.js // (see targetMatchers[Targets.AVA]) const testFiles = [ + 'build-system/server/app-index/test/*test*.js', 'build-system/server/test/app-utils.test.js', 'build-system/tasks/get-zindex/get-zindex.test.js', 'build-system/tasks/make-extension/test/test.js', diff --git a/build-system/tasks/dev-dashboard-tests.js b/build-system/tasks/dev-dashboard-tests.js deleted file mode 100644 index e3dc9d10c89d..000000000000 --- a/build-system/tasks/dev-dashboard-tests.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright 2019 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -'use strict'; - -const config = require('../test-configs/config'); -const globby = require('globby'); -const Mocha = require('mocha'); -const {isCiBuild} = require('../common/ci'); - -/** - * Run all the dev dashboard tests - * @return {!Promise} - */ -async function devDashboardTests() { - const mocha = new Mocha({ - reporter: isCiBuild() ? 'mocha-silent-reporter' : 'spec', - }); - - // Add our files - const allDevDashboardTests = globby.sync(config.devDashboardTestPaths); - allDevDashboardTests.forEach((file) => { - mocha.addFile(file); - }); - - // Create our deffered - let resolver; - const deferred = new Promise((resolverIn) => { - resolver = resolverIn; - }); - - // Run the tests. - mocha.run(function (failures) { - if (failures) { - process.exitCode = 1; - } - resolver(); - }); - return deferred; -} - -module.exports = { - devDashboardTests, -}; - -devDashboardTests.description = 'Run all the dev dashboard tests'; diff --git a/build-system/tasks/pr-check.js b/build-system/tasks/pr-check.js index 7616187c7335..44b9aa55bb4c 100644 --- a/build-system/tasks/pr-check.js +++ b/build-system/tasks/pr-check.js @@ -93,10 +93,6 @@ async function prCheck() { runCheck('amp check-links --local_changes'); } - if (buildTargetsInclude(Targets.DEV_DASHBOARD)) { - runCheck('amp dev-dashboard-tests'); - } - if (buildTargetsInclude(Targets.OWNERS)) { runCheck('amp check-owners'); }