Vmeda.Online/src/testing/services/behat-blocking.ts

242 lines
7.3 KiB
TypeScript
Raw Normal View History

// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { Injectable } from '@angular/core';
import { makeSingleton } from '@singletons';
import { BehatTestsWindow, TestsBehatRuntime } from './behat-runtime';
/**
* Behat block JS manager.
*/
@Injectable({ providedIn: 'root' })
export class TestsBehatBlockingService {
protected waitingBlocked = false;
protected recentMutation = false;
protected lastMutation = 0;
protected initialized = false;
/**
* Listen to mutations and override XML Requests.
*/
init(): void {
if (this.initialized) {
return;
}
this.initialized = true;
this.listenToMutations();
this.xmlRequestOverride();
const win = window as BehatTestsWindow;
// Set up the M object - only pending_js is implemented.
win.M = win.M ?? {};
win.M.util = win.M.util ?? {};
win.M.util.pending_js = win.M.util.pending_js ?? [];
TestsBehatRuntime.log('Initialized!');
}
/**
* Get pending list on window M object.
*/
protected get pendingList(): string[] {
const win = window as BehatTestsWindow;
return win.M?.util?.pending_js || [];
}
/**
* Set pending list on window M object.
*/
protected set pendingList(values: string[]) {
const win = window as BehatTestsWindow;
if (!win.M?.util?.pending_js) {
return;
}
win.M.util.pending_js = values;
}
/**
* Adds a pending key to the array.
*
* @param key Key to add.
*/
block(key: string): void {
// Add a special DELAY entry whenever another entry is added.
if (this.pendingList.length === 0) {
this.pendingList.push('DELAY');
}
this.pendingList.push(key);
TestsBehatRuntime.log('PENDING+: ' + this.pendingList);
}
/**
* Removes a pending key from the array. If this would clear the array, the actual clear only
* takes effect after the queued events are finished.
*
* @param key Key to remove
*/
unblock(key: string): void {
// Remove the key immediately.
this.pendingList = this.pendingList.filter((x) => x !== key);
TestsBehatRuntime.log('PENDING-: ' + this.pendingList);
// If the only thing left is DELAY, then remove that as well, later...
if (this.pendingList.length === 1) {
this.runAfterEverything(() => {
// Check there isn't a spinner...
this.checkUIBlocked();
// Only remove it if the pending array is STILL empty after all that.
if (this.pendingList.length === 1) {
this.pendingList = [];
TestsBehatRuntime.log('PENDING-: ' + this.pendingList);
}
});
}
}
/**
* Adds a pending key to the array, but removes it after some setTimeouts finish.
*/
delay(): void {
this.block('...');
this.unblock('...');
}
/**
* Run after several setTimeouts to ensure queued events are finished.
*
* @param target Function to run.
* @param count Number of times to do setTimeout (leave blank for 10).
*/
protected runAfterEverything(target: () => void, count = 10): void {
setTimeout(() => {
count--;
if (count === 0) {
target();
return;
}
this.runAfterEverything(target, count);
}, 0);
}
/**
* It would be really beautiful if you could detect CSS transitions and animations, that would
* cover almost everything, but sadly there is no way to do this because the transitionstart
* and animationcancel events are not implemented in Chrome, so we cannot detect either of
* these reliably. Instead, we have to look for any DOM changes and do horrible polling. Most
* of the animations are set to 500ms so we allow it to continue from 500ms after any DOM
* change.
*/
protected listenToMutations(): void {
// Set listener using the mutation callback.
const observer = new MutationObserver(() => {
this.lastMutation = Date.now();
if (!this.recentMutation) {
this.recentMutation = true;
this.block('dom-mutation');
setTimeout(() => {
this.pollRecentMutation();
}, 500);
}
// Also update the spinner presence if needed.
this.checkUIBlocked();
});
observer.observe(document, { attributes: true, childList: true, subtree: true });
}
/**
* Called from the mutation callback to remove the pending tag after 500ms if nothing else
* gets mutated.
*
* This will be called after 500ms, then every 100ms until there have been no mutation events
* for 500ms.
*/
protected pollRecentMutation(): void {
if (Date.now() - this.lastMutation > 500) {
this.recentMutation = false;
this.unblock('dom-mutation');
return;
}
setTimeout(() => {
this.pollRecentMutation();
}, 100);
}
/**
* Checks if a loading spinner is present and visible; if so, adds it to the pending array
* (and if not, removes it).
*/
protected checkUIBlocked(): void {
const blocked = document.querySelector<HTMLElement>('span.core-loading-spinner, ion-loading, .click-block-active');
if (blocked?.offsetParent) {
if (!this.waitingBlocked) {
this.block('blocked');
this.waitingBlocked = true;
}
} else {
if (this.waitingBlocked) {
this.unblock('blocked');
this.waitingBlocked = false;
}
}
}
/**
* Override XMLHttpRequest to mark things pending while there is a request waiting.
*/
protected xmlRequestOverride(): void {
const realOpen = XMLHttpRequest.prototype.open;
let requestIndex = 0;
XMLHttpRequest.prototype.open = function(...args) {
const index = requestIndex++;
const key = 'httprequest-' + index;
try {
// Add to the list of pending requests.
TestsBehatBlocking.block(key);
// Detect when it finishes and remove it from the list.
this.addEventListener('loadend', () => {
TestsBehatBlocking.unblock(key);
});
return realOpen.apply(this, args);
} catch (error) {
TestsBehatBlocking.unblock(key);
throw error;
}
};
}
}
export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService);