From 3fcbee1067b874c183335849281d6620e8f5959a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 21 Aug 2018 18:36:32 +0100 Subject: [PATCH 1/3] fix(test): less noisy test-js-watch Don't print coverage to console and only test changed files --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 59c70948d..e6bb8959f 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,9 @@ test-js-watch: .build/deps-build-node.ok @# hitting issues with jest --watch due to @# https://github.com/facebook/jest/issues/3436 @# use onchange for now - cd ui && ./node_modules/onchange/cli.js 'src/*.js' 'src/**/*.js' -- npm test -- --coverage + cd ui && ./node_modules/onchange/cli.js 'src/*.js' 'src/**/*.js' -- \ + npm test -- \ + --coverage --coverageReporters=lcov --onlyChanged .PHONY: test test: lint test-go test-js From 9fe6494830f68031f565ed778ed0af784e85a034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 21 Aug 2018 18:36:56 +0100 Subject: [PATCH 2/3] feat(test): add mock modules for fetch() and console --- ui/src/__mocks__/Console.js | 4 ++++ ui/src/__mocks__/Fetch.js | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 ui/src/__mocks__/Console.js create mode 100644 ui/src/__mocks__/Fetch.js diff --git a/ui/src/__mocks__/Console.js b/ui/src/__mocks__/Console.js new file mode 100644 index 000000000..a7dfdef42 --- /dev/null +++ b/ui/src/__mocks__/Console.js @@ -0,0 +1,4 @@ +const ConsoleMock = level => + jest.spyOn(console, level).mockImplementation(() => jest.fn()); + +export { ConsoleMock }; diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js new file mode 100644 index 000000000..add9b7a23 --- /dev/null +++ b/ui/src/__mocks__/Fetch.js @@ -0,0 +1,41 @@ +import moment from "moment"; + +const FetchMock = data => + jest.fn().mockImplementation(() => + Promise.resolve({ + json: () => data + }) + ); + +const EmptyAPIResponse = () => ({ + status: "success", + timestamp: moment().toISOString(), + version: "fakeVersion", + upstreams: { + counters: { total: 1, healthy: 1, failed: 0 }, + instances: [{ name: "default", uri: "file:///mock", error: "" }] + }, + silences: { default: {} }, + groups: {}, + totalAlerts: 0, + colors: {}, + filters: [ + { + text: "label=value", + name: "label", + matcher: "=", + value: "value", + hits: 0, + isValid: true + } + ], + counters: {}, + settings: { + staticColorLabels: ["job"], + annotationsDefaultHidden: false, + annotationsHidden: [], + annotationsVisible: [] + } +}); + +export { FetchMock, EmptyAPIResponse }; From faedbf96e3a4377e6b22c13840c8a04aad8e73cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 21 Aug 2018 18:37:25 +0100 Subject: [PATCH 3/3] feat(test): more test coverage for AlertStore module --- ui/src/Stores/AlertStore.js | 41 ++++++------ ui/src/Stores/AlertStore.test.js | 103 +++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index d8c3ee582..fea8bcc69 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -192,31 +192,32 @@ class AlertStore { this.filters.setFilters(initialFilters); } - // fetch is throttled to once per 500ms - fetch = action( - throttle(() => { - this.status.setInProgress(); + fetch = action(() => { + this.status.setInProgress(); - const alertsURI = - FormatUnseeBackendURI("alerts.json?") + - FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); + const alertsURI = + FormatUnseeBackendURI("alerts.json?") + + FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); - fetch(alertsURI) - .then(result => result.json()) - .then(result => { - this.parseAPIResponse(result); - }) - .catch(err => - this.handleFetchError( - `Request for ${alertsURI} failed with "${err.message}"` - ) + return fetch(alertsURI) + .then(result => result.json()) + .then(result => { + return this.parseAPIResponse(result); + }) + .catch(err => { + console.trace(err); + return this.handleFetchError( + `Request for ${alertsURI} failed with "${err.message}"` ); - }, 500) - ); + }); + }); + + fetchWithThrottle = throttle(this.fetch, 500); parseAPIResponse = action(result => { if (result.error) { this.handleFetchError(result.error); + return; } const queryFilters = new Set( @@ -226,7 +227,9 @@ class AlertStore { .sort() ); const responseFilters = new Set(result.filters.map(m => m.text).sort()); - if (JSON.stringify(queryFilters) !== JSON.stringify(responseFilters)) { + if ( + JSON.stringify([...queryFilters]) !== JSON.stringify([...responseFilters]) + ) { console.info( `Got response with filters=${responseFilters} while expecting results for ${queryFilters}, ignoring` ); diff --git a/ui/src/Stores/AlertStore.test.js b/ui/src/Stores/AlertStore.test.js index f168b0686..67bd24a4a 100644 --- a/ui/src/Stores/AlertStore.test.js +++ b/ui/src/Stores/AlertStore.test.js @@ -1,3 +1,6 @@ +import { ConsoleMock } from "__mocks__/Console"; +import { FetchMock, EmptyAPIResponse } from "__mocks__/Fetch"; + import { AlertStore, AlertStoreStatuses, @@ -5,6 +8,16 @@ import { DecodeLocationSearch } from "Stores/AlertStore"; +beforeEach(() => { + // wipe REACT_APP_BACKEND_URI env on each run as it's used by some tests + delete process.env.REACT_APP_BACKEND_URI; +}); + +afterEach(() => { + // same after each + delete process.env.REACT_APP_BACKEND_URI; +}); + describe("AlertStore.status", () => { it("status is initially idle with no error", () => { const store = new AlertStore([]); @@ -116,11 +129,6 @@ describe("AlertStore.filters", () => { }); describe("FormatUnseeBackendURI", () => { - beforeEach(() => { - // wipe REACT_APP_BACKEND_URI env on each run as it's used by some tests - delete process.env.REACT_APP_BACKEND_URI; - }); - it("FormatUnseeBackendURI without REACT_APP_BACKEND_URI env returns ./ prefixed URIs", () => { const uri = FormatUnseeBackendURI("foo/bar"); expect(uri).toEqual("./foo/bar"); @@ -182,3 +190,88 @@ describe("DecodeLocationSearch", () => { }); }); }); + +describe("AlertStore.fetch", () => { + it("parseAPIResponse() rejects a response with mismatched filters", () => { + const consoleSpy = ConsoleMock("info"); + + const response = EmptyAPIResponse(); + const store = new AlertStore([]); + store.parseAPIResponse(response); + + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + // there should be no filters set on AlertStore instance since we started + // with 0 and rejected response with 1 filter + expect(store.filters.values).toHaveLength(0); + // console.info should have been called since we emited a warning + expect(consoleSpy).toHaveBeenCalledTimes(1); + + consoleSpy.mockRestore(); + }); + + it("parseAPIResponse() works for a single filter 'label=value'", () => { + const response = EmptyAPIResponse(); + + const store = new AlertStore(["label=value"]); + store.parseAPIResponse(response); + + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + expect(store.info.version).toBe("fakeVersion"); + }); + + it("fetch() works with valid response", async () => { + const response = EmptyAPIResponse(); + global.fetch = FetchMock(response); + + const store = new AlertStore(["label=value"]); + await expect(store.fetch()).resolves.toBeUndefined(); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + expect(store.info.version).toBe("fakeVersion"); + + global.fetch.mockRestore(); + }); + + it("fetch() handles response with error correctly", async () => { + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + json: () => ({ + error: "Fetch error" + }) + }) + ); + + const store = new AlertStore([]); + await expect(store.fetch()).resolves.toBeUndefined(); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Failure); + expect(store.info.version).toBe("unknown"); + + global.fetch.mockRestore(); + }); + + it("fetch() handles response that throws an error correctly", async () => { + const consoleSpy = ConsoleMock("trace"); + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + json: () => { + throw new Error("Failed fetch"); + } + }) + ); + + const store = new AlertStore([]); + await expect(store.fetch()).resolves.toHaveProperty("error"); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Failure); + expect(store.info.version).toBe("unknown"); + // there should be a trace of the error + expect(consoleSpy).toHaveBeenCalledTimes(1); + + consoleSpy.mockRestore(); + global.fetch.mockRestore(); + }); +});