Merge pull request #3310 from dpalou/MOBILE-4069

Mobile 4069
main
Pau Ferrer Ocaña 2022-06-09 12:40:23 +02:00 committed by GitHub
commit d1ec772ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 552 additions and 14 deletions

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { EventEmitter } from '@angular/core';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
/**
* Subscribable object.
@ -33,20 +33,29 @@ export class CoreSubscriptions {
* @param onError Callback to run when the an error happens.
*/
static once<T>(subscribable: Subscribable<T>, onSuccess: (value: T) => unknown, onError?: (error: unknown) => unknown): void {
const subscription = subscribable.subscribe(
let unsubscribe = false;
let subscription: Subscription | null = null;
subscription = subscribable.subscribe(
value => {
// Unsubscribe using a timeout because we can receive a value immediately.
setTimeout(() => subscription.unsubscribe(), 0);
// Subscription variable might not be set because we can receive a value immediately.
unsubscribe = true;
subscription?.unsubscribe();
onSuccess(value);
},
error => {
// Unsubscribe using a timeout because we can receive a value immediately.
setTimeout(() => subscription.unsubscribe(), 0);
// Subscription variable might not be set because we can receive a value immediately.
unsubscribe = true;
subscription?.unsubscribe();
onError && onError(error);
},
);
if (unsubscribe) {
subscription.unsubscribe();
}
}
}

View File

@ -0,0 +1,120 @@
// (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.
/* eslint-disable no-console */
import { CoreConstants } from '@/core/constants';
import { CoreBrowser } from '@singletons/browser';
import { CoreLogger } from '@singletons/logger';
describe('CoreLogger singleton', () => {
beforeEach(() => {
console.log = jest.fn();
console.info = jest.fn();
console.warn = jest.fn();
console.debug = jest.fn();
console.error = jest.fn();
});
it('adds logs to the console in dev environment', () => {
// Simulate dev environment.
const isTesting = CoreConstants.BUILD.isTesting;
const isProduction = CoreConstants.BUILD.isProduction;
CoreConstants.BUILD.isTesting = false;
CoreConstants.BUILD.isProduction = false;
const logger = CoreLogger.getInstance('TestName');
logger.log('Log message');
expect((<jest.Mock> console.log).mock.calls[0][0]).toContain('TestName: Log message');
logger.info('Info message');
expect((<jest.Mock> console.info).mock.calls[0][0]).toContain('TestName: Info message');
logger.warn('Warn message');
expect((<jest.Mock> console.warn).mock.calls[0][0]).toContain('TestName: Warn message');
logger.debug('Debug message');
expect((<jest.Mock> console.debug).mock.calls[0][0]).toContain('TestName: Debug message');
logger.error('Error message');
expect((<jest.Mock> console.error).mock.calls[0][0]).toContain('TestName: Error message');
CoreConstants.BUILD.isTesting = isTesting;
CoreConstants.BUILD.isProduction = isProduction;
});
it('adds logs to the console if enabled via dev setting', () => {
// Enable logging.
CoreBrowser.setDevelopmentSetting('LoggingEnabled', '1');
const logger = CoreLogger.getInstance('TestName');
logger.log('Log message');
expect((<jest.Mock> console.log).mock.calls[0][0]).toContain('TestName: Log message');
logger.info('Info message');
expect((<jest.Mock> console.info).mock.calls[0][0]).toContain('TestName: Info message');
logger.warn('Warn message');
expect((<jest.Mock> console.warn).mock.calls[0][0]).toContain('TestName: Warn message');
logger.debug('Debug message');
expect((<jest.Mock> console.debug).mock.calls[0][0]).toContain('TestName: Debug message');
logger.error('Error message');
expect((<jest.Mock> console.error).mock.calls[0][0]).toContain('TestName: Error message');
CoreBrowser.clearDevelopmentSetting('LoggingEnabled');
});
it('doesn\'t log to the console in testing environment', () => {
// Disable production.
const isProduction = CoreConstants.BUILD.isProduction;
CoreConstants.BUILD.isProduction = false;
const logger = CoreLogger.getInstance('TestName');
logger.log('Log message');
expect(console.log).not.toHaveBeenCalled();
logger.info('Info message');
expect(console.info).not.toHaveBeenCalled();
logger.warn('Warn message');
expect(console.warn).not.toHaveBeenCalled();
logger.debug('Debug message');
expect(console.debug).not.toHaveBeenCalled();
logger.error('Error message');
expect(console.error).not.toHaveBeenCalled();
CoreConstants.BUILD.isProduction = isProduction;
});
it('displays a warning in production environment', () => {
// Enable production.
const isProduction = CoreConstants.BUILD.isProduction;
CoreConstants.BUILD.isProduction = true;
CoreLogger.getInstance('TestName');
expect(console.warn).toHaveBeenCalledWith('Log is disabled in production app');
CoreConstants.BUILD.isProduction = isProduction;
});
});

