Allow to write and run Typescript tests

Change-Id: I695901dca367b342705b7d86b001d2b400510112
This commit is contained in:
Dmitrii Filippov
2020-10-21 15:35:41 +02:00
committed by Luca Milanesio
parent 702c51fb21
commit 68aac7fb2e
24 changed files with 619 additions and 285 deletions

View File

@@ -280,28 +280,17 @@ module.exports = {
// it catches almost all errors related to invalid usage of this.
"no-invalid-this": "off",
"node/no-extraneous-import": "off",
// Typescript already checks for undef
"no-undef": "off",
"jsdoc/no-types": 2,
},
"parserOptions": {
"project": path.resolve(__dirname, "./tsconfig_eslint.json"),
}
},
{
"files": ["**/*.ts"],
"excludedFiles": "*.d.ts",
"rules": {
// Custom rule from the //tools/js/eslint-rules directory.
// See //tools/js/eslint-rules/README.md for details
"ts-imports-js": 2,
}
},
{
"files": ["**/*.d.ts"],
"rules": {
// See details in the //tools/js/eslint-rules/report-ts-error.js file.
"report-ts-error": "error",
}
},
{
"files": ["*.html", "test.js", "test-infra.js"],
"rules": {

View File

@@ -3,7 +3,8 @@ load("//tools/js:eslint.bzl", "eslint")
package(default_visibility = ["//visibility:public"])
# This list must be in sync with the "include" list in the tsconfig.json file
# This list must be in sync with the "include" list in the follwoing files:
# tsconfig.json, tsconfig_bazel.json, tsconfig_bazel_test.json
src_dirs = [
"constants",
"elements",
@@ -27,6 +28,7 @@ compiled_pg_srcs = compile_ts(
]],
exclude = [
"**/*_test.js",
"**/*_test.ts",
],
),
# The same outdir also appears in the following files:
@@ -40,6 +42,7 @@ compiled_pg_srcs_with_tests = compile_ts(
[
"**/*.js",
"**/*.ts",
"test/@types/*.d.ts",
],
exclude = [
"node_modules/**",
@@ -48,6 +51,7 @@ compiled_pg_srcs_with_tests = compile_ts(
"rollup.config.js",
],
),
include_tests = True,
# The same outdir also appears in the following files:
# wct_test.sh
# karma.conf.js

View File

@@ -22,7 +22,7 @@ import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {sortComments} from '../../../utils/comment-util.js';
import {Side} from '../../../constants/constants.js';
import {generateChange} from '../../../test/test-utils';
import {generateChange} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-diff-host');

View File

@@ -19,7 +19,7 @@ import '../../../test/common-test-setup-karma.js';
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus} from '../../../constants/constants.js';
import {generateChange, TestKeyboardShortcutBinder} from '../../../test/test-utils';
import {generateChange, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {_testOnly_findCommentById} from '../gr-comment-api/gr-comment-api.js';

View File

@@ -21,7 +21,7 @@ import {GrPopupInterface} from './gr-popup-interface.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {createIronOverlayBackdropStyleEl} from '../../../test/test-utils';
import {createIronOverlayBackdropStyleEl} from '../../../test/test-utils.js';
class GrUserTestPopupElement extends PolymerElement {
static get is() { return 'gr-user-test-popup'; }

View File

@@ -551,6 +551,14 @@ export class ShortcutManager {
private readonly bindings = new Map<Shortcut, string[]>();
public _testOnly_getBindings() {
return this.bindings;
}
public _testOnly_isEmpty() {
return this.activeHosts.size === 0 && this.listeners.size === 0;
}
private readonly listeners = new Set<ShortcutListener>();
bindShortcut(shortcut: Shortcut, ...bindings: string[]) {

View File

@@ -34,7 +34,7 @@ def _get_ts_output_files(outdir, srcs):
result.append(_get_ts_compiled_path(outdir, f))
return result
def compile_ts(name, srcs, ts_outdir):
def compile_ts(name, srcs, ts_outdir, include_tests = False):
"""Compiles srcs files with the typescript compiler
Args:
@@ -50,16 +50,31 @@ def compile_ts(name, srcs, ts_outdir):
# List of files produced by the typescript compiler
generated_js = _get_ts_output_files(ts_outdir, srcs)
all_srcs = srcs + [
":tsconfig.json",
":tsconfig_bazel.json",
"@ui_npm//:node_modules",
]
ts_project = "tsconfig_bazel.json"
if include_tests:
all_srcs = all_srcs + [
":tsconfig_bazel_test.json",
"@ui_dev_npm//:node_modules",
]
ts_project = "tsconfig_bazel_test.json"
# Run the compiler
native.genrule(
name = ts_rule_name,
srcs = srcs + [
":tsconfig.json",
"@ui_npm//:node_modules",
],
srcs = all_srcs,
outs = generated_js,
cmd = " && ".join([
"$(location //tools/node_tools:tsc-bin) --project $(location :tsconfig.json) --outdir $(RULEDIR)/" + ts_outdir + " --baseUrl ./external/ui_npm/node_modules",
"$(location //tools/node_tools:tsc-bin) --project $(location :" +
ts_project +
") --outdir $(RULEDIR)/" +
ts_outdir +
" --baseUrl ./external/ui_npm/node_modules/",
]),
tools = ["//tools/node_tools:tsc-bin"],
)

View File

@@ -57,10 +57,16 @@ declare global {
const security = window.security;
export function installPolymerResin(safeTypesBridge: SafeTypeBridge) {
export const _testOnly_defaultResinReportHandler =
security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
export function installPolymerResin(
safeTypesBridge: SafeTypeBridge,
reportHandler = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER
) {
window.security.polymer_resin.install({
allowedIdentifierPrefixes: [''],
reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
reportHandler,
safeTypesBridge,
});
}

View File

@@ -14,7 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const grReportingMock = {
import {ReportingService, Timer} from './gr-reporting';
export class MockTimer implements Timer {
end(): this {
return this;
}
reset(): this {
return this;
}
withMaximum(_: number): this {
return this;
}
}
export const grReportingMock: ReportingService = {
appStarted: () => {},
beforeLocationChanged: () => {},
changeDisplayed: () => {},
@@ -25,7 +41,7 @@ export const grReportingMock = {
diffViewFullyLoaded: () => {},
fileListDisplayed: () => {},
getTimer: () => {
return {end: () => {}};
return new MockTimer();
},
locationChanged: () => {},
onVisibilityChange: () => {},

View File

@@ -0,0 +1,27 @@
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* 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.
*/
declare module 'sinon/pkg/sinon-esm' {
// sinon-esm doesn't have it's own d.ts, reexport all types from sinon
// This is a trick - @types/sinon adds interfaces and sinon instance
// to a global variables/namespace. We reexport it here, so we
// can use in our code when importing sinon-esm
// eslint-disable-next-line import/no-default-export
export default sinon;
const sinon: Sinon.SinonStatic;
export {SinonSpy};
}

View File

@@ -14,14 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './common-test-setup.js';
import '@polymer/test-fixture/test-fixture.js';
import 'chai/chai.js';
self.assert = window.chai.assert;
self.expect = window.chai.expect;
import './common-test-setup';
import '@polymer/test-fixture/test-fixture';
import 'chai/chai';
declare global {
interface Window {
flush: typeof flushImpl;
fixtureFromTemplate: typeof fixtureFromTemplateImpl;
fixtureFromElement: typeof fixtureFromElementImpl;
}
let flush: typeof flushImpl;
let fixtureFromTemplate: typeof fixtureFromTemplateImpl;
let fixtureFromElement: typeof fixtureFromElementImpl;
}
// Workaround for https://github.com/karma-runner/karma-mocha/issues/227
let unhandledError = null;
let unhandledError: ErrorEvent;
window.addEventListener('error', e => {
// For uncaught error mochajs doesn't print the full stack trace.
@@ -31,7 +40,7 @@ window.addEventListener('error', e => {
unhandledError = e;
});
let originalOnBeforeUnload;
let originalOnBeforeUnload: typeof window.onbeforeunload;
suiteSetup(() => {
// This suiteSetup() method is called only once before all tests
@@ -39,7 +48,7 @@ suiteSetup(() => {
// Can't use window.addEventListener("beforeunload",...) here,
// the handler is raised too late.
originalOnBeforeUnload = window.onbeforeunload;
window.onbeforeunload = e => {
window.onbeforeunload = function (e: BeforeUnloadEvent) {
// If a test reloads a page, we can't prevent it.
// However we can print earror and the stack trace with assert.fail
try {
@@ -48,7 +57,9 @@ suiteSetup(() => {
console.error('Page reloading attempt detected.');
console.error(e.stack.toString());
}
originalOnBeforeUnload(e);
if (originalOnBeforeUnload) {
originalOnBeforeUnload.call(this, e);
}
};
});
@@ -64,18 +75,18 @@ suiteTeardown(() => {
// Keep the original one for use in test utils methods.
const nativeSetTimeout = window.setTimeout;
function flushImpl(): Promise<void>;
function flushImpl(callback: () => void): void;
/**
* Triggers a flush of any pending events, observations, etc and calls you back
* after they have been processed if callback is passed; otherwise returns
* promise.
*
* @param {function()} callback
*/
function flush(callback) {
function flushImpl(callback?: () => void): Promise<void> | void {
// Ideally, this function would be a call to Polymer.dom.flush, but that
// doesn't support a callback yet
// (https://github.com/Polymer/polymer-dev/issues/851)
window.Polymer.dom.flush();
(window as any).Polymer.dom.flush();
if (callback) {
nativeSetTimeout(callback, 0);
} else {
@@ -85,19 +96,12 @@ function flush(callback) {
}
}
self.flush = flush;
self.flush = flushImpl;
class TestFixtureIdProvider {
static get instance() {
if (!TestFixtureIdProvider._instance) {
TestFixtureIdProvider._instance = new TestFixtureIdProvider();
}
return TestFixtureIdProvider._instance;
}
public static readonly instance: TestFixtureIdProvider = new TestFixtureIdProvider();
constructor() {
this.fixturesCount = 1;
}
private fixturesCount = 1;
generateNewFixtureId() {
this.fixturesCount++;
@@ -105,22 +109,24 @@ class TestFixtureIdProvider {
}
}
interface TagTestFixture<T extends Element> {
instantiate(model?: unknown): T;
}
class TestFixture {
constructor(fixtureId) {
this.fixtureId = fixtureId;
}
constructor(private readonly fixtureId: string) {}
/**
* Create an instance of a fixture's template.
*
* @param {Object} model - see Data-bound sections at
* @param model - see Data-bound sections at
* https://www.webcomponents.org/element/@polymer/test-fixture
* @return {HTMLElement | HTMLElement[]} - if the fixture's template contains
* @return - if the fixture's template contains
* a single element, returns the appropriated instantiated element.
* Otherwise, it return an array of all instantiated elements from the
* template.
*/
instantiate(model) {
instantiate(model?: unknown): HTMLElement | HTMLElement[] {
// The window.fixture method is defined in common-test-setup.js
return window.fixture(this.fixtureId, model);
}
@@ -153,10 +159,9 @@ class TestFixture {
* });
* }
*
* @param {HTMLTemplateElement} template - a template for a fixture
* @return {TestFixture} - the instance of TestFixture class
* @param template - a template for a fixture
*/
function fixtureFromTemplate(template) {
function fixtureFromTemplateImpl(template: HTMLTemplateElement): TestFixture {
const fixtureId = TestFixtureIdProvider.instance.generateNewFixtureId();
const testFixture = document.createElement('test-fixture');
testFixture.setAttribute('id', fixtureId);
@@ -183,14 +188,17 @@ function fixtureFromTemplate(template) {
* });
* }
*
* @param {HTMLTemplateElement} template - a template for a fixture
* @return {TestFixture} - the instance of TestFixture class
* @param tagName - a template for a fixture is <tagName></tagName>
*/
function fixtureFromElement(tagName) {
function fixtureFromElementImpl<T extends keyof HTMLElementTagNameMap>(
tagName: T
): TagTestFixture<HTMLElementTagNameMap[T]> {
const template = document.createElement('template');
template.innerHTML = `<${tagName}></${tagName}>`;
return fixtureFromTemplate(template);
return (fixtureFromTemplate(template) as unknown) as TagTestFixture<
HTMLElementTagNameMap[T]
>;
}
window.fixtureFromTemplate = fixtureFromTemplate;
window.fixtureFromElement = fixtureFromElement;
window.fixtureFromTemplate = fixtureFromTemplateImpl;
window.fixtureFromElement = fixtureFromElementImpl;

View File

@@ -15,51 +15,81 @@
* limitations under the License.
*/
// This should be the first import to install handler before any other code
import './source-map-support-install.js';
import './source-map-support-install';
// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
import '../scripts/bundled-polymer.js';
import 'polymer-resin/standalone/polymer-resin.js';
import '@polymer/iron-test-helpers/iron-test-helpers.js';
import './test-router.js';
import '../scripts/bundled-polymer';
import '@polymer/iron-test-helpers/iron-test-helpers';
import './test-router';
import {_testOnlyInitAppContext} from './test-app-context-init';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api.js';
import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
import {cleanupTestUtils, TestKeyboardShortcutBinder} from './test-utils.js';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api';
import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface';
import {
cleanupTestUtils,
getCleanupsCount,
registerTestCleanup,
TestKeyboardShortcutBinder,
} from './test-utils';
import {flushDebouncers} from '@polymer/polymer/lib/utils/debounce';
import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import sinon from 'sinon/pkg/sinon-esm.js';
import {safeTypesBridge} from '../utils/safe-types-util.js';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit.js';
import {initGlobalVariables} from '../elements/gr-app-global-var-init.js';
import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import sinon, {SinonSpy} from 'sinon/pkg/sinon-esm';
import {safeTypesBridge} from '../utils/safe-types-util';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
import {initGlobalVariables} from '../elements/gr-app-global-var-init';
import 'chai/chai';
import {
_testOnly_defaultResinReportHandler,
installPolymerResin,
} from '../scripts/polymer-resin-install';
import {hasOwnProperty} from '../utils/common-util';
declare global {
interface Window {
assert: typeof chai.assert;
expect: typeof chai.expect;
fixture: typeof fixtureImpl;
stub: typeof stubImpl;
sinon: typeof sinon;
}
let assert: typeof chai.assert;
let expect: typeof chai.expect;
let stub: typeof stubImpl;
let sinon: typeof sinon;
}
window.assert = chai.assert;
window.expect = chai.expect;
window.sinon = sinon;
security.polymer_resin.install({
allowedIdentifierPrefixes: [''],
reportHandler(isViolation, fmt, ...args) {
const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
log(isViolation, fmt, ...args);
if (isViolation) {
// This will cause the test to fail if there is a data binding
// violation.
throw new Error(
'polymer-resin violation: ' + fmt +
JSON.stringify(args));
}
},
safeTypesBridge,
installPolymerResin(safeTypesBridge, (isViolation, fmt, ...args) => {
const log = _testOnly_defaultResinReportHandler;
log(isViolation, fmt, ...args);
if (isViolation) {
// This will cause the test to fail if there is a data binding
// violation.
throw new Error('polymer-resin violation: ' + fmt + JSON.stringify(args));
}
});
const cleanups = [];
interface TestFixtureElement extends HTMLElement {
restore(): void;
create(model?: unknown): HTMLElement | HTMLElement[];
}
function getFixtureElementById(fixtureId: string) {
return document.getElementById(fixtureId) as TestFixtureElement;
}
// For karma always set our implementation
// (karma doesn't provide the fixture method)
window.fixture = function(fixtureId, model) {
function fixtureImpl(fixtureId: string, model: unknown) {
// This method is inspired by web-component-tester method
cleanups.push(() => document.getElementById(fixtureId).restore());
return document.getElementById(fixtureId).create(model);
};
registerTestCleanup(() => getFixtureElementById(fixtureId).restore());
return getFixtureElementById(fixtureId).create(model);
}
window.fixture = fixtureImpl;
setup(() => {
window.Gerrit = {};
@@ -67,16 +97,18 @@ setup(() => {
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(cleanups.length, 0);
assert.equal(getCleanupsCount(), 0);
// The following calls is nessecary to avoid influence of previously executed
// tests.
TestKeyboardShortcutBinder.push();
_testOnlyInitAppContext();
_testOnly_initGerritPluginApi();
const mgr = _testOnly_getShortcutManagerInstance();
assert.equal(mgr.activeHosts.size, 0);
assert.equal(mgr.listeners.size, 0);
document.getSelection().removeAllRanges();
assert.isTrue(mgr._testOnly_isEmpty());
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
}
const pl = _testOnly_resetPluginLoader();
// For testing, always init with empty plugin list
// Since when serve in gr-app, we always retrieve the list
@@ -92,49 +124,68 @@ setup(() => {
// For karma always set our implementation
// (karma doesn't provide the stub method)
window.stub = function(tagName, implementation) {
function stubImpl<T extends keyof HTMLElementTagNameMap>(
tagName: T,
implementation: Partial<HTMLElementTagNameMap[T]>
) {
// This method is inspired by web-component-tester method
const proto = document.createElement(tagName).constructor.prototype;
const stubs = Object.keys(implementation)
.map(key => sinon.stub(proto, key).callsFake(implementation[key]));
cleanups.push(() => {
const proto = document.createElement(tagName).constructor
.prototype as HTMLElementTagNameMap[T];
let key: keyof HTMLElementTagNameMap[T];
const stubs: SinonSpy[] = [];
for (key in implementation) {
if (hasOwnProperty(implementation, key)) {
stubs.push(sinon.stub(proto, key).callsFake(implementation[key]));
}
}
registerTestCleanup(() => {
stubs.forEach(stub => {
stub.restore();
});
});
};
}
window.stub = stubImpl;
// Very simple function to catch unexpected elements in documents body.
// It can't catch everything, but in most cases it is enough.
function checkChildAllowed(element) {
function checkChildAllowed(element: Element) {
const allowedTags = ['SCRIPT', 'IRON-A11Y-ANNOUNCER'];
if (allowedTags.includes(element.tagName)) {
return;
}
if (element.tagName === 'TEST-FIXTURE') {
if (element.children.length == 0 ||
(element.children.length == 1 &&
element.children[0].tagName === 'TEMPLATE')) {
if (
element.children.length === 0 ||
(element.children.length === 1 &&
element.children[0].tagName === 'TEMPLATE')
) {
return;
}
assert.fail(`Test fixture
assert.fail(
`Test fixture
${element.outerHTML}` +
`isn't resotred after the test is finished. Please ensure that ` +
`restore() method is called for this test-fixture. Usually the call` +
`happens automatically.`);
"isn't resotred after the test is finished. Please ensure that " +
'restore() method is called for this test-fixture. Usually the call' +
'happens automatically.'
);
return;
}
if (element.tagName === 'DIV' && element.id === 'gr-hovercard-container' &&
element.childNodes.length === 0) {
if (
element.tagName === 'DIV' &&
element.id === 'gr-hovercard-container' &&
element.childNodes.length === 0
) {
return;
}
assert.fail(
`The following node remains in document after the test:
`The following node remains in document after the test:
${element.tagName}
Outer HTML:
${element.outerHTML},
Stack trace:
${element.stackTrace}`);
${(element as any).stackTrace}`
);
}
function checkGlobalSpace() {
for (const child of document.body.children) {
@@ -145,8 +196,6 @@ function checkGlobalSpace() {
teardown(() => {
sinon.restore();
cleanupTestUtils();
cleanups.forEach(cleanup => cleanup());
cleanups.splice(0);
TestKeyboardShortcutBinder.pop();
checkGlobalSpace();
// Clean Polymer debouncer queue, so next tests will not be affected.

View File

@@ -15,6 +15,19 @@
* limitations under the License.
*/
// Mark the file as a module. Otherwise typescript assumes this is a script
// and doesn't allow "declare global".
// See: https://www.typescriptlang.org/docs/handbook/modules.html
export {};
declare global {
interface Window {
sourceMapSupport: {
install(): void;
};
}
}
// The karma.conf.js file loads required module before any other modules
// The source-map-support.js can't be imported with import ... statement
window.sourceMapSupport.install();

View File

@@ -16,14 +16,17 @@
*/
// Init app context before any other imports
import {initAppContext} from '../services/app-context-init.js';
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
import {appContext} from '../services/app-context.js';
import {initAppContext} from '../services/app-context-init';
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
import {AppContext, appContext} from '../services/app-context';
export function _testOnlyInitAppContext() {
initAppContext();
function setMock(serviceName, setupMock) {
function setMock<T extends keyof AppContext>(
serviceName: T,
setupMock: AppContext[T]
) {
Object.defineProperty(appContext, serviceName, {
get() {
return setupMock;

View File

@@ -14,6 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {GerritNav} from '../elements/core/gr-navigation/gr-navigation.js';
import {GerritNav} from '../elements/core/gr-navigation/gr-navigation';
GerritNav.setup(url => { /* noop */ }, params => '', () => []);
GerritNav.setup(
() => {
/* noop */
},
() => '',
() => [],
() => {
return {};
}
);

View File

@@ -1,140 +0,0 @@
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* 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 {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
import {testOnly_resetInternalState} from '../elements/shared/gr-js-api-interface/gr-api-utils.js';
import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
export const mockPromise = () => {
let res;
const promise = new Promise(resolve => {
res = resolve;
});
promise.resolve = res;
return promise;
};
export const isHidden = el => getComputedStyle(el).display === 'none';
// Some tests/elements can define its own binding. We want to restore bindings
// at the end of the test. The TestKeyboardShortcutBinder store bindings in
// stack, so it is possible to override bindings in nested suites.
export class TestKeyboardShortcutBinder {
static push() {
if (!this.stack) {
this.stack = [];
}
const testBinder = new TestKeyboardShortcutBinder();
this.stack.push(testBinder);
return _testOnly_getShortcutManagerInstance();
}
static pop() {
this.stack.pop()._restoreShortcuts();
}
constructor() {
this._originalBinding = new Map(
_testOnly_getShortcutManagerInstance().bindings);
}
_restoreShortcuts() {
const bindings = _testOnly_getShortcutManagerInstance().bindings;
bindings.clear();
this._originalBinding.forEach((value, key) => {
bindings.set(key, value);
});
}
}
// Provide reset plugins function to clear installed plugins between tests.
// No gr-app found (running tests)
export const resetPlugins = () => {
testOnly_resetInternalState();
_testOnly_resetEndpoints();
const pl = _testOnly_resetPluginLoader();
pl.loadPlugins([]);
};
const cleanups = [];
function registerTestCleanup(cleanupCallback) {
cleanups.push(cleanupCallback);
}
export function cleanupTestUtils() {
cleanups.forEach(cleanup => cleanup());
cleanups.splice(0);
}
export function stubBaseUrl(newUrl) {
const originalCanonicalPath = window.CANONICAL_PATH;
window.CANONICAL_PATH = newUrl;
registerTestCleanup(() => window.CANONICAL_PATH = originalCanonicalPath);
}
export function generateChange(options) {
const change = {
_number: 42,
project: 'testRepo',
};
const revisionIdStart = 1;
const messageIdStart = 1000;
// We want to distinguish between empty arrays/objects and undefined
// If an option is not set - the appropriate property is not set
// If an options is set - the property always set
if (options && typeof options.revisionsCount !== 'undefined') {
const revisions = {};
for (let i = 0; i < options.revisionsCount; i++) {
const revisionId = (i + revisionIdStart).toString(16);
revisions[revisionId] = {
_number: i+1,
commit: {parents: []},
};
}
change.revisions = revisions;
}
if (options && typeof options.messagesCount !== 'undefined') {
const messages = [];
for (let i = 0; i < options.messagesCount; i++) {
messages.push({
id: (i + messageIdStart).toString(16),
date: new Date(2020, 1, 1),
message: `This is a message N${i + 1}`,
});
}
change.messages = messages;
}
if (options && options.status) {
change.status = options.status;
}
return change;
}
/**
* Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
* otherwise the backdrop stays around in the DOM for too long waiting for
* an animation to finish. This could be considered to be moved to a
* common-test-setup file.
*/
export function createIronOverlayBackdropStyleEl() {
const ironOverlayBackdropStyleEl = document.createElement('style');
document.head.appendChild(ironOverlayBackdropStyleEl);
ironOverlayBackdropStyleEl.sheet.insertRule(
'body { --iron-overlay-backdrop-opacity: 0; }');
return ironOverlayBackdropStyleEl;
}

View File

@@ -0,0 +1,233 @@
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* 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 '../types/globals';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {testOnly_resetInternalState} from '../elements/shared/gr-js-api-interface/gr-api-utils';
import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
import {
_testOnly_getShortcutManagerInstance,
Shortcut,
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {ChangeStatus, RevisionKind} from '../constants/constants';
import {
AccountInfo,
BranchName,
ChangeId,
ChangeInfo,
ChangeInfoId,
ChangeMessageId,
ChangeMessageInfo,
CommitInfo,
GitPersonInfo,
GitRef,
NumericChangeId,
PatchSetNum,
RepoName,
RevisionInfo,
Timestamp,
TimezoneOffset,
} from '../types/common';
import {formatDate} from '../utils/date-util';
export interface MockPromise extends Promise<unknown> {
resolve: (value?: unknown) => void;
}
export const mockPromise = () => {
let res: (value?: unknown) => void;
const promise: MockPromise = new Promise(resolve => {
res = resolve;
}) as MockPromise;
promise.resolve = res!;
return promise;
};
export const isHidden = (el: Element) =>
getComputedStyle(el).display === 'none';
// Some tests/elements can define its own binding. We want to restore bindings
// at the end of the test. The TestKeyboardShortcutBinder store bindings in
// stack, so it is possible to override bindings in nested suites.
export class TestKeyboardShortcutBinder {
private static stack: TestKeyboardShortcutBinder[] = [];
static push() {
const testBinder = new TestKeyboardShortcutBinder();
this.stack.push(testBinder);
return _testOnly_getShortcutManagerInstance();
}
static pop() {
const item = this.stack.pop();
if (!item) {
throw new Error('stack is empty');
}
item._restoreShortcuts();
}
private readonly originalBinding: Map<Shortcut, string[]>;
constructor() {
this.originalBinding = new Map(
_testOnly_getShortcutManagerInstance()._testOnly_getBindings()
);
}
_restoreShortcuts() {
const bindings = _testOnly_getShortcutManagerInstance()._testOnly_getBindings();
bindings.clear();
this.originalBinding.forEach((value, key) => {
bindings.set(key, value);
});
}
}
// Provide reset plugins function to clear installed plugins between tests.
// No gr-app found (running tests)
export const resetPlugins = () => {
testOnly_resetInternalState();
_testOnly_resetEndpoints();
const pl = _testOnly_resetPluginLoader();
pl.loadPlugins([]);
};
export type CleanupCallback = () => void;
const cleanups: CleanupCallback[] = [];
export function getCleanupsCount() {
return cleanups.length;
}
export function registerTestCleanup(cleanupCallback: CleanupCallback) {
cleanups.push(cleanupCallback);
}
export function cleanupTestUtils() {
cleanups.forEach(cleanup => cleanup());
cleanups.splice(0);
}
export function stubBaseUrl(newUrl: string) {
const originalCanonicalPath = window.CANONICAL_PATH;
window.CANONICAL_PATH = newUrl;
registerTestCleanup(() => (window.CANONICAL_PATH = originalCanonicalPath));
}
export interface GenerateChangeOptions {
revisionsCount?: number;
messagesCount?: number;
status: ChangeStatus;
}
export function dateToTimestamp(date: Date): Timestamp {
const nanosecondSuffix = '.000000000';
return (formatDate(date, 'YYYY-MM-DD HH:mm:ss') +
nanosecondSuffix) as Timestamp;
}
export function generateChange(options: GenerateChangeOptions) {
const project = 'testRepo' as RepoName;
const branch = 'test_branch' as BranchName;
const changeId = 'abcdef' as ChangeId;
const id = `${project}~${branch}~${changeId}` as ChangeInfoId;
const owner: AccountInfo = {};
const createdDate = new Date(2020, 1, 1, 1, 2, 3);
const change: ChangeInfo = {
_number: 42 as NumericChangeId,
project,
branch,
change_id: changeId,
created: dateToTimestamp(createdDate),
deletions: 0,
id,
insertions: 0,
owner,
reviewers: {},
status: options?.status ?? ChangeStatus.NEW,
subject: '',
submitter: owner,
updated: dateToTimestamp(new Date(2020, 10, 5, 1, 2, 3)),
};
const revisionIdStart = 1;
const messageIdStart = 1000;
// We want to distinguish between empty arrays/objects and undefined
// If an option is not set - the appropriate property is not set
// If an options is set - the property always set
if (options && typeof options.revisionsCount !== 'undefined') {
const revisions: {[revisionId: string]: RevisionInfo} = {};
const revisionDate = createdDate;
for (let i = 0; i < options.revisionsCount; i++) {
const revisionId = (i + revisionIdStart).toString(16);
const person: GitPersonInfo = {
name: 'Test person',
email: 'email@google.com',
date: dateToTimestamp(new Date(2019, 11, 6, 14, 5, 8)),
tz: 0 as TimezoneOffset,
};
const commit: CommitInfo = {
parents: [],
author: person,
committer: person,
subject: 'Test commit subject',
message: 'Test commit message',
};
const revision: RevisionInfo = {
_number: (i + 1) as PatchSetNum,
commit,
created: dateToTimestamp(revisionDate),
kind: RevisionKind.REWORK,
ref: `refs/changes/5/6/${i + 1}` as GitRef,
uploader: owner,
};
revisions[revisionId] = revision;
// advance 1 day
revisionDate.setDate(revisionDate.getDate() + 1);
}
change.revisions = revisions;
}
if (options && typeof options.messagesCount !== 'undefined') {
const messages: ChangeMessageInfo[] = [];
for (let i = 0; i < options.messagesCount; i++) {
messages.push({
id: (i + messageIdStart).toString(16) as ChangeMessageId,
date: '2020-01-01 00:00:00.000000000' as Timestamp,
message: `This is a message N${i + 1}`,
});
}
change.messages = messages;
}
if (options && options.status) {
change.status = options.status;
}
return change;
}
/**
* Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
* otherwise the backdrop stays around in the DOM for too long waiting for
* an animation to finish. This could be considered to be moved to a
* common-test-setup file.
*/
export function createIronOverlayBackdropStyleEl() {
const ironOverlayBackdropStyleEl = document.createElement('style');
document.head.appendChild(ironOverlayBackdropStyleEl);
ironOverlayBackdropStyleEl.sheet!.insertRule(
'body { --iron-overlay-backdrop-opacity: 0; }'
);
return ironOverlayBackdropStyleEl;
}

View File

@@ -44,7 +44,9 @@
// If allowJs is set to true, .js and .jsx files are included as well.
// Note: gerrit doesn't have .tsx and .jsx files
"include": [
// This items below must be in sync with the src_dirs list in the BUILD file
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig_bazel.json, tsconfig_bazel_test.json
// (include and exclude arrays are overriden when extends)
"constants/**/*",
"elements/**/*",
"embed/**/*",
@@ -56,7 +58,6 @@
"styles/**/*",
"types/**/*",
"utils/**/*",
// Directory for test utils (not included in src_dirs in the BUILD file)
"test/**/*"
]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"typeRoots": [
"../../external/ui_npm/node_modules/@types",
"../../external/ui_dev_npm/node_modules/@types"
]
},
"include": [
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig.json, tsconfig_bazel_test.json
// (include and exclude arrays are overriden when extends)
"constants/**/*",
"elements/**/*",
"embed/**/*",
"gr-diff/**/*",
"mixins/**/*",
"samples/**/*",
"scripts/**/*",
"services/**/*",
"styles/**/*",
"types/**/*",
"utils/**/*"
],
"exclude": [
"**/*_test.ts",
"**/*_test.js"
]
}

View File

@@ -0,0 +1,28 @@
{
"extends": "./tsconfig_bazel.json",
"compilerOptions": {
"typeRoots": [
"./test/@types",
"../../external/ui_npm/node_modules/@types",
"../../external/ui_dev_npm/node_modules/@types"
],
},
"include": [
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig.json, tsconfig_test.json
// (include and exclude arrays are overriden when extends)
"constants/**/*",
"elements/**/*",
"embed/**/*",
"gr-diff/**/*",
"mixins/**/*",
"samples/**/*",
"scripts/**/*",
"services/**/*",
"styles/**/*",
"types/**/*",
"utils/**/*",
"test/**/*"
],
"exclude": []
}

View File

@@ -15,28 +15,37 @@
* limitations under the License.
*/
import '../test/common-test-setup-karma.js';
import {toSortedPermissionsArray} from './access-util.js';
import '../test/common-test-setup-karma';
import {toSortedPermissionsArray} from './access-util';
suite('access-util tests', () => {
test('toSortedPermissionsArray', () => {
const rules = {
'global:Project-Owners': {
action: 'ALLOW', force: false,
action: 'ALLOW',
force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW', force: false,
action: 'ALLOW',
force: false,
},
};
const expectedResult = [
{id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
action: 'ALLOW', force: false,
}},
{id: 'global:Project-Owners', value: {
action: 'ALLOW', force: false,
}},
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
value: {
action: 'ALLOW',
force: false,
},
},
{
id: 'global:Project-Owners',
value: {
action: 'ALLOW',
force: false,
},
},
];
assert.deepEqual(toSortedPermissionsArray(rules), expectedResult);
});
});

View File

@@ -141,6 +141,7 @@ export function formatDate(date: Date, format: string) {
if (format.includes('ss')) {
options.second = '2-digit';
}
let locale = 'en-US';
// Workaround for Chrome 80, en-US is using h24 (midnight is 24:00),
// en-GB is using h23 (midnight is 00:00)

View File

@@ -2,7 +2,11 @@
"name": "polygerrit-ui-dev-dependencies",
"description": "Gerrit Code Review - Polygerrit dev dependencies",
"browser": true,
"dependencies": {},
"dependencies": {
"@types/chai": "^4.2.14",
"@types/mocha": "^8.0.3",
"@types/sinon": "^9.0.8"
},
"devDependencies": {
"@open-wc/karma-esm": "^2.16.16",
"@polymer/iron-test-helpers": "^3.0.1",

View File

@@ -989,6 +989,11 @@
resolved "https://registry.yarnpkg.com/@types/caniuse-api/-/caniuse-api-3.0.0.tgz#af31cc52062be0ab24583be072fd49b634dcc2fe"
integrity sha512-wT1VfnScjAftZsvLYaefu/UuwYJdYBwD2JDL2OQd01plGmuAoir5V6HnVHgrfh7zEwcasoiyO2wQ+W58sNh2sw==
"@types/chai@^4.2.14":
version "4.2.14"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193"
integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==
"@types/command-line-args@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.0.0.tgz#484e704d20dbb8754a8f091eee45cdd22bcff28c"
@@ -1140,6 +1145,11 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/mocha@^8.0.3":
version "8.0.3"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.0.3.tgz#51b21b6acb6d1b923bbdc7725c38f9f455166402"
integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==
"@types/node@*":
version "14.0.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce"
@@ -1175,6 +1185,18 @@
"@types/express-serve-static-core" "*"
"@types/mime" "*"
"@types/sinon@^9.0.8":
version "9.0.8"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.8.tgz#1ed0038d356784f75b086104ef83bfd4130bb81b"
integrity sha512-IVnI820FZFMGI+u1R+2VdRaD/82YIQTdqLYC9DLPszZuynAJDtCvCtCs3bmyL66s7FqRM3+LPX7DhHnVTaagDw==
dependencies:
"@types/sinonjs__fake-timers" "*"
"@types/sinonjs__fake-timers@*":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae"
integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==
"@types/whatwg-url@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"