diff --git a/ui/src/App.test.js b/ui/src/App.test.js index 9862a8e57..36f95d8d4 100644 --- a/ui/src/App.test.js +++ b/ui/src/App.test.js @@ -21,6 +21,7 @@ beforeEach(() => { }); afterEach(() => { + localStorage.setItem("savedFilters", ""); jest.restoreAllMocks(); window.history.pushState({}, "App", "/"); }); @@ -119,4 +120,38 @@ describe("", () => { NewUnappliedFilter("use=query") ); }); + + it("popstate event updates alertStore filters", () => { + const tree = shallow( + + ); + expect(tree.instance().alertStore.filters.values).toHaveLength(1); + expect(tree.instance().alertStore.filters.values[0]).toMatchObject( + NewUnappliedFilter("foo") + ); + + delete global.window.location; + global.window.location = { + href: "http://localhost/?q=bar", + search: "?q=bar" + }; + + let event = new PopStateEvent("popstate"); + window.onpopstate(event); + + expect(tree.instance().alertStore.filters.values).toHaveLength(1); + expect(tree.instance().alertStore.filters.values[0]).toMatchObject( + NewUnappliedFilter("bar") + ); + }); + + it("unmounts without crashing", () => { + const tree = shallow( + + ); + tree.instance().componentWillUnmount(); + + let event = new PopStateEvent("popstate"); + window.onpopstate(event); + }); }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3a1906491..00f962665 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -62,6 +62,20 @@ class App extends Component { this.alertStore = new AlertStore(filters); } + onPopState = (event: PopStateEvent) => { + event.preventDefault(); + const p = DecodeLocationSearch(window.location.search); + this.alertStore.filters.setWithoutLocation(p.params.q); + }; + + componentDidMount() { + window.onpopstate = this.onPopState; + } + + componentWillUnmount() { + window.onpopstate = () => {}; + } + render() { return ( diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index d19c1e1ad..8e187606d 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -1,4 +1,4 @@ -import { observable, action } from "mobx"; +import { observable, action, toJS } from "mobx"; import { throttle } from "lodash"; @@ -128,6 +128,15 @@ class AlertStore { setFilters(raws) { this.values = raws.map(raw => NewUnappliedFilter(raw)); UpdateLocationSearch({ q: this.values.map(f => f.raw) }); + }, + setWithoutLocation(raws) { + const filtersByRaw = this.values.reduce(function(map, obj) { + map[toJS(obj.raw)] = toJS(obj); + return map; + }, {}); + this.values = raws.map(raw => + filtersByRaw[raw] ? filtersByRaw[raw] : NewUnappliedFilter(raw) + ); } }, { diff --git a/ui/src/Stores/AlertStore.test.js b/ui/src/Stores/AlertStore.test.js index 210ccba93..02c29099d 100644 --- a/ui/src/Stores/AlertStore.test.js +++ b/ui/src/Stores/AlertStore.test.js @@ -132,6 +132,81 @@ describe("AlertStore.filters", () => { expect(store.filters.values).toHaveLength(1); expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("bar")); }); + + it("addFilter() updates window.history", () => { + const store = new AlertStore([]); + const historyMock = jest.spyOn(global.window.history, "pushState"); + store.filters.addFilter("foo"); + expect(historyMock).toHaveBeenLastCalledWith( + null, + null, + "http://localhost/?q=foo" + ); + }); + + it("replaceFilter() updates window.history", () => { + const store = new AlertStore(["foo"]); + const historyMock = jest.spyOn(global.window.history, "pushState"); + store.filters.replaceFilter("foo", "bar"); + expect(historyMock).toHaveBeenLastCalledWith( + null, + null, + "http://localhost/?q=bar" + ); + }); + + it("addFilter() updates window.history", () => { + const store = new AlertStore([]); + const historyMock = jest.spyOn(global.window.history, "pushState"); + store.filters.addFilter("foo"); + expect(historyMock).toHaveBeenLastCalledWith( + null, + null, + "http://localhost/?q=foo" + ); + }); + + it("setFilters() updates window.history", () => { + const store = new AlertStore([]); + store.filters.addFilter("foo"); + store.filters.addFilter("bar"); + + const historyMock = jest.spyOn(global.window.history, "pushState"); + store.filters.setFilters(["baz", "far"]); + expect(store.filters.values).toHaveLength(2); + expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("baz")); + expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("far")); + expect(historyMock).toHaveBeenLastCalledWith( + null, + null, + "http://localhost/?q=baz&q=far" + ); + }); + + it("setWithoutLocation() doesn't update window.history", () => { + const store = new AlertStore(["far", "foo"]); + + const historyMock = jest.spyOn(global.window.history, "pushState"); + store.filters.setWithoutLocation(["baz", "far"]); + expect(store.filters.values).toHaveLength(2); + expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("baz")); + expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("far")); + expect(historyMock).not.toHaveBeenCalled(); + }); + + it("setWithoutLocation() adds missing filters", () => { + const store = new AlertStore([]); + store.filters.setWithoutLocation(["foo", "bar"]); + expect(store.filters.values).toHaveLength(2); + expect(store.filters.values[0]).toMatchObject(NewUnappliedFilter("foo")); + expect(store.filters.values[1]).toMatchObject(NewUnappliedFilter("bar")); + }); + + it("setWithoutLocation() removes orphaned filters", () => { + const store = new AlertStore(["far"]); + store.filters.setWithoutLocation([]); + expect(store.filters.values).toHaveLength(0); + }); }); describe("FormatBackendURI", () => {