From e755ff568dc82e5b61b772344a270cb16e453171 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 8 Jun 2022 16:39:06 +0200 Subject: [PATCH 1/7] MOBILE-4069 behat: Stop searching containers once element found --- src/testing/services/behat-dom.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index e0b1af642..e3d892407 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -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)), []); + 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'); From c6ae1f991dcd96aae00e85cb2fe5cb52984abc4b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 08:27:48 +0200 Subject: [PATCH 2/7] MOBILE-4069 tests: Add unit tests for CoreLogger --- src/core/singletons/tests/logger.test.ts | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/core/singletons/tests/logger.test.ts diff --git a/src/core/singletons/tests/logger.test.ts b/src/core/singletons/tests/logger.test.ts new file mode 100644 index 000000000..702bc0f3b --- /dev/null +++ b/src/core/singletons/tests/logger.test.ts @@ -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(( console.log).mock.calls[0][0]).toContain('TestName: Log message'); + + logger.info('Info message'); + expect(( console.info).mock.calls[0][0]).toContain('TestName: Info message'); + + logger.warn('Warn message'); + expect(( console.warn).mock.calls[0][0]).toContain('TestName: Warn message'); + + logger.debug('Debug message'); + expect(( console.debug).mock.calls[0][0]).toContain('TestName: Debug message'); + + logger.error('Error message'); + expect(( 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(( console.log).mock.calls[0][0]).toContain('TestName: Log message'); + + logger.info('Info message'); + expect(( console.info).mock.calls[0][0]).toContain('TestName: Info message'); + + logger.warn('Warn message'); + expect(( console.warn).mock.calls[0][0]).toContain('TestName: Warn message'); + + logger.debug('Debug message'); + expect(( console.debug).mock.calls[0][0]).toContain('TestName: Debug message'); + + logger.error('Error message'); + expect(( 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; + }); + +}); From 9ac2374820afcab8b96a20c61219b1fff6e83d7f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 08:34:53 +0200 Subject: [PATCH 3/7] MOBILE-4069 tests: Add unit tests for CoreMath --- src/core/singletons/tests/math.test.ts | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/core/singletons/tests/math.test.ts diff --git a/src/core/singletons/tests/math.test.ts b/src/core/singletons/tests/math.test.ts new file mode 100644 index 000000000..839f53742 --- /dev/null +++ b/src/core/singletons/tests/math.test.ts @@ -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); + }); + +}); From a6aa9e7a884a4d8e2ff7f4b43d10f283f74b7cd0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 09:36:48 +0200 Subject: [PATCH 4/7] MOBILE-4069 tests: Add unit tests for CoreObject --- src/core/singletons/tests/object.test.ts | 184 +++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/core/singletons/tests/object.test.ts diff --git a/src/core/singletons/tests/object.test.ts b/src/core/singletons/tests/object.test.ts new file mode 100644 index 000000000..a3b5409b7 --- /dev/null +++ b/src/core/singletons/tests/object.test.ts @@ -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. + } + +} From d0618312ccc65234709bc995b50037d0e39087c7 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 10:35:00 +0200 Subject: [PATCH 5/7] MOBILE-4069 tests: Add unit tests for CoreSubscriptions --- src/core/singletons/subscriptions.ts | 21 +++++-- .../singletons/tests/subscriptions.test.ts | 62 +++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/core/singletons/tests/subscriptions.test.ts diff --git a/src/core/singletons/subscriptions.ts b/src/core/singletons/subscriptions.ts index 3ea609a32..e65b862d5 100644 --- a/src/core/singletons/subscriptions.ts +++ b/src/core/singletons/subscriptions.ts @@ -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(subscribable: Subscribable, 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(); + } } } diff --git a/src/core/singletons/tests/subscriptions.test.ts b/src/core/singletons/tests/subscriptions.test.ts new file mode 100644 index 000000000..7c7a08412 --- /dev/null +++ b/src/core/singletons/tests/subscriptions.test.ts @@ -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(); + }); + +}); From b5d5469f0650b584dbf846f952257d9a9feabb98 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 10:55:46 +0200 Subject: [PATCH 6/7] MOBILE-4069 tests: Add unit tests for CoreText --- src/core/singletons/tests/text.test.ts | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/core/singletons/tests/text.test.ts diff --git a/src/core/singletons/tests/text.test.ts b/src/core/singletons/tests/text.test.ts new file mode 100644 index 000000000..10994b35e --- /dev/null +++ b/src/core/singletons/tests/text.test.ts @@ -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'); + }); + +}); From f2a8de8e09891a8bbc08c0c76ae55126608a4a10 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 9 Jun 2022 12:03:05 +0200 Subject: [PATCH 7/7] MOBILE-4069 tests: Add unit tests for CoreTime --- src/core/singletons/tests/time.test.ts | 69 ++++++++++++++++++++++++++ src/core/singletons/time.ts | 2 +- src/testing/utils.ts | 13 ++++- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/core/singletons/tests/time.test.ts diff --git a/src/core/singletons/tests/time.test.ts b/src/core/singletons/tests/time.test.ts new file mode 100644 index 000000000..b5f2122f8 --- /dev/null +++ b/src/core/singletons/tests/time.test.ts @@ -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); + }); + +}); diff --git a/src/core/singletons/time.ts b/src/core/singletons/time.ts index 5c95ed837..fd855b383 100644 --- a/src/core/singletons/time.ts +++ b/src/core/singletons/time.ts @@ -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); diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 4f097a35a..4b9091735 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -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 { }, time); }); } + +/** + * Mocks translate service with certain translations. + * + * @param translations List of translations. + */ +export function mockTranslate(translations: Record): void { + mockSingleton(Translate, { + instant: (key) => translations[key] ?? key, + }); +}