From 4b3ca15e9a73039e2c0742fd603fbda64c5b53a9 Mon Sep 17 00:00:00 2001 From: Michael Solati Date: Thu, 13 Jan 2022 15:46:16 -0800 Subject: [PATCH] test: increase code coverage --- .assets/sample.idl | 115 +++++++ package-lock.json | 3 + package.json | 15 +- tests/{ => lib}/buffer.js | 2 +- tests/lib/cache-helper.js | 57 ++++ tests/lib/chrome-source.js | 53 ++++ tests/lib/chrome-versions.js | 64 ++++ tests/lib/comment.js | 101 ++++++ tests/lib/feature-query.js | 71 +++++ tests/lib/idl-convert.js | 36 +++ tests/{ => lib}/line-hash.js | 2 +- tests/{ => lib}/render-context.js | 6 +- tests/{ => lib}/semaphore.js | 2 +- tests/lib/spawn-helper.js | 47 +++ tests/{ => lib}/traverse.js | 4 +- tests/override.js | 494 ++++++++++++++++++++++++++++++ tools/lib/cache-helper.js | 2 +- tools/lib/chrome-source.js | 4 +- tools/lib/chrome-versions.js | 4 +- tools/lib/comment.js | 6 +- tools/override.js | 12 +- tools/prepare.js | 2 +- 22 files changed, 1073 insertions(+), 29 deletions(-) create mode 100644 .assets/sample.idl rename tests/{ => lib}/buffer.js (95%) create mode 100644 tests/lib/cache-helper.js create mode 100644 tests/lib/chrome-source.js create mode 100644 tests/lib/chrome-versions.js create mode 100644 tests/lib/comment.js create mode 100644 tests/lib/feature-query.js create mode 100644 tests/lib/idl-convert.js rename tests/{ => lib}/line-hash.js (93%) rename tests/{ => lib}/render-context.js (95%) rename tests/{ => lib}/semaphore.js (94%) create mode 100644 tests/lib/spawn-helper.js rename tests/{ => lib}/traverse.js (98%) create mode 100644 tests/override.js diff --git a/.assets/sample.idl b/.assets/sample.idl new file mode 100644 index 0000000..5c72c95 --- /dev/null +++ b/.assets/sample.idl @@ -0,0 +1,115 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Use the chrome.webrtcLoggingPrivate API to control diagnostic +// WebRTC logging. +namespace webrtcLoggingPrivate { + dictionary MetaDataEntry { + // The meta data entry key. + DOMString key; + + // The meta data entry value. + DOMString value; + }; + + dictionary UploadResult { + // The report ID for the uploaded log. Will be empty if not successful. + DOMString reportId; + }; + + dictionary RequestInfo { + // The tab identifier from the chrome.tabs API, if the request is from a + // tab. + long? tabId; + + // The guest process id for the requester, if the request is from a + // webview. + long? guestProcessId; + }; + + callback GenericDoneCallback = void (); + callback UploadDoneCallback = void (UploadResult result); + + interface Functions { + // For all functions, |request| determines which render process to apply + // the operation on. |request| identifies the requesting process. + // |securityOrigin| is the security origin for the tab identified by |tabId| + // and is used for verifying that the tab is the correct one and has not + // been navigated away from. + + // Sets additional custom meta data that will be uploaded along with the + // log. |metaData| is a dictionary of the metadata (key, value). + static void setMetaData(RequestInfo request, + DOMString securityOrigin, + MetaDataEntry[] metaData, + GenericDoneCallback callback); + + // Starts logging. If logging has already been started for this render + // process, the call will be ignored. |appSessionId| is the unique session + // ID which will be added to the log. + static void start(RequestInfo request, + DOMString securityOrigin, + GenericDoneCallback callback); + + // Sets whether the log should be uploaded automatically for the case when + // the render process goes away (tab is closed or crashes) and stop has not + // been called before that. If |shouldUpload| is true it will be uploaded, + // otherwise it will be discarded. The default setting is to discard it. + static void setUploadOnRenderClose(RequestInfo request, + DOMString securityOrigin, + boolean shouldUpload); + + // Stops logging. After stop has finished, either upload() or discard() + // should be called, otherwise the log will be kept in memory until the + // render process is closed or logging restarted. + static void stop(RequestInfo request, + DOMString securityOrigin, + GenericDoneCallback callback); + + // Stores the current log without uploading. The log may stay around for + // as much as 5 days. The application has the option of supplying an id + // for uniquely identifying the log for later upload via a call to + // uploadStored(). + static void store(RequestInfo request, + DOMString securityOrigin, + DOMString logId, + GenericDoneCallback callback); + + // Uploads a previously kept log that was stored via a call to store(). + // The caller needs to know the logId as was originally provided in the + // call to store(). + static void uploadStored(RequestInfo request, + DOMString securityOrigin, + DOMString logId, + UploadDoneCallback callback); + + // Uploads the log and the RTP dumps, if they exist. Logging and RTP dumping + // must be stopped before this function is called. + static void upload(RequestInfo request, + DOMString securityOrigin, + UploadDoneCallback callback); + + // Discards the log. Logging must be stopped before this function is called. + static void discard(RequestInfo request, + DOMString securityOrigin, + GenericDoneCallback callback); + + // Starts RTP dumping. If it has already been started for this render + // process, the call will be ignored. + static void startRtpDump(RequestInfo request, + DOMString securityOrigin, + boolean incoming, + boolean outgoing, + GenericDoneCallback callback); + + // Stops RTP dumping. After stop has finished, the dumps will be + // uploaded with the log if upload is called. Otherwise, the dumps will be + // discarded. + static void stopRtpDump(RequestInfo request, + DOMString securityOrigin, + boolean incoming, + boolean outgoing, + GenericDoneCallback callback); + }; +}; diff --git a/package-lock.json b/package-lock.json index a42ba92..4c2e019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,9 @@ "@types/tmp": "^0.2.1", "@types/turndown": "^5.0.1", "check-code-coverage": "^1.10.0" + }, + "engines": { + "node": "16" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index ee2f635..6f8a45a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,13 @@ { + "scripts": { + "build": "node tools/run-release.js", + "test": "c8 --reporter=lcov --reporter=json --all --src=tools ava" + }, + "private": true, "type": "module", + "engines": { + "node": "16" + }, "devDependencies": { "@types/fancy-log": "^1.3.1", "@types/mri": "^1.1.1", @@ -26,10 +34,5 @@ "tmp": "^0.2.1", "turndown": "^7.1.1", "typescript": "^4.4.3" - }, - "scripts": { - "build": "node tools/run-release.js", - "test": "c8 --reporter=lcov --reporter=json --all --src=tools ava" - }, - "private": true + } } diff --git a/tests/buffer.js b/tests/lib/buffer.js similarity index 95% rename from tests/buffer.js rename to tests/lib/buffer.js index dbbe37a..f5c3e10 100644 --- a/tests/buffer.js +++ b/tests/lib/buffer.js @@ -15,7 +15,7 @@ */ import test from 'ava'; -import { RenderBuffer } from '../tools/lib/buffer.js'; +import { RenderBuffer } from '../../tools/lib/buffer.js'; test('comment', t => { diff --git a/tests/lib/cache-helper.js b/tests/lib/cache-helper.js new file mode 100644 index 0000000..5a323a8 --- /dev/null +++ b/tests/lib/cache-helper.js @@ -0,0 +1,57 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { readFromCache, writeToCache } from '../../tools/lib/cache-helper.js'; + +const filename = 'file.txt'; +const message = 'abc'; + +test('readFromCache', (t) => { + let rfc; + + // Returns message if file is found + writeToCache(filename, message); + rfc = readFromCache(filename); + t.deepEqual(rfc, Buffer.from(message)); + + // Returns null if file has expired + writeToCache(filename, message); + rfc = readFromCache(filename, -100); + t.is(rfc, null); + + // Returns null if file is not found + rfc = readFromCache(String(Math.random())); + t.is(rfc, null); + + // Returns null if no file name is provided + rfc = readFromCache(''); + t.is(rfc, null); +}); + +test('writeToCache', (t) => { + // `writeToCache` doesn't throw an error with valid arguments + t.notThrows(() => { + writeToCache(filename, message); + writeToCache(filename, Buffer.from(message)); + }); + + // `writeToCache` throws an error with invalid arguments + t.throws(() => { + //@ts-ignore + writeToCache(filename, new Promise()); + }); +}); diff --git a/tests/lib/chrome-source.js b/tests/lib/chrome-source.js new file mode 100644 index 0000000..2be0799 --- /dev/null +++ b/tests/lib/chrome-source.js @@ -0,0 +1,53 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import {tmpdir} from 'os'; +import { urlFor, fetchAllTo } from '../../tools/lib/chrome-source.js'; + +const revision = '23a41c068e35f33df1c3579a3b0b469d4458e6c1'; +const chromePath = 'chrome/common/apps/platform_apps/api'; +const chromePaths = [ + 'tools/json_schema_compiler', + 'tools/json_schema_compiler/test', + 'tools/json_comment_eater', + 'ppapi/generators', + 'third_party/ply', +]; + +test('urlFor', (t) => { + // Returns valid url + const uf = urlFor(revision, chromePath); + t.is(uf.startsWith('https://'), true); + t.is(uf.includes(revision), true); + t.is(uf.endsWith(`${chromePath}.tar.gz`), true); +}); + +test('fetchAllTo', async (t) => { + let fta; + // Returns chrome source tree files for each path + fta = await fetchAllTo(tmpdir(), chromePaths, revision); + t.is(Array.isArray(fta), true); + t.is(fta.length, chromePaths.length); + + // Throws error with invalid arguments + // @ts-ignore + await t.throwsAsync(() => fetchAllTo(null, chromePaths, revision)); + // @ts-ignore + t.throws(() => fetchAllTo(tmpdir(), null, revision)); + // @ts-ignore + await t.throwsAsync(() => fetchAllTo(tmpdir(), chromePaths, null)); +}); diff --git a/tests/lib/chrome-versions.js b/tests/lib/chrome-versions.js new file mode 100644 index 0000000..c6ef5e0 --- /dev/null +++ b/tests/lib/chrome-versions.js @@ -0,0 +1,64 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { + splitChromeRelease, + chromeVersions, + chromePublishedStable, +} from '../../tools/lib/chrome-versions.js'; + +test('splitChromeRelease', async (t) => { + /** @type {any} */ + let scr; + + // Returns undefined if too few parts + scr = await splitChromeRelease('88.0.4324'); + t.is(scr, undefined); + + // Returns undefined if too many parts + scr = await splitChromeRelease('88.0.4324.1.1'); + t.is(scr, undefined); + + // Returns major version is invalid + scr = await splitChromeRelease('a.b.c.d'); + t.is(scr, undefined); + + // Returns undefined if semantic version is invalid + scr = await splitChromeRelease('88.0.a.1'); + t.is(scr, undefined); + + // Returns release and rest of release tag if valid + scr = await splitChromeRelease('88.0.4324.47'); + t.deepEqual(Object.keys(scr), ['release', 'rest']); + t.is(typeof scr.release, 'number'); + t.is(typeof scr.rest, 'string'); +}); + +test('chromeVersions', async (t) => { + // Returns head and map of chrome version releases + const cv = await chromeVersions(); + t.is(typeof cv.head, 'string'); + t.is(cv.releases instanceof Map, true); + t.is(cv.releases.size > 0, true); + t.deepEqual(Object.keys(cv), ['head', 'releases']); +}) + +test('chromePublishedStable', async (t) => { + // Returns number representing current stable version of Chrome + const cps = await chromePublishedStable(); + t.is(typeof cps, 'number'); +}); diff --git a/tests/lib/comment.js b/tests/lib/comment.js new file mode 100644 index 0000000..a3c8236 --- /dev/null +++ b/tests/lib/comment.js @@ -0,0 +1,101 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { + regexpParts, + transformEven, + isUrl, + rewriteCommentHrefs, +} from '../../tools/lib/comment.js'; + +test('regexpParts', (t) => { + let rp; + + // Returns original text as single element in array if no match + rp = regexpParts('abc', /1/); + t.is(rp[0], 'abc'); + t.is(rp.length, 1); + + // Returns original text if no match + const match = `{@link ContentBounds}`; + const text = `Get the window's inner bounds as a ${match} object.`; + rp = regexpParts(text, /{@link.+?}/gs); + t.is(rp[1], match); + t.is(rp.length, 3); +}); + +test('transformEven', (t) => { + let te; + let arr = ['1', '2', '3', '4']; + const helper = (s) => s + '-'; + + // Modify even elements + te = transformEven(arr, helper); + for (let i = 0; i < te.length; i++) { + const addedByHelper = i % 2 === 0 ? '-' : ''; + t.is(te[i], arr[i] + addedByHelper); + } +}); + +test('isUrl', async (t) => { + let iu; + + // Valid URLs should return true + iu = isUrl('https://web.dev'); + t.is(iu, true); + iu = isUrl('http://web.dev'); + t.is(iu, true); + iu = isUrl('https://web.dev/learn'); + t.is(iu, true); + + // Incalid URLs should return false + iu = isUrl(':web.dev'); + t.is(iu, false); + iu = isUrl('web.dev'); + t.is(iu, false); +}); + +test('rewriteCommentHrefs', async (t) => { + let rch; + + // rewrite href + const resolveHref = (path) => new URL(path, 'https://web.dev').toString(); + const href = '/learn'; + const contents = `Here's a link!`; + const rewrittenHref = resolveHref(href); + const text = `${contents}`; + const rewrittenText = `${contents}`; + rch = rewriteCommentHrefs(text, resolveHref); + t.is(rch, rewrittenText); + + // throw when anchor has other properties + t.throws(() => + rewriteCommentHrefs(``, resolveHref) + ); + t.throws(() => + rewriteCommentHrefs(``, resolveHref) + ); + + // If `resolveHref` function returns `undefined` then don't change link + // @ts-ignore + rch = rewriteCommentHrefs(text, () => undefined); + t.is(rch, text); + + // If `resolveHref` function doesn't return a url create `@link` + rch = rewriteCommentHrefs(text, () => contents); + t.is(rch, `{@link ${contents}}`); +}); diff --git a/tests/lib/feature-query.js b/tests/lib/feature-query.js new file mode 100644 index 0000000..7d69430 --- /dev/null +++ b/tests/lib/feature-query.js @@ -0,0 +1,71 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { FeatureQuery } from '../../tools/lib/feature-query.js'; + +test('FeatureQuery.mergeComplexFeature', (t) => { + const fq = new FeatureQuery({}); + let mcf; + + // If all dependencie match, then super.mergeComplexFeature returns dependencies + mcf = fq.mergeComplexFeature([{}, {}]); + t.deepEqual(mcf, { dependencies: [] }); + + // If all dependencie don't equal the first, then super.mergeComplexFeature returns undefined + mcf = fq.mergeComplexFeature([ + { disallow_for_service_workers: false }, + { disallow_for_service_workers: true }, + ]); + t.deepEqual(mcf, undefined); +}); + +test('FeatureQuery.filter', (t) => { + const fq = new FeatureQuery({}); + let f; + + // If `allowlist` length is greater than `0` return `false` + f = fq.filter({ allowlist: ['allowed'] }); + t.is(f, false); + + // If `command_line_switch` is defined return `false` + f = fq.filter({ command_line_switch: 'switch' }); + t.is(f, false); + + // If `session_types` length is greater than `0` don't return `false` (currently this is a passthrough) + f = fq.filter({ session_types: ['kiosk'] }); + t.is(f, true); + + // If `session_types` exists and includes `'regular'` don't return `false` (currently this is a passthrough) + f = fq.filter({ session_types: ['regular'] }); + t.is(f, true); + + // If `matches` length is greater than `0` return `false` + f = fq.filter({ matches: [''] }); + t.is(f, false); + + // If `matches` exists and includes `''` don't return `false` + f = fq.filter({ matches: [''] }); + t.is(f, true); + + // If `extension_types` exists and includes `'extension'` or `'platform_app'` return `true` + f = fq.filter({ extension_types: ['extension', 'platform_app'] }); + t.is(f, true); + + // If `extension_types` exists and does not include `'extension'` or `'platform_app'` return `false` + f = fq.filter({ extension_types: ['hosted_app'] }); + t.is(f, false); +}); \ No newline at end of file diff --git a/tests/lib/idl-convert.js b/tests/lib/idl-convert.js new file mode 100644 index 0000000..53ac18e --- /dev/null +++ b/tests/lib/idl-convert.js @@ -0,0 +1,36 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { convertFromIdl } from '../../tools/lib/idl-convert.js'; +import { fetchAllTo } from '../../tools/lib/chrome-source.js'; +import { toolsPaths } from '../../tools/prepare.js'; +import { chromeVersions } from '../../tools/lib/chrome-versions.js'; + +test('convertFromIdl', async (t) => { + // Make sure IDL still generated + const filename = '.assets/sample.idl'; + let root = process.cwd(); + + if (process.env.CI) { + const {head} = await chromeVersions() + await fetchAllTo(root, toolsPaths, head); + } + + await t.notThrowsAsync(() => + convertFromIdl(root, filename) + ); +}); diff --git a/tests/line-hash.js b/tests/lib/line-hash.js similarity index 93% rename from tests/line-hash.js rename to tests/lib/line-hash.js index 789585e..e78b969 100644 --- a/tests/line-hash.js +++ b/tests/lib/line-hash.js @@ -15,7 +15,7 @@ */ import test from 'ava'; -import { generateLinesHash } from '../tools/lib/line-hash.js'; +import { generateLinesHash } from '../../tools/lib/line-hash.js'; test('line-hash', t => { const s1 = `/** This comment can change */ diff --git a/tests/render-context.js b/tests/lib/render-context.js similarity index 95% rename from tests/render-context.js rename to tests/lib/render-context.js index 227cb73..2b6c283 100644 --- a/tests/render-context.js +++ b/tests/lib/render-context.js @@ -15,9 +15,9 @@ */ import test from 'ava'; -import { RenderContext } from '../tools/lib/render-context.js'; -import { EmptyRenderOverride } from '../tools/override.js'; -import * as chromeTypes from '../types/chrome.js'; +import { RenderContext } from '../../tools/lib/render-context.js'; +import { EmptyRenderOverride } from '../../tools/override.js'; +import * as chromeTypes from '../../types/chrome.js'; // TODO: This isn't as exhaustive as it should be. Chrome has a variety of types we should be diff --git a/tests/semaphore.js b/tests/lib/semaphore.js similarity index 94% rename from tests/semaphore.js rename to tests/lib/semaphore.js index 2818b05..00a79ec 100644 --- a/tests/semaphore.js +++ b/tests/lib/semaphore.js @@ -15,7 +15,7 @@ */ import test from 'ava'; -import { Semaphore } from '../tools/lib/semaphore.js'; +import { Semaphore } from '../../tools/lib/semaphore.js'; test('semaphore', async t => { const s = new Semaphore(); diff --git a/tests/lib/spawn-helper.js b/tests/lib/spawn-helper.js new file mode 100644 index 0000000..a47c1c0 --- /dev/null +++ b/tests/lib/spawn-helper.js @@ -0,0 +1,47 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { + rootDir, + run, + toolInvoke +} from '../../tools/lib/spawn-helper.js'; + +test('rootDir',(t) => { + // rootDir is correct + t.is(rootDir, `${process.cwd()}/`); +}); + +test('run', (t) => { + // Runs code in terminal + t.notThrows(() => run(['node', '-v'])); + + // Throws error with invalid command + t.throws(() => run(['abc'])); + + // Throws error when status is output + t.throws(() => run(['node', '-e', 'process.exit(1);'])); +}) + +test('toolInvoke', async (t) => { + // Don't run because it actually runs full command, does work though + // // Runs file + // t.notThrows(() => toolInvoke('prepare-history.js')); + + // Throws error with invalid file + t.throws(() => toolInvoke('')); +}); diff --git a/tests/traverse.js b/tests/lib/traverse.js similarity index 98% rename from tests/traverse.js rename to tests/lib/traverse.js index bdfe59c..72d7043 100644 --- a/tests/traverse.js +++ b/tests/lib/traverse.js @@ -15,8 +15,8 @@ */ import test from 'ava'; -import * as traverse from '../tools/lib/traverse.js'; -import * as chromeTypes from '../types/chrome.js'; +import * as traverse from '../../tools/lib/traverse.js'; +import * as chromeTypes from '../../types/chrome.js'; // Helper used for forEach tests below. diff --git a/tests/override.js b/tests/override.js new file mode 100644 index 0000000..428248b --- /dev/null +++ b/tests/override.js @@ -0,0 +1,494 @@ +/** + * Copyright 2022 Google LLC + * + * 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 test from 'ava'; +import { + FeatureQueryAll, + EmptyRenderOverride, + RenderOverride, +} from '../tools/override.js'; +import { FeatureQuery } from '../tools/lib/feature-query.js'; + +const featureQuery = new FeatureQuery({ + spec: { + dependencies: ['dependency-one'], + channel: 'beta', + }, +}); + +/** + * @type {import('../types/chrome.js').EventSpec} + */ +const eventSpec = { + name: 'hi', + returns: undefined, + parameters: [], + type: 'function', + options: {}, + extraParameters: [], + filters: [], + $ref: '$ref', +}; +eventSpec.returns = eventSpec; +eventSpec.filters?.push(eventSpec); +eventSpec.extraParameters?.push(eventSpec); + +const validId = 'api:declarativeContent.onPageChanged'; + +test('FeatureQueryAll.mergeComplexFeature', (t) => { + const fqa = new FeatureQueryAll({}); + let mcf; + + // Returns super.mergeComplexFeature result when not undefined + mcf = fqa.mergeComplexFeature([]); + t.deepEqual(mcf, { dependencies: [] }); + + // If all dependencie match, then super.mergeComplexFeature returns dependencies + mcf = fqa.mergeComplexFeature([{}, {}]); + t.deepEqual(mcf, { dependencies: [] }); + + // If all dependencie don't equal the first, then super.mergeComplexFeature returns undefined + mcf = fqa.mergeComplexFeature([ + { disallow_for_service_workers: false }, + { disallow_for_service_workers: true }, + ]); + t.deepEqual(mcf, undefined); +}); + +test('EmptyRenderOverride.isVisible', (t) => { + const erq = new EmptyRenderOverride(); + let iv; + + // `isVisible` always returns true + iv = erq.isVisible([]); + t.is(iv, true); + iv = erq.isVisible(); + t.is(iv, true); + iv = erq.isVisible(1, 2); + t.is(iv, true); +}); + +test('EmptyRenderOverride.objectTemplatesFor', (t) => { + const erq = new EmptyRenderOverride(); + let oTR; + + // `objectTemplatesFor` always returns undefined + oTR = erq.objectTemplatesFor([]); + t.is(oTR, undefined); + oTR = erq.objectTemplatesFor(); + t.is(oTR, undefined); + oTR = erq.objectTemplatesFor('hello world'); + t.is(oTR, undefined); +}); + +test('EmptyRenderOverride.typeOverride', (t) => { + const erq = new EmptyRenderOverride(); + let tO; + + // `typeOverride` always returns undefined + tO = erq.typeOverride([]); + t.is(tO, undefined); + tO = erq.typeOverride(); + t.is(tO, undefined); + tO = erq.typeOverride(1, 2); + t.is(tO, undefined); +}); + +test('EmptyRenderOverride.tagsFor', (t) => { + const erq = new EmptyRenderOverride(); + let tf; + + // `tagsFor` always returns undefined + tf = erq.tagsFor([]); + t.is(tf, undefined); + tf = erq.tagsFor(); + t.is(tf, undefined); + tf = erq.tagsFor(1, 2); + t.is(tf, undefined); +}); + +test('EmptyRenderOverride.rewriteComment', (t) => { + const erq = new EmptyRenderOverride(); + let rc; + + // `rewriteComment` always returns undefined + rc = erq.rewriteComment([]); + t.is(rc, undefined); + rc = erq.rewriteComment(); + t.is(rc, undefined); + rc = erq.rewriteComment(1, 2, 3); + t.is(rc, undefined); +}); + +test('RenderOverride', (t) => { + // Doesn't throw error with valid arguments + t.notThrows(() => new RenderOverride({}, featureQuery)); +}); + +test('RenderOverride.isVisible', (t) => { + let ro = new RenderOverride({}, featureQuery); + let iv; + + // Not visible if internal API aren't specifically disallowed + iv = ro.isVisible({}, 'api:test'); + t.is(iv, false); + + // Not visible if invalid id + iv = ro.isVisible({}, 'Private'); + t.is(iv, false); + iv = ro.isVisible({}, 'Internal'); + t.is(iv, false); + + // Not visible if invalid id + iv = ro.isVisible({}, 'Private'); + t.is(iv, false); + iv = ro.isVisible({}, 'Internal'); + t.is(iv, false); + + // If specific API's return `true` + iv = ro.isVisible({}, 'api:contextMenus.OnClickData'); + t.is(iv, true); + iv = ro.isVisible({}, 'api:notifications.NotificationBitmap'); + t.is(iv, true); + + // If `spec.nodoc` is truthy return `false` + iv = ro.isVisible({ nodoc: true }, 'test'); + t.is(iv, false); + // If `spec.nodoc` is falsey return `true` + iv = ro.isVisible({ nodoc: false }, 'test'); + t.is(iv, true); +}); + +test('RenderOverride.objectTemplatesFor', (t) => { + let ro = new RenderOverride({}, featureQuery); + let otr; + + // If id is special case, return appropriate type + otr = ro.objectTemplatesFor('api:events.Event'); + t.is(otr, 'H extends (...args: any) => void, C = void, A = void'); + otr = ro.objectTemplatesFor('api:events.Rule'); + t.is(otr, 'C = any, A = any'); + otr = ro.objectTemplatesFor('api:contentSettings.ContentSetting'); + t.is(otr, 'T'); + otr = ro.objectTemplatesFor('api:types.ChromeSetting'); + t.is(otr, 'T'); + + // If id is not special case, return nothing + otr = ro.objectTemplatesFor(''); + t.is(otr, undefined); +}); + +test('RenderOverride.rewriteEventsToProperties', (t) => { + let ro = new RenderOverride({}, featureQuery); + /** @type {any} */ + let retp; + + // If no events return undefined + retp = ro.rewriteEventsToProperties({}, 'id'); + t.is(retp, undefined); + retp = ro.rewriteEventsToProperties({ events: [] }, 'id'); + t.is(retp, undefined); + + // Throw error if `options.supportsListeners === false` and no `options.conditions` + t.throws(() => + ro.rewriteEventsToProperties( + { + events: [ + { + ...eventSpec, + options: { supportsListeners: false, conditions: [] }, + }, + ], + }, + 'id' + ) + ); + + // Throw error if `options.supportsListeners === false` and no `options.actions` + t.throws(() => + ro.rewriteEventsToProperties( + { + events: [ + { + ...eventSpec, + options: { + supportsListeners: false, + conditions: ['a'], + actions: [], + }, + }, + ], + }, + 'id' + ) + ); + + // Returns expected object + retp = ro.rewriteEventsToProperties({ events: [eventSpec] }, validId); + t.is(!!retp, true); + t.is(retp._event, true); + t.is(retp.events, undefined); + t.deepEqual(Object.keys(retp), ['events', 'properties', '_event']); + t.deepEqual(Object.keys(retp.properties), ['hi']); + t.is(retp.properties.hi['$ref'], 'CustomChromeEvent'); + t.is(retp.properties.hi._event, true); + t.is(retp.properties.hi.name, 'hi'); + t.is(Array.isArray(retp.properties.hi.value), true); + t.deepEqual(Object.keys(retp.properties.hi), [ + 'name', + '$ref', + 'value', + '_event', + ]); +}); + +test('RenderOverride.typeOverride', (t) => { + let ro = new RenderOverride({}, featureQuery); + /** @type {import('../types/chrome.js').TypeSpec|import('../types/chrome.js').NamespaceSpec|undefined} */ + let to; + + // If `rewriteEventsToProperties` is truthy return object + to = ro.typeOverride({ ...eventSpec, events: [eventSpec] }, validId); + t.is(to?.constructor, {}.constructor); + + // If `id` is `api:contextMenus` and has `OnClickData` event return `undefined` + to = ro.typeOverride( + // @ts-ignore + { ...eventSpec, types: [{ id: 'OnClickData' }] }, + 'api:contextMenus' + ); + t.is(to, undefined); + + // If `id` is `api:contextMenus` and doesn't have `OnClickData` event return object + to = ro.typeOverride( + // @ts-ignore + { ...eventSpec, types: [{ id: 'NotOnClickData' }] }, + 'api:contextMenus' + ); + t.is(to?.constructor, {}.constructor); + + // If `id` is `api:contextMenus.onClicked` and doesn't have valid `spec.value` return `undefined` + to = ro.typeOverride(eventSpec, 'api:contextMenus.onClicked'); + t.is(to, undefined); + to = ro.typeOverride( + { ...eventSpec, value: 'events' }, + 'api:contextMenus.onClicked' + ); + t.is(to, undefined); + to = ro.typeOverride( + { ...eventSpec, value: [null, {}] }, + 'api:contextMenus.onClicked' + ); + t.is(to, undefined); + + // If `id` is `api:contextMenus.onClicked` and has valid `spec.value` return object + to = ro.typeOverride( + { ...eventSpec, value: [{}, { $ref: 'ref' }] }, + 'api:contextMenus.onClicked' + ); + t.is(to?.constructor, {}.constructor); + + // If `id` is a special switch case return $ref + to = ro.typeOverride(eventSpec, 'api:events.Event.addListener.callback'); + // @ts-ignore + t.is(to.$ref, 'H'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + to = ro.typeOverride(eventSpec, 'api:events.Event.removeListener.callback'); + // @ts-ignore + t.is(to.$ref, 'H'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + to = ro.typeOverride(eventSpec, 'api:events.Event.hasListener.callback'); + // @ts-ignore + t.is(to.$ref, 'H'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + to = ro.typeOverride(eventSpec, 'api:events.Event.addRules.rules[]'); + // @ts-ignore + t.is(to.$ref, 'Rule'); + // @ts-ignore + t.is(Array.isArray(to.value), true); + // @ts-ignore + t.is(to.value.length, 3); + t.deepEqual(Object.keys(to || {}), ['$ref', 'value']); + to = ro.typeOverride(eventSpec, 'api:events.Event.addRules.callback.rules[]'); + // @ts-ignore + t.is(to.$ref, 'Rule'); + // @ts-ignore + t.is(Array.isArray(to.value), true); + // @ts-ignore + t.is(to.value.length, 3); + t.deepEqual(Object.keys(to || {}), ['$ref', 'value']); + to = ro.typeOverride(eventSpec, 'api:events.Event.getRules.callback.rules[]'); + // @ts-ignore + t.is(to.$ref, 'Rule'); + // @ts-ignore + t.is(Array.isArray(to.value), true); + // @ts-ignore + t.is(to.value.length, 3); + t.deepEqual(Object.keys(to || {}), ['$ref', 'value']); + + // Fix and return bad runtime.Port APIs in older Chrome versions + to = ro.typeOverride( + { ...eventSpec, $ref: 'events.Event', value: false }, + 'id' + ); + // @ts-ignore + t.is(to?.value.length, 2); + // @ts-ignore + t.is(Array.isArray(to?.value), true); + t.is(to?.constructor, {}.constructor); + + // Fix and return bad contextMenusInternal references in older Chrome versions + to = ro.typeOverride({ ...eventSpec, $ref: 'contextMenusInternal.' }, 'id'); + // // @ts-ignore + // t.is(to.$ref, `contextMenus.`); + // t.is(to?.constructor, {}.constructor); + t.is(to, undefined); + + // Fix and return isInstanceOf usages + to = ro.typeOverride({ ...eventSpec, isInstanceOf: 'Promise' }, 'id'); + // @ts-ignore + t.is(to.$ref, `Promise`); + // @ts-ignore + t.is(to?.value.length, 2); + // @ts-ignore + t.is(Array.isArray(to?.value), true); + t.deepEqual(Object.keys(to || {}), ['$ref', 'value']); + t.is(to?.constructor, {}.constructor); + + to = ro.typeOverride({ ...eventSpec, isInstanceOf: 'global' }, 'id'); + // @ts-ignore + t.is(to.$ref, `Window`); + t.deepEqual(Object.keys(to || {}), ['$ref']); + t.is(to?.constructor, {}.constructor); + + // If `spec.type === 'any'` and `id` is a special switch case return $ref + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:events.Rule.conditions[]' + ); + // @ts-ignore + t.is(to.$ref, 'C'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:events.Rule.actions[]' + ); + // @ts-ignore + t.is(to.$ref, 'A'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:contentSettings.ContentSetting.get.return.setting' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:contentSettings.ContentSetting.get.callback.details.setting' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:types.ChromeSetting.set.details.setting' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:types.ChromeSetting.onChange.details.value' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:types.ChromeSetting.get.return.value' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:types.ChromeSetting.get.callback.details.value' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); + + to = ro.typeOverride( + { ...eventSpec, type: 'any' }, + 'api:types.ChromeSetting.set.details.value' + ); + // @ts-ignore + t.is(to.$ref, 'T'); + t.deepEqual(Object.keys(to || {}), ['$ref']); +}); + +test('RenderOverride.bestChannelFor', (t) => { + const ro = new RenderOverride({}, featureQuery); + let bcf; + + // If no best channel found return `stable` + bcf = ro.bestChannelFor('api:test'); + t.is(bcf, 'stable'); + + // If best channel found return it + bcf = ro.bestChannelFor('spec'); + t.is(bcf, 'beta'); +}); + +test('RenderOverride.completeTagsFor', (t) => { + const ro = new RenderOverride({}, featureQuery); + let ctf; + + // Returns completed tag + ctf = ro.completeTagsFor({ ...eventSpec, _event: true }, 'api:test'); + t.is(ctf[0].name, 'event'); +}); + +test('RenderOverride.tagsFor', (t) => { + const ro = new RenderOverride({}, featureQuery); + let tf; + + // Returns array + tf = ro.tagsFor(eventSpec, validId); + t.is(Array.isArray(tf), true); + tf = ro.tagsFor({ ...eventSpec, _event: true }, validId); + t.is(Array.isArray(tf), true); +}); + +test('RenderOverride.rewriteComment', (t) => { + const ro = new RenderOverride( + { declarativeContent: { namespace: 'declarativeContent' } }, + featureQuery + ); + let rc; + + // Returns `undefined` if no rewrite + rc = ro.rewriteComment('', validId); + t.is(rc, undefined); +}); diff --git a/tools/lib/cache-helper.js b/tools/lib/cache-helper.js index 73cc68d..dcff20c 100644 --- a/tools/lib/cache-helper.js +++ b/tools/lib/cache-helper.js @@ -30,7 +30,7 @@ export const { pathname: cacheDir } = new URL('../../.cache', import.meta.url); /** * @param {string} name to fetch - * @param {number=} expiry default 48hr + * @param {number} [expiry] default 48hr */ export function readFromCache(name, expiry = 48 * hourMs) { const p = path.join(cacheDir, name); diff --git a/tools/lib/chrome-source.js b/tools/lib/chrome-source.js index 290da47..75a7743 100644 --- a/tools/lib/chrome-source.js +++ b/tools/lib/chrome-source.js @@ -33,7 +33,7 @@ import fetch from 'node-fetch'; * @param {string} chromePath * @return {string} */ -function urlFor(revision, chromePath) { +export function urlFor(revision, chromePath) { return `https://chromium.googlesource.com/chromium/src/+archive/${revision}/${chromePath}.tar.gz`; } @@ -46,7 +46,7 @@ function urlFor(revision, chromePath) { * @param {string} revision revision to fetch * @return {Promise<(string[]?)[]>} files written for paths, null for skipped */ -export async function fetchAllTo(targetPath, chromePaths, revision) { +export function fetchAllTo(targetPath, chromePaths, revision) { // This removes any trailing "/" from the paths. chromePaths = chromePaths.map(chromePath => path.join(chromePath, '.')); diff --git a/tools/lib/chrome-versions.js b/tools/lib/chrome-versions.js index d111700..c3b4c61 100644 --- a/tools/lib/chrome-versions.js +++ b/tools/lib/chrome-versions.js @@ -43,7 +43,7 @@ const omahaProxyUrl = 'https://omahaproxy.appspot.com/all.json'; /** * @param {string} tag */ -function splitChromeRelease(tag) { +export async function splitChromeRelease(tag) { // Look for versions like "88.0.4324.47". Ignore other tags. const parts = tag.split('.'); if (parts.length !== 4) { @@ -99,7 +99,7 @@ export async function chromeVersions() { continue; } const tag = rawTag.substr(tagPrefix.length); - const info = splitChromeRelease(tag); + const info = await splitChromeRelease(tag); if (!info) { continue; } diff --git a/tools/lib/comment.js b/tools/lib/comment.js index 4297acc..a786bc7 100644 --- a/tools/lib/comment.js +++ b/tools/lib/comment.js @@ -38,7 +38,7 @@ const backtickSymbolRegexp = /`([\w\.]+)`/g; * @param {string} text * @param {RegExp} re */ -function regexpParts(text, re) { +export function regexpParts(text, re) { /** @type {string[]} */ const parts = []; @@ -63,7 +63,7 @@ function regexpParts(text, re) { * @param {string[]} parts * @param {(s: string) => string} helper */ -function transformEven(parts, helper) { +export function transformEven(parts, helper) { return parts.map((s, i) => { if ((i % 2) === 0) { return helper(s); @@ -153,7 +153,7 @@ const hrefRegexp = /]*?)href\s*=\s*(["'])(.*?)\2(.*?)>(.*?)<\/a>/gms; * @param {string} raw * @return {boolean} is this probably a URL? */ -function isUrl(raw) { +export function isUrl(raw) { try { new URL(raw); return true; diff --git a/tools/override.js b/tools/override.js index 31e6493..f40a843 100644 --- a/tools/override.js +++ b/tools/override.js @@ -89,7 +89,7 @@ export class EmptyRenderOverride { } /** - * @return {string | undefined} + * @return {string|undefined} */ rewriteComment(s, id, tagName) { return undefined; @@ -341,7 +341,7 @@ export class RenderOverride extends EmptyRenderOverride { ...namespace, types: [ ...namespace.types ?? [], - ...this.#api['contextMenusInternal'].types ?? [], + ...this.#api['contextMenusInternal']?.types ?? [], ], }; } @@ -401,6 +401,7 @@ export class RenderOverride extends EmptyRenderOverride { } // Fix bad contextMenusInternal references in older Chrome versions. + // @TODO should this return? if (spec.$ref && spec.$ref.startsWith('contextMenusInternal.') && !id.startsWith('api:contextMenusInternal.')) { spec.$ref = spec.$ref.replace(/^contextMenusInternal\./, 'contextMenus.'); } @@ -426,8 +427,6 @@ export class RenderOverride extends EmptyRenderOverride { case 'api:contentSettings.ContentSetting.get.return.setting': case 'api:contentSettings.ContentSetting.get.callback.details.setting': case 'api:types.ChromeSetting.set.details.setting': - return { $ref: 'T' }; - case 'api:types.ChromeSetting.onChange.details.value': case 'api:types.ChromeSetting.get.return.value': case 'api:types.ChromeSetting.get.callback.details.value': @@ -445,7 +444,7 @@ export class RenderOverride extends EmptyRenderOverride { /** @type {chromeTypes.Channel | undefined} */ let bestChannel = undefined; - this.#fq.checkFeature(id, (f, otherId) => { + this.#fq.checkFeature(id, (f) => { bestChannel = mostReleasedChannel(bestChannel, f.channel); }); @@ -644,8 +643,9 @@ export class RenderOverride extends EmptyRenderOverride { /** * @param {string} s * @param {string} id + * @return {string|undefined} */ - rewriteComment(s, id) { + rewriteComment(s, id) { const namespace = namespaceNameFromId(id); const update = this.#commentRewriter(namespace, s); if (update !== s) { diff --git a/tools/prepare.js b/tools/prepare.js index 5ea3f72..d95600d 100755 --- a/tools/prepare.js +++ b/tools/prepare.js @@ -51,7 +51,7 @@ const definitionPaths = [ /** * Fetch these folders to run the IDL => JSON converter. */ -const toolsPaths = [ +export const toolsPaths = [ 'tools/json_schema_compiler', 'tools/json_comment_eater', 'ppapi/generators',