Merge pull request #60 from prymitive/ui-tests

More test coverage for the UI code
This commit is contained in:
Łukasz Mierzwa
2018-08-21 19:32:27 +01:00
committed by GitHub
5 changed files with 168 additions and 25 deletions

View File

@@ -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

View File

@@ -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`
);

View File

@@ -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();
});
});

View File

@@ -0,0 +1,4 @@
const ConsoleMock = level =>
jest.spyOn(console, level).mockImplementation(() => jest.fn());
export { ConsoleMock };

41
ui/src/__mocks__/Fetch.js Normal file
View File

@@ -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 };