View File

@ -0,0 +1,29 @@
// (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 { CoreMath } from '@singletons/math';
describe('CoreMath singleton', () => {
it('clamps values', () => {
expect(CoreMath.clamp(150, 100, 200)).toEqual(150);
expect(CoreMath.clamp(25, 100, 200)).toEqual(100);
expect(CoreMath.clamp(-100, 100, 200)).toEqual(100);
expect(CoreMath.clamp(500, 100, 200)).toEqual(200);
expect(CoreMath.clamp(50.55, 100.11, 200.22)).toEqual(100.11);
expect(CoreMath.clamp(100, -200.22, -100.11)).toEqual(-100.11);
expect(CoreMath.clamp(-500, -200.22, -100.11)).toEqual(-200.22);
});
});

View File

@ -0,0 +1,184 @@
// (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 { CoreObject } from '@singletons/object';
describe('CoreObject singleton', () => {
it('compares two values, checking all subproperties if needed', () => {
expect(CoreObject.deepEquals(1, 1)).toBe(true);
expect(CoreObject.deepEquals(1, 2)).toBe(false);
expect(CoreObject.deepEquals(NaN, NaN)).toBe(true);
expect(CoreObject.deepEquals(NaN, 0)).toBe(false);
expect(CoreObject.deepEquals('foo', 'foo')).toBe(true);
expect(CoreObject.deepEquals('foo', 'bar')).toBe(false);
expect(CoreObject.deepEquals(true, true)).toBe(true);
expect(CoreObject.deepEquals(true, false)).toBe(false);
expect(CoreObject.deepEquals(null, null)).toBe(true);
expect(CoreObject.deepEquals(undefined, undefined)).toBe(true);
expect(CoreObject.deepEquals(null, undefined)).toBe(false);
const firstObject = {
foo: 'bar',
subobject: {
foo: 'bar',
subobject: {
foo: 'bar',
items: [1, 2, 3],
},
},
};
const secondObject = {
foo: 'bar',
subobject: {
foo: 'bar',
subobject: {
foo: 'bar',
items: [1, 2, 3],
},
},
};
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(true);
secondObject.foo = 'baz';
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(false);
secondObject.foo = 'bar';
secondObject.subobject.foo = 'baz';
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(false);
secondObject.subobject.foo = 'bar';
secondObject.subobject.subobject.foo = 'baz';
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(false);
secondObject.subobject.subobject.foo = 'bar';
secondObject.subobject.subobject.items[0] = 0;
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(false);
secondObject.subobject.subobject.items[0] = 1;
expect(CoreObject.deepEquals(firstObject, secondObject)).toBe(true);
});
it('gets all property names', () => {
expect(CoreObject.getAllPropertyNames(null)).toEqual(new Set([]));
expect(CoreObject.getAllPropertyNames(undefined)).toEqual(new Set([]));
expect(CoreObject.getAllPropertyNames(1)).toEqual(new Set([]));
expect(CoreObject.getAllPropertyNames('foo')).toEqual(new Set([]));
expect(CoreObject.getAllPropertyNames({
foo: 1,
bar: 2,
doSomething: () => {
// Nothing to do.
},
})).toEqual(new Set(['foo', 'bar', 'doSomething']));
expect(CoreObject.getAllPropertyNames(new TestParentClass()))
.toEqual(new Set(['constructor', 'foo', 'bar', 'baz', 'doSomething']));
expect(CoreObject.getAllPropertyNames(new TestSubClass()))
.toEqual(new Set(['constructor', 'foo', 'bar', 'baz', 'doSomething', 'sub', 'doSomethingElse']));
});
it('checks if an object is empty', () => {
expect(CoreObject.isEmpty({})).toEqual(true);
expect(CoreObject.isEmpty({ foo: 1 })).toEqual(false);
});
it('creates a copy of an object without certain properties', () => {
const originalObject = {
foo: 1,
bar: 2,
baz: 3,
};
expect(CoreObject.without(originalObject, [])).toEqual(originalObject);
expect(CoreObject.without(originalObject, ['foo'])).toEqual({
bar: 2,
baz: 3,
});
expect(CoreObject.without(originalObject, ['foo', 'baz'])).toEqual({
bar: 2,
});
expect(originalObject).toEqual({
foo: 1,
bar: 2,
baz: 3,
});
});
it('creates a copy of an object without null/undefined properties', () => {
const objectWithoutEmpty = {
bool: false,
num: 0,
nan: NaN,
str: '',
obj: {},
arr: [],
};
expect(CoreObject.withoutEmpty({
...objectWithoutEmpty,
foo: null,
bar: undefined,
baz: null,
})).toEqual(objectWithoutEmpty);
});
it('creates a copy of an object without undefined properties', () => {
const objectWithoutUndefined = {
bool: false,
num: 0,
nan: NaN,
str: '',
obj: {},
arr: [],
foo: null,
};
expect(CoreObject.withoutUndefined({
...objectWithoutUndefined,
bar: undefined,
baz: undefined,
})).toEqual(objectWithoutUndefined);
});
});
class TestParentClass {
foo = 1;
protected bar = 2;
private baz = 3;
protected doSomething(): void {
// Nothing to do.
}
}
class TestSubClass extends TestParentClass {
foo = 10;
protected bar = 20;
private sub = 30;
protected doSomethingElse(): void {
// Nothing to do.
}
}

