mirror of https://github.com/ghostfolio/ghostfolio
11 changed files with 402 additions and 38 deletions
@ -0,0 +1,56 @@ |
|||
import { |
|||
AUTO_REFRESH_INTERVAL_MS, |
|||
AutoRefreshService |
|||
} from './auto-refresh.service'; |
|||
|
|||
describe('AutoRefreshService', () => { |
|||
const setVisibility = (state: 'hidden' | 'visible') => { |
|||
Object.defineProperty(document, 'visibilityState', { |
|||
configurable: true, |
|||
get: () => state |
|||
}); |
|||
document.dispatchEvent(new Event('visibilitychange')); |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
jest.useFakeTimers(); |
|||
setVisibility('visible'); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
jest.useRealTimers(); |
|||
}); |
|||
|
|||
it('emits at the interval while the page is visible', () => { |
|||
const service = new AutoRefreshService(); |
|||
const next = jest.fn(); |
|||
const subscription = service.refresh$.subscribe(next); |
|||
|
|||
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS - 1); |
|||
expect(next).not.toHaveBeenCalled(); |
|||
|
|||
jest.advanceTimersByTime(1); |
|||
expect(next).toHaveBeenCalledTimes(1); |
|||
|
|||
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS * 2); |
|||
expect(next).toHaveBeenCalledTimes(3); |
|||
|
|||
subscription.unsubscribe(); |
|||
}); |
|||
|
|||
it('pauses while hidden and refreshes immediately when visible again', () => { |
|||
const service = new AutoRefreshService(); |
|||
const next = jest.fn(); |
|||
const subscription = service.refresh$.subscribe(next); |
|||
|
|||
setVisibility('hidden'); |
|||
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS * 3); |
|||
expect(next).not.toHaveBeenCalled(); |
|||
|
|||
setVisibility('visible'); |
|||
jest.advanceTimersByTime(0); |
|||
expect(next).toHaveBeenCalledTimes(1); |
|||
|
|||
subscription.unsubscribe(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,45 @@ |
|||
import { Service } from '@angular/core'; |
|||
import { EMPTY, fromEvent, Observable, timer } from 'rxjs'; |
|||
import { |
|||
distinctUntilChanged, |
|||
map, |
|||
share, |
|||
startWith, |
|||
switchMap |
|||
} from 'rxjs/operators'; |
|||
|
|||
export const AUTO_REFRESH_INTERVAL_MS = 15_000; |
|||
|
|||
/** |
|||
* Emits at a fixed interval while the page is visible so that pages can |
|||
* refresh their numbers silently. The timer stops while the tab is hidden |
|||
* and fires once immediately when the tab becomes visible again. |
|||
*/ |
|||
@Service() |
|||
export class AutoRefreshService { |
|||
public readonly refresh$: Observable<void>; |
|||
|
|||
public constructor() { |
|||
let isFirstActivation = true; |
|||
|
|||
this.refresh$ = fromEvent(document, 'visibilitychange').pipe( |
|||
startWith(undefined), |
|||
map(() => document.visibilityState === 'visible'), |
|||
distinctUntilChanged(), |
|||
switchMap((isVisible) => { |
|||
if (!isVisible) { |
|||
return EMPTY; |
|||
} |
|||
|
|||
const dueTime = isFirstActivation ? AUTO_REFRESH_INTERVAL_MS : 0; |
|||
|
|||
isFirstActivation = false; |
|||
|
|||
return timer(dueTime, AUTO_REFRESH_INTERVAL_MS).pipe( |
|||
map(() => undefined) |
|||
); |
|||
}), |
|||
share() |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
import { getValueChangeDirection } from './value-change.helper'; |
|||
|
|||
describe('getValueChangeDirection', () => { |
|||
it('detects an increase', () => { |
|||
expect( |
|||
getValueChangeDirection({ |
|||
currentValue: 101, |
|||
digits: 2, |
|||
previousValue: 100 |
|||
}) |
|||
).toBe('up'); |
|||
}); |
|||
|
|||
it('detects a decrease', () => { |
|||
expect( |
|||
getValueChangeDirection({ |
|||
currentValue: 99.5, |
|||
digits: 2, |
|||
previousValue: 100 |
|||
}) |
|||
).toBe('down'); |
|||
}); |
|||
|
|||
it('ignores changes below the displayed precision', () => { |
|||
expect( |
|||
getValueChangeDirection({ |
|||
currentValue: 100.001, |
|||
digits: 2, |
|||
previousValue: 100 |
|||
}) |
|||
).toBeUndefined(); |
|||
}); |
|||
|
|||
it('ignores values which are not finite numbers', () => { |
|||
expect( |
|||
getValueChangeDirection({ |
|||
currentValue: NaN, |
|||
digits: 2, |
|||
previousValue: 100 |
|||
}) |
|||
).toBeUndefined(); |
|||
|
|||
expect( |
|||
getValueChangeDirection({ |
|||
currentValue: '101', |
|||
digits: 2, |
|||
previousValue: 100 |
|||
}) |
|||
).toBeUndefined(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Returns the direction in which a displayed number has changed, or |
|||
* `undefined` if it is unchanged at the displayed precision. |
|||
*/ |
|||
export function getValueChangeDirection({ |
|||
currentValue, |
|||
digits, |
|||
previousValue |
|||
}: { |
|||
currentValue: unknown; |
|||
digits: number; |
|||
previousValue: unknown; |
|||
}): 'down' | 'up' | undefined { |
|||
if ( |
|||
typeof currentValue !== 'number' || |
|||
typeof previousValue !== 'number' || |
|||
!Number.isFinite(currentValue) || |
|||
!Number.isFinite(previousValue) |
|||
) { |
|||
return undefined; |
|||
} |
|||
|
|||
const factor = 10 ** digits; |
|||
const difference = |
|||
Math.round(currentValue * factor) - Math.round(previousValue * factor); |
|||
|
|||
if (difference === 0) { |
|||
return undefined; |
|||
} |
|||
|
|||
return difference > 0 ? 'up' : 'down'; |
|||
} |
|||
Loading…
Reference in new issue