2022-05-12 15:47:23 +02:00
|
|
|
// (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';
|
2022-05-09 18:00:30 +02:00
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
|
|
import { makeSingleton, NgZone } from '@singletons';
|
2022-05-12 15:47:23 +02:00
|
|
|
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;
|
2022-05-09 18:00:30 +02:00
|
|
|
protected keyIndex = 0;
|
2022-05-12 15:47:23 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2022-05-09 18:00:30 +02:00
|
|
|
* @param key Key to add. It will be generated if none.
|
|
|
|
* @return Key name.
|
2022-05-12 15:47:23 +02:00
|
|
|
*/
|
2022-05-09 18:00:30 +02:00
|
|
|
block(key = ''): string {
|
2022-05-12 15:47:23 +02:00
|
|
|
// Add a special DELAY entry whenever another entry is added.
|
|
|
|
if (this.pendingList.length === 0) {
|
|
|
|
this.pendingList.push('DELAY');
|
|
|
|
}
|
2022-05-09 18:00:30 +02:00
|
|
|
if (!key) {
|
|
|
|
key = 'generated-' + this.keyIndex;
|
|
|
|
this.keyIndex++;
|
|
|
|
}
|
2022-05-12 15:47:23 +02:00
|
|
|
this.pendingList.push(key);
|
|
|
|
|
|
|
|
TestsBehatRuntime.log('PENDING+: ' + this.pendingList);
|
2022-05-09 18:00:30 +02:00
|
|
|
|
|
|
|
return key;
|
2022-05-12 15:47:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2022-05-09 18:00:30 +02:00
|
|
|
async unblock(key: string): Promise<void> {
|
2022-05-12 15:47:23 +02:00
|
|
|
// 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) {
|
2022-05-09 18:00:30 +02:00
|
|
|
if (!document.hidden) {
|
|
|
|
// When tab is not active, ticks should be slower and may do Behat to fail.
|
|
|
|
// From Timers API:
|
|
|
|
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers
|
|
|
|
// "This API does not guarantee that timers will run exactly on schedule.
|
|
|
|
// Delays due to CPU load, other tasks, etc, are to be expected."
|
|
|
|
await CoreUtils.nextTicks(10);
|
|
|
|
}
|
2022-05-12 15:47:23 +02:00
|
|
|
|
2022-05-09 18:00:30 +02:00
|
|
|
// Check there isn't a spinner...
|
|
|
|
await 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);
|
|
|
|
}
|
|
|
|
}
|
2022-05-12 15:47:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-09 18:00:30 +02:00
|
|
|
* Adds a pending key to the array, but removes it after some ticks.
|
2022-05-12 15:47:23 +02:00
|
|
|
*/
|
2022-05-09 18:00:30 +02:00
|
|
|
async delay(): Promise<void> {
|
|
|
|
const key = this.block('forced-delay');
|
|
|
|
this.unblock(key);
|
2022-05-12 15:47:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
*/
|
2022-05-09 18:00:30 +02:00
|
|
|
protected async checkUIBlocked(): Promise<void> {
|
|
|
|
await CoreUtils.nextTick();
|
|
|
|
const blocked = document.querySelector<HTMLElement>('div.core-loading-container, ion-loading, .click-block-active');
|
2022-05-12 15:47:23 +02:00
|
|
|
|
|
|
|
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) {
|
2022-05-09 18:00:30 +02:00
|
|
|
NgZone.run(() => {
|
|
|
|
const index = requestIndex++;
|
|
|
|
const key = 'httprequest-' + index;
|
2022-05-12 15:47:23 +02:00
|
|
|
|
2022-05-09 18:00:30 +02:00
|
|
|
try {
|
2022-05-12 15:47:23 +02:00
|
|
|
// Add to the list of pending requests.
|
2022-05-09 18:00:30 +02:00
|
|
|
TestsBehatBlocking.block(key);
|
2022-05-12 15:47:23 +02:00
|
|
|
|
2022-05-09 18:00:30 +02:00
|
|
|
// Detect when it finishes and remove it from the list.
|
|
|
|
this.addEventListener('loadend', () => {
|
|
|
|
TestsBehatBlocking.unblock(key);
|
|
|
|
});
|
2022-05-12 15:47:23 +02:00
|
|
|
|
2022-05-09 18:00:30 +02:00
|
|
|
return realOpen.apply(this, args);
|
|
|
|
} catch (error) {
|
|
|
|
TestsBehatBlocking.unblock(key);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
});
|
2022-05-12 15:47:23 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService);
|