View File

@ -0,0 +1,62 @@
// (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 { CoreSubscriptions } from '@singletons/subscriptions';
import { BehaviorSubject, Subject } from 'rxjs';
describe('CoreSubscriptions singleton', () => {
it('calls callbacks only once', async () => {
// Test call success function.
let subject = new Subject();
let success = jest.fn();
let error = jest.fn();
CoreSubscriptions.once(subject, success, error);
subject.next('foo');
expect(success).toHaveBeenCalledTimes(1);
expect(success).toHaveBeenCalledWith('foo');
subject.next('bar');
subject.error('foo');
expect(success).toHaveBeenCalledTimes(1);
expect(error).not.toHaveBeenCalled();
// Test call error function.
subject = new Subject(); // Create a new Subject because the previous one already has an error.
success = jest.fn();
CoreSubscriptions.once(subject, success, error);
subject.error('foo');
expect(error).toHaveBeenCalledWith('foo');
subject.next('foo');
subject.error('bar');
expect(error).toHaveBeenCalledTimes(1);
expect(success).not.toHaveBeenCalled();
// Test with behaviour subject (success callback called immediately).
const beaviourSubject = new BehaviorSubject('foo');
error = jest.fn();
CoreSubscriptions.once(beaviourSubject, success, error);
expect(success).toHaveBeenCalledWith('foo');
beaviourSubject.next('bar');
beaviourSubject.error('foo');
expect(success).toHaveBeenCalledTimes(1);
expect(error).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,42 @@
// (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 { CoreText } from '@singletons/text';
describe('CoreText singleton', () => {
it('adds a starting slash if needed', () => {
expect(CoreText.addStartingSlash('')).toEqual('/');
expect(CoreText.addStartingSlash('foo')).toEqual('/foo');
expect(CoreText.addStartingSlash('/foo')).toEqual('/foo');
});
it('remove ending slash if needed', () => {
expect(CoreText.removeEndingSlash('/')).toEqual('');
expect(CoreText.removeEndingSlash('foo')).toEqual('foo');
expect(CoreText.removeEndingSlash('foo/')).toEqual('foo');
expect(CoreText.removeEndingSlash('foo//')).toEqual('foo/');
});
it('concatenates paths', () => {
expect(CoreText.concatenatePaths('', 'foo/bar')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo/bar', '')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo', 'bar')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo/', 'bar')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo', '/bar')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo/', '/bar')).toEqual('foo/bar');
expect(CoreText.concatenatePaths('foo/bar', 'baz')).toEqual('foo/bar/baz');
});
});

View File

@ -0,0 +1,69 @@
// (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 { mockTranslate } from '@/testing/utils';
import { CoreTime } from '@singletons/time';
describe('CoreTime singleton', () => {
it('formats time in a human readable format', () => {
mockTranslate({
'core.days': 'days',
'core.day': 'day',
'core.hours': 'hours',
'core.hour': 'hour',
'core.mins': 'mins',
'core.min': 'min',
'core.now': 'now',
'core.secs': 'secs',
'core.sec': 'sec',
'core.years': 'years',
'core.year': 'year',
});
expect(CoreTime.formatTime(0)).toEqual('now');
expect(CoreTime.formatTime(-5)).toEqual('5 secs');
expect(CoreTime.formatTime(61)).toEqual('1 min 1 sec');
expect(CoreTime.formatTime(7321)).toEqual('2 hours 2 mins');
expect(CoreTime.formatTime(352861)).toEqual('4 days 2 hours');
expect(CoreTime.formatTime(31888861)).toEqual('1 year 4 days');
expect(CoreTime.formatTime(-31888861)).toEqual('1 year 4 days');
// Test different precisions.
expect(CoreTime.formatTime(31888861, 1)).toEqual('1 year');
expect(CoreTime.formatTime(31888861, 3)).toEqual('1 year 4 days 2 hours');
expect(CoreTime.formatTime(31888861, 4)).toEqual('1 year 4 days 2 hours 1 min');
expect(CoreTime.formatTime(31888861, 5)).toEqual('1 year 4 days 2 hours 1 min 1 sec');
});
it('formats time in a "short" human readable format', () => {
expect(CoreTime.formatTimeShort(0)).toEqual('0\'\'');
expect(CoreTime.formatTimeShort(61)).toEqual('1\' 1\'\'');
expect(CoreTime.formatTimeShort(7321)).toEqual('122\' 1\'\'');
});
it('calls a function only once', () => {
const testFunction = jest.fn();
const onceFunction = CoreTime.once(testFunction);
expect(testFunction).not.toHaveBeenCalled();
onceFunction('foo', 'bar');
expect(testFunction).toHaveBeenCalledWith('foo', 'bar');
onceFunction('baz');
expect(testFunction).toHaveBeenCalledTimes(1);
});
});

View File

@ -39,7 +39,7 @@ export class CoreTime {
let remainder = totalSecs - (years * CoreConstants.SECONDS_YEAR);
const days = Math.floor(remainder / CoreConstants.SECONDS_DAY);
remainder = totalSecs - (days * CoreConstants.SECONDS_DAY);
remainder = remainder - (days * CoreConstants.SECONDS_DAY);
const hours = Math.floor(remainder / CoreConstants.SECONDS_HOUR);
remainder = remainder - (hours * CoreConstants.SECONDS_HOUR);

View File

@ -326,7 +326,7 @@ export class TestsBehatDomUtils {
* @return First found element.
*/
static findElementBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement {
return this.findElementsBasedOnText(locator, containerName)[0];
return this.findElementsBasedOnText(locator, containerName, true)[0];
}
/**
@ -334,13 +334,25 @@ export class TestsBehatDomUtils {
*
* @param locator Element locator.
* @param containerName Whether to search only inside a specific container.
* @param stopWhenFound Stop looking in containers once an element is found.
* @return Found elements
*/
protected static findElementsBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement[] {
protected static findElementsBasedOnText(
locator: TestBehatElementLocator,
containerName = '',
stopWhenFound = false,
): HTMLElement[] {
const topContainers = this.getCurrentTopContainerElements(containerName);
let elements: HTMLElement[] = [];
return topContainers.reduce((elements, container) =>
elements.concat(this.findElementsBasedOnTextInContainer(locator, container)), <HTMLElement[]> []);
for (let i = 0; i < topContainers.length; i++) {
elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i]));
if (stopWhenFound && elements.length) {
break;
}
}
return elements;
}
/**
@ -357,7 +369,7 @@ export class TestsBehatDomUtils {
let container: HTMLElement | null = topContainer;
if (locator.within) {
const withinElements = this.findElementsBasedOnText(locator.within);
const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer);
if (withinElements.length === 0) {
throw new Error('There was no match for within text');
@ -375,7 +387,7 @@ export class TestsBehatDomUtils {
}
if (topContainer && locator.near) {
const nearElements = this.findElementsBasedOnText(locator.near);
const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer);
if (nearElements.length === 0) {
throw new Error('There was no match for near text');

View File

@ -19,7 +19,7 @@ import { Observable, Subject } from 'rxjs';
import { sep } from 'path';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreSingletonProxy, Network, Platform } from '@singletons';
import { CoreSingletonProxy, Network, Platform, Translate } from '@singletons';
import { CoreTextUtilsProvider } from '@services/utils/text';
import { TranslatePipeStub } from './stubs/pipes/translate';
@ -269,3 +269,14 @@ export function wait(time: number): Promise<void> {
}, time);
});
}
/**
* Mocks translate service with certain translations.
*
* @param translations List of translations.
*/
export function mockTranslate(translations: Record<string, string>): void {
mockSingleton(Translate, {
instant: (key) => translations[key] ?? key,
});
}