homehub/packages/homehub_app/tests/services/weather.spec.js
2021-08-26 12:33:15 +02:00

579 lines
16 KiB
JavaScript

import * as HomeHubCore from '@bthlabs/homehub-core';
import noop from 'lodash/noop';
import {DateTime} from 'luxon';
import {Clock} from 'src/lib/clock';
import * as WeatherService from 'src/services/weather';
import {WeatherFactory} from 'tests/__fixtures__/weather';
describe('src/services/weather', () => {
describe('WeatherService', () => {
let fakeCharacteristics = null;
let fakeResult = null;
beforeEach(() => {
fakeCharacteristics = {
city: 'Wroclaw,PL',
units: 'metric',
};
fakeResult = {
data: WeatherFactory(),
};
spyOn(HomeHubCore.API.Services, 'start').and.returnValue(fakeResult);
spyOn(HomeHubCore.API.Services, 'stop').and.returnValue('ok');
});
describe('emptyCharacteristics', () => {
it('returns empty characteristics', () => {
// Given
const result = WeatherService.WeatherService.emptyCharacteristics();
// Then
expect(result).toEqual({
city: '',
units: 'metric',
});
});
});
describe('start', () => {
it('notifies subscribers with the result of start service API call', async () => {
// Given
const service = new WeatherService.WeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'notify');
// When
await service.start();
// Then
expect(HomeHubCore.API.Services.start).toHaveBeenCalledWith(
service.kind, 'testing', fakeCharacteristics
);
expect(service.notify).toHaveBeenCalledWith(fakeResult);
});
});
describe('stop', () => {
it('calls the stop service API method', async () => {
// Given
const service = new WeatherService.WeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
// When
await service.stop();
// Then
expect(HomeHubCore.API.Services.stop).toHaveBeenCalledWith(
service.kind, 'testing'
);
});
});
describe('setCharacteristics', () => {
it('updates the service characteristics', () => {
// Given
const newCharacteristics = {
city: 'Poznan,PL',
units: 'imperial',
};
const service = new WeatherService.WeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'restart');
// When
service.setCharacteristics(newCharacteristics);
// Then
expect(service.characteristics).toEqual(newCharacteristics);
});
it('restarts the service if city characteristic changed', () => {
// Given
const newCharacteristics = {
...fakeCharacteristics,
city: 'Poznan,PL',
};
const service = new WeatherService.WeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'restart');
// When
service.setCharacteristics(newCharacteristics);
// Then
expect(service.restart).toHaveBeenCalled();
});
it('restarts the service if units characteristic changed', () => {
// Given
const newCharacteristics = {
...fakeCharacteristics,
units: 'imperial',
};
const service = new WeatherService.WeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'restart');
// When
service.setCharacteristics(newCharacteristics);
// Then
expect(service.restart).toHaveBeenCalled();
});
});
});
describe('OfflineWeatherService', () => {
let fakeCharacteristics = null;
let fakeClock = null;
let fakeDateTime = null;
let fakeResult = null;
beforeEach(() => {
fakeCharacteristics = {
city: 'Wroclaw,PL',
units: 'metric',
apiKey: 'API_KEY',
};
fakeClock = jasmine.createSpyObj(
'FakeClock', ['addEventListener', 'removeEventListener']
);
fakeClock.now = DateTime.local(1987, 10, 3, 8, 0, 0);
spyOn(Clock, 'instance').and.returnValue(fakeClock);
fakeDateTime = DateTime.local(1987, 10, 3, 8, 0, 0);
spyOn(DateTime, 'fromSeconds').and.returnValue(fakeDateTime);
fakeResult = {
data: WeatherFactory(),
};
spyOn(document, 'createElement');
spyOn(document.body, 'appendChild');
spyOn(document.body, 'removeChild');
spyOn(HomeHubCore.LocalStorage, 'getItem').and.returnValue(null);
spyOn(HomeHubCore.LocalStorage, 'setItem');
});
afterEach(() => {
const kind = WeatherService.OfflineWeatherService.kind;
const callbackKey = `${kind}_testing_callback`;
window[callbackKey] = undefined;
});
it('defines availability properties', () => {
// Then
expect(WeatherService.OfflineWeatherService.availableOnline).toBe(false);
expect(WeatherService.OfflineWeatherService.availableOffline).toBe(true);
});
describe('emptyCharacteristics', () => {
it('returns empty characteristics', () => {
// Given
const result = WeatherService.OfflineWeatherService.emptyCharacteristics();
// Then
expect(result).toEqual({
city: '',
units: 'metric',
apiKey: '',
});
});
});
describe('constructor', () => {
it('initializes an instance', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'this-should-be-an-uuid4',
characteristics: fakeCharacteristics,
});
// Then
expect(service.instanceKey).toEqual(
`${service.kind}_this_should_be_an_uuid4`
);
expect(service.callbackKey).toEqual(
`${service.kind}_this_should_be_an_uuid4_callback`
);
expect(service.lastScriptElement).toBe(null);
expect(service.lastData).toBe(null);
expect(service.lastRefreshDt).toBe(null);
});
});
describe('start', () => {
it('installs the JSONP callback', async () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'refresh');
// When
await service.start();
// Then
expect(window[service.callbackKey]).toEqual(service.onWeatherCallback);
});
it('does not initialize the last data if it is missing from local storage', async () => {
// Given
HomeHubCore.LocalStorage.getItem.and.returnValue(null);
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'refresh');
// When
await service.start();
// Then
expect(service.lastData).toBe(null);
expect(service.lastRefreshDt).toBe(null);
expect(HomeHubCore.LocalStorage.getItem).toHaveBeenCalledWith(
`${service.instanceKey}_lastData`
);
});
it('initializes last data if it is present in local storage', async () => {
// Given
HomeHubCore.LocalStorage.getItem.and.returnValue(fakeResult);
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'refresh');
// When
await service.start();
// Then
expect(service.lastData).toEqual(fakeResult);
expect(service.lastRefreshDt).toEqual(fakeDateTime);
expect(DateTime.fromSeconds).toHaveBeenCalledWith(fakeResult.dt);
});
it('triggers a refresh', async () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'refresh');
// When
await service.start();
// Then
expect(service.refresh).toHaveBeenCalled();
});
it('adds event listener for the clock tick event', async () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'refresh');
// When
await service.start();
// Then
expect(Clock.instance().addEventListener).toHaveBeenCalledWith(
'tick', service.onClockTick
);
});
});
describe('stop', () => {
it('replaces the JSONP callback with a noop', async () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
// When
await service.stop();
// Then
expect(window[service.callbackKey]).toEqual(noop);
});
it('removes event listener for the clock tick event', async () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
// When
await service.stop();
// Then
expect(Clock.instance().removeEventListener).toHaveBeenCalledWith(
'tick', service.onClockTick
);
});
});
describe('initialState', () => {
it('returns the initial state', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
// When
const result = service.initialState();
// Then
expect(result).toEqual({
error: null,
data: null,
});
});
it('returns state with last data if it is present', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastData = fakeResult;
// When
const result = service.initialState();
// Then
expect(result).toEqual({
error: null,
data: fakeResult,
});
});
});
describe('onClockTick', () => {
it('does not request a refresh when it should not refresh', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'shouldRefresh').and.returnValue(false);
spyOn(service, 'refresh');
// When
service.onClockTick();
// Then
expect(service.refresh).not.toHaveBeenCalled();
expect(service.shouldRefresh).toHaveBeenCalled();
});
it('requests a refresh when it should refresh', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'shouldRefresh').and.returnValue(true);
spyOn(service, 'refresh');
// When
service.onClockTick();
// Then
expect(service.refresh).toHaveBeenCalled();
});
});
describe('shouldRefresh', () => {
it('returns true if last data is null', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
// When
const result = service.shouldRefresh();
// Then
expect(result).toBe(true);
});
it('returns true if last refresh was more than 600 secs ago', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastData = fakeResult;
service.lastRefreshDt = DateTime.local(1987, 10, 3, 7, 50, 0);
// When
const result = service.shouldRefresh();
// Then
expect(result).toBe(true);
});
it('returns false if last refresh was less than 600 secs ago', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastData = fakeResult;
service.lastRefreshDt = DateTime.local(1987, 10, 3, 7, 50, 1);
// When
const result = service.shouldRefresh();
// Then
expect(result).toBe(false);
});
});
describe('onWeatherCallback', () => {
it('removes the last script element', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastScriptElement = jasmine.createSpy();
spyOn(service, 'notify');
// When
service.onWeatherCallback(fakeResult);
// Then
expect(document.body.removeChild).toHaveBeenCalledWith(
service.lastScriptElement
);
});
it('stores the data in local storage and instance', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastScriptElement = jasmine.createSpy();
spyOn(service, 'notify');
// When
service.onWeatherCallback(fakeResult);
// Then
expect(service.lastData).toEqual(fakeResult);
expect(service.lastRefreshDt).toEqual(fakeClock.now);
expect(HomeHubCore.LocalStorage.setItem).toHaveBeenCalledWith(
`${service.instanceKey}_lastData`, fakeResult
);
});
it('notifies subscribers with the loaded data', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
service.lastScriptElement = jasmine.createSpy();
spyOn(service, 'notify');
// When
service.onWeatherCallback(fakeResult);
// Then
expect(service.notify).toHaveBeenCalledWith({
error: null,
data: fakeResult,
});
});
});
describe('refresh', () => {
it('is a noop when it should not refresh', () => {
// Given
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'shouldRefresh').and.returnValue(false);
// When
service.refresh();
// Then
expect(document.createElement).not.toHaveBeenCalled();
expect(document.body.appendChild).not.toHaveBeenCalled();
});
it('calls the API via JSONP when it should refresh', () => {
// Given
const fakeScriptElement = jasmine.createSpy();
document.createElement.and.returnValue(fakeScriptElement);
const service = new WeatherService.OfflineWeatherService({
instance: 'testing',
characteristics: fakeCharacteristics,
});
spyOn(service, 'shouldRefresh').and.returnValue(true);
// When
service.refresh();
// Then
expect(document.createElement).toHaveBeenCalledWith('script');
expect(fakeScriptElement.async).toBe(true);
expect(fakeScriptElement.src).toBeDefined();
expect(fakeScriptElement.type).toEqual('text/javascript');
expect(service.lastScriptElement).toEqual(fakeScriptElement);
expect(document.body.appendChild).toHaveBeenCalledWith(
fakeScriptElement
);
expect(fakeScriptElement.src.startsWith(
WeatherService.OfflineWeatherService.apiURL
)).toBe(
true
);
const scriptSrcURL = new URL(fakeScriptElement.src);
const scriptSrcURLParams = {};
for (let pair of scriptSrcURL.searchParams.entries()) {
scriptSrcURLParams[pair[0]] = pair[1];
}
expect(scriptSrcURLParams).toEqual({
APPID: fakeCharacteristics.apiKey,
callback: service.callbackKey,
q: fakeCharacteristics.city,
units: fakeCharacteristics.units,
});
});
});
});
});