blob: 03a95255ccf35baa90a8a15928ec5a0e98dc55f4 [file] [log] [blame]
/**
* @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_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
import {
_testOnly_getShortcutManagerInstance,
Shortcut,
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {appContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy} from 'sinon/pkg/sinon-esm';
import {StorageService} from '../services/storage/gr-storage';
import {AuthService} from '../services/gr-auth/gr-auth';
import {ReportingService} from '../services/gr-reporting/gr-reporting';
import {CommentsService} from '../services/comments/comments-service';
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 function isHidden(el: Element | undefined | null) {
if (!el) return true;
return getComputedStyle(el).display === 'none';
}
export function queryAll<E extends Element = Element>(
el: Element | undefined,
selector: string
): NodeListOf<E> {
if (!el) assert.fail('element not defined');
const root = el.shadowRoot ?? el;
return root.querySelectorAll<E>(selector);
}
export function query<E extends Element = Element>(
el: Element | undefined,
selector: string
): E | undefined {
if (!el) return undefined;
const root = el.shadowRoot ?? el;
return root.querySelector<E>(selector) ?? undefined;
}
export function queryAndAssert<E extends Element = Element>(
el: Element | undefined,
selector: string
): E {
const found = query<E>(el, selector);
if (!found) assert.fail(`selector '${selector}' did not match anything'`);
return found;
}
export function isVisible(el: Element) {
assert.ok(el);
return getComputedStyle(el).getPropertyValue('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_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 addListenerForTest(
el: EventTarget,
type: string,
listener: EventListenerOrEventListenerObject
) {
el.addEventListener(type, listener);
registerListenerCleanup(el, type, listener);
}
export function registerListenerCleanup(
el: EventTarget,
type: string,
listener: EventListenerOrEventListenerObject
) {
registerTestCleanup(() => {
el.removeEventListener(type, listener);
});
}
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 function stubRestApi<K extends keyof RestApiService>(method: K) {
return sinon.stub(appContext.restApiService, method);
}
export function spyRestApi<K extends keyof RestApiService>(method: K) {
return sinon.spy(appContext.restApiService, method);
}
export function stubComments<K extends keyof CommentsService>(method: K) {
return sinon.stub(appContext.commentsService, method);
}
export function stubStorage<K extends keyof StorageService>(method: K) {
return sinon.stub(appContext.storageService, method);
}
export function spyStorage<K extends keyof StorageService>(method: K) {
return sinon.spy(appContext.storageService, method);
}
export function stubAuth<K extends keyof AuthService>(method: K) {
return sinon.stub(appContext.authService, method);
}
export function stubReporting<K extends keyof ReportingService>(method: K) {
return sinon.stub(appContext.reportingService, method);
}
export type SinonSpyMember<F extends (...args: any) => any> = SinonSpy<
Parameters<F>,
ReturnType<F>
>;
/**
* 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.
*/
export function addIronOverlayBackdropStyleEl() {
const el = document.createElement('style');
el.setAttribute('id', 'backdrop-style');
document.head.appendChild(el);
el.sheet!.insertRule('body { --iron-overlay-backdrop-opacity: 0; }');
}
export function removeIronOverlayBackdropStyleEl() {
const el = document.getElementById('backdrop-style');
if (!el?.parentNode) throw new Error('Backdrop style element not found.');
el.parentNode?.removeChild(el);
}
/**
* Promisify an event callback to simplify async...await tests.
*
* Use like this:
* await listenOnce(el, 'render');
* ...
*/
export function listenOnce(el: EventTarget, eventType: string) {
return new Promise<void>(resolve => {
const listener = () => {
removeEventListener();
resolve();
};
el.addEventListener(eventType, listener);
let removeEventListener = () => {
el.removeEventListener(eventType, listener);
removeEventListener = () => {};
};
registerTestCleanup(removeEventListener);
});
}