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, }); }); }); }); });