579 lines
16 KiB
JavaScript
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,
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|