mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
feat(ui): follow browser preferences when setting theme
This commit is contained in:
Generated
+24
@@ -11390,6 +11390,14 @@
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"json2mq": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
|
||||
"integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=",
|
||||
"requires": {
|
||||
"string-convert": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"json3": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
|
||||
@@ -15348,6 +15356,17 @@
|
||||
"react-infinite-scroller": "^1.0.12"
|
||||
}
|
||||
},
|
||||
"react-media": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-media/-/react-media-1.10.0.tgz",
|
||||
"integrity": "sha512-FjgYmFoaPTImST06jqotuu0Mk8LOXiGYS/fIyiXuLnf20l3DPniBwtrxi604/HxxjqvmHS3oz5rAwnqdvosV4A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"invariant": "^2.2.2",
|
||||
"json2mq": "^0.2.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-moment": {
|
||||
"version": "0.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.6.tgz",
|
||||
@@ -17527,6 +17546,11 @@
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-convert": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
|
||||
"integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c="
|
||||
},
|
||||
"string-length": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"react-json-pretty": "2.2.0",
|
||||
"react-linkify": "0.2.2",
|
||||
"react-masonry-infinite": "1.2.2",
|
||||
"react-media": "1.10.0",
|
||||
"react-moment": "0.9.6",
|
||||
"react-onclickoutside": "6.9.0",
|
||||
"react-popper": "1.3.6",
|
||||
|
||||
+124
-27
@@ -2,6 +2,7 @@ import React from "react";
|
||||
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
||||
import { mockMatchMedia } from "__mocks__/matchMedia";
|
||||
import { NewUnappliedFilter } from "Stores/AlertStore";
|
||||
import { App } from "./App";
|
||||
|
||||
@@ -19,14 +20,14 @@ beforeEach(() => {
|
||||
// ensure it's wiped after each test
|
||||
window.history.pushState({}, "App", "/");
|
||||
|
||||
document.body.className = "";
|
||||
// matchMedia needs mocking
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.setItem("savedFilters", "");
|
||||
jest.restoreAllMocks();
|
||||
window.history.pushState({}, "App", "/");
|
||||
document.body.className = "";
|
||||
});
|
||||
|
||||
describe("<App />", () => {
|
||||
@@ -157,44 +158,140 @@ describe("<App />", () => {
|
||||
let event = new PopStateEvent("popstate");
|
||||
window.onpopstate(event);
|
||||
});
|
||||
});
|
||||
|
||||
it("appends correct theme class to #root if dark mode is disabled", () => {
|
||||
const tree = shallow(
|
||||
describe("<App /> theme", () => {
|
||||
const getApp = theme =>
|
||||
mount(
|
||||
<App
|
||||
defaultFilters={["foo=bar"]}
|
||||
uiDefaults={Object.assign({}, uiDefaults, { DarkMode: false })}
|
||||
uiDefaults={Object.assign({}, uiDefaults, { Theme: theme })}
|
||||
/>
|
||||
);
|
||||
|
||||
it("configures light theme when uiDefaults passes it", () => {
|
||||
const tree = getApp("light");
|
||||
expect(tree.instance().settingsStore.themeConfig.config.theme).toBe(
|
||||
"light"
|
||||
);
|
||||
tree.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
expect(document.body.className.split(" ")).toContain("theme-light");
|
||||
it("configures dark theme when uiDefaults passes it", () => {
|
||||
const tree = getApp("dark");
|
||||
expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("dark");
|
||||
tree.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
it("appends 'theme-dark' class to #root if dark mode is enabled", () => {
|
||||
const tree = shallow(
|
||||
<App
|
||||
defaultFilters={["foo=bar"]}
|
||||
uiDefaults={Object.assign({}, uiDefaults, { DarkMode: true })}
|
||||
/>
|
||||
);
|
||||
it("configures automatic theme when uiDefaults passes it", () => {
|
||||
const tree = getApp("auto");
|
||||
expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("auto");
|
||||
tree.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
it("configures automatic theme when uiDefaults doesn't pass any value", () => {
|
||||
const tree = mount(<App defaultFilters={["foo=bar"]} uiDefaults={null} />);
|
||||
expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("auto");
|
||||
tree.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
expect(document.body.className.split(" ")).toContain("theme-dark");
|
||||
it("applies light theme when theme=auto and browser doesn't support prefers-color-scheme", () => {
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
const tree = getApp("auto");
|
||||
expect(tree.find("LightTheme")).toHaveLength(1);
|
||||
tree.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
it("toggling settingsStore.themeConfig.config.darkTheme modifies the theme", () => {
|
||||
const tree = mount(
|
||||
<App
|
||||
defaultFilters={["foo=bar"]}
|
||||
uiDefaults={Object.assign({}, uiDefaults, { DarkMode: false })}
|
||||
/>
|
||||
);
|
||||
tree.update();
|
||||
expect(document.body.className.split(" ")).toContain("theme-light");
|
||||
const lightMatch = () => ({
|
||||
"(prefers-color-scheme)": {
|
||||
media: "(prefers-color-scheme)",
|
||||
matches: true
|
||||
},
|
||||
"(prefers-color-scheme: light)": {
|
||||
media: "(prefers-color-scheme: light)",
|
||||
matches: true
|
||||
},
|
||||
"(prefers-color-scheme: dark)": {
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
matches: false
|
||||
}
|
||||
});
|
||||
|
||||
tree.instance().settingsStore.themeConfig.config.darkTheme = true;
|
||||
tree.update();
|
||||
expect(document.body.className.split(" ")).toContain("theme-dark");
|
||||
tree.instance().componentWillUnmount();
|
||||
const darkMatch = () => ({
|
||||
"(prefers-color-scheme)": {
|
||||
media: "(prefers-color-scheme)",
|
||||
matches: true
|
||||
},
|
||||
"(prefers-color-scheme: light)": {
|
||||
media: "(prefers-color-scheme: light)",
|
||||
matches: false
|
||||
},
|
||||
"(prefers-color-scheme: dark)": {
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
matches: true
|
||||
}
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name:
|
||||
"applies LightTheme when config=auto and browser doesn't support prefers-color-scheme",
|
||||
settings: "auto",
|
||||
matchMedia: {},
|
||||
theme: "LightTheme"
|
||||
},
|
||||
{
|
||||
name:
|
||||
"applies LightTheme when config=auto and browser prefers-color-scheme:light matches",
|
||||
settings: "auto",
|
||||
matchMedia: lightMatch(),
|
||||
theme: "LightTheme"
|
||||
},
|
||||
{
|
||||
name:
|
||||
"applies DarkTheme when config=auto and browser prefers-color-scheme:dark matches",
|
||||
settings: "auto",
|
||||
matchMedia: darkMatch(),
|
||||
theme: "DarkTheme"
|
||||
},
|
||||
|
||||
{
|
||||
name:
|
||||
"applies LightTheme when config=light and browser doesn't support prefers-color-scheme",
|
||||
settings: "light",
|
||||
matchMedia: {},
|
||||
theme: "LightTheme"
|
||||
},
|
||||
{
|
||||
name:
|
||||
"applies LightTheme when config=light and browser prefers-color-scheme:light matches",
|
||||
settings: "light",
|
||||
matchMedia: lightMatch(),
|
||||
theme: "LightTheme"
|
||||
},
|
||||
|
||||
{
|
||||
name:
|
||||
"applies DarkTheme when config=dark and browser doesn't support prefers-color-scheme",
|
||||
settings: "dark",
|
||||
matchMedia: {},
|
||||
theme: "DarkTheme"
|
||||
},
|
||||
{
|
||||
name:
|
||||
"applies DarkTheme when config=dark and browser prefers-color-scheme:dark matches",
|
||||
settings: "dark",
|
||||
matchMedia: darkMatch(),
|
||||
theme: "DarkTheme"
|
||||
}
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
window.matchMedia = mockMatchMedia(testCase.matchMedia);
|
||||
const tree = getApp(testCase.settings);
|
||||
expect(tree.find(testCase.theme)).toHaveLength(1);
|
||||
tree.instance().componentWillUnmount();
|
||||
window.matchMedia.mockRestore();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+50
-33
@@ -2,6 +2,8 @@ import React, { Component } from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import Media from "react-media";
|
||||
|
||||
import { AlertStore, DecodeLocationSearch } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
@@ -11,7 +13,7 @@ import {
|
||||
ReactSelectColors,
|
||||
ReactSelectStyles
|
||||
} from "Components/Theme/ReactSelect";
|
||||
import { Theme, ThemeContext } from "Components/Theme";
|
||||
import { BodyTheme, ThemeContext } from "Components/Theme";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
import "Styles/ResetCSS.scss";
|
||||
@@ -59,8 +61,6 @@ const App = observer(
|
||||
this.silenceFormStore = new SilenceFormStore();
|
||||
this.settingsStore = new Settings(uiDefaults);
|
||||
|
||||
this.state = { darkTheme: false };
|
||||
|
||||
let filters;
|
||||
|
||||
// parse and decode request query args
|
||||
@@ -90,15 +90,6 @@ const App = observer(
|
||||
|
||||
componentDidMount() {
|
||||
window.onpopstate = this.onPopState;
|
||||
|
||||
document.body.classList.toggle(
|
||||
"theme-dark",
|
||||
this.settingsStore.themeConfig.config.darkTheme
|
||||
);
|
||||
document.body.classList.toggle(
|
||||
"theme-light",
|
||||
!this.settingsStore.themeConfig.config.darkTheme
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -108,29 +99,55 @@ const App = observer(
|
||||
render() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Theme settingsStore={this.settingsStore} />
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
reactSelectStyles: this.settingsStore.themeConfig.config.darkTheme
|
||||
? ReactSelectStyles(ReactSelectColors.Dark)
|
||||
: ReactSelectStyles(ReactSelectColors.Light)
|
||||
<span data-theme={`${this.settingsStore.themeConfig.config.theme}`} />
|
||||
<Media
|
||||
queries={{
|
||||
isSupported: "(prefers-color-scheme)",
|
||||
light: "(prefers-color-scheme: light)",
|
||||
dark: "(prefers-color-scheme: dark)"
|
||||
}}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<NavBar
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Grid
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</ThemeContext.Provider>
|
||||
{matches => (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
isDark:
|
||||
this.settingsStore.themeConfig.config.theme ===
|
||||
this.settingsStore.themeConfig.options.auto.value &&
|
||||
matches.isSupported
|
||||
? matches.dark
|
||||
: this.settingsStore.themeConfig.config.theme ===
|
||||
this.settingsStore.themeConfig.options.dark.value,
|
||||
reactSelectStyles:
|
||||
this.settingsStore.themeConfig.config.theme ===
|
||||
this.settingsStore.themeConfig.options.auto.value &&
|
||||
matches.isSupported
|
||||
? matches.dark
|
||||
? ReactSelectStyles(ReactSelectColors.Dark)
|
||||
: ReactSelectStyles(ReactSelectColors.Light)
|
||||
: this.settingsStore.themeConfig.config.theme ===
|
||||
this.settingsStore.themeConfig.options.dark.value
|
||||
? ReactSelectStyles(ReactSelectColors.Dark)
|
||||
: ReactSelectStyles(ReactSelectColors.Light)
|
||||
}}
|
||||
>
|
||||
<BodyTheme />
|
||||
<React.Suspense fallback={null}>
|
||||
<NavBar
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Grid
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
silenceFormStore={this.silenceFormStore}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</ThemeContext.Provider>
|
||||
)}
|
||||
</Media>
|
||||
<FaviconBadge alertStore={this.alertStore} />
|
||||
<Fetcher
|
||||
alertStore={this.alertStore}
|
||||
|
||||
@@ -22,7 +22,7 @@ const AlertGroupTitleBarColor = observer(
|
||||
const { settingsStore } = this.props;
|
||||
|
||||
return (
|
||||
<div className="form-group mb-2">
|
||||
<div className="form-group mb-0">
|
||||
<div className="form-check form-check-inline">
|
||||
<span className="custom-control custom-switch">
|
||||
<input
|
||||
|
||||
@@ -4,54 +4,68 @@ import PropTypes from "prop-types";
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import Select from "react-select";
|
||||
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
|
||||
const ThemeConfiguration = observer(
|
||||
class ThemeConfiguration extends Component {
|
||||
static propTypes = {
|
||||
settingsStore: PropTypes.instanceOf(Settings).isRequired
|
||||
};
|
||||
static contextType = ThemeContext;
|
||||
|
||||
onChange = action(event => {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.validateConfig();
|
||||
}
|
||||
|
||||
valueToOption = val => {
|
||||
const { settingsStore } = this.props;
|
||||
settingsStore.themeConfig.config.darkTheme = event.target.checked;
|
||||
|
||||
document.body.classList.toggle(
|
||||
"theme-dark",
|
||||
settingsStore.themeConfig.config.darkTheme
|
||||
);
|
||||
document.body.classList.toggle(
|
||||
"theme-light",
|
||||
!settingsStore.themeConfig.config.darkTheme
|
||||
);
|
||||
return {
|
||||
label: settingsStore.themeConfig.options[val].label,
|
||||
value: val
|
||||
};
|
||||
};
|
||||
|
||||
validateConfig = action(() => {
|
||||
const { settingsStore } = this.props;
|
||||
|
||||
if (
|
||||
!Object.values(settingsStore.themeConfig.options)
|
||||
.map(o => o.value)
|
||||
.includes(settingsStore.themeConfig.config.theme)
|
||||
) {
|
||||
settingsStore.themeConfig.config.theme =
|
||||
settingsStore.themeConfig.options.auto.value;
|
||||
}
|
||||
});
|
||||
|
||||
onCollapseChange = action((newValue, actionMeta) => {
|
||||
const { settingsStore } = this.props;
|
||||
|
||||
settingsStore.themeConfig.config.theme = newValue.value;
|
||||
});
|
||||
|
||||
render() {
|
||||
const { settingsStore } = this.props;
|
||||
|
||||
return (
|
||||
<div className="form-group mb-0">
|
||||
<div className="form-check form-check-inline">
|
||||
<span className="custom-control custom-switch">
|
||||
<input
|
||||
id="configuration-theme"
|
||||
className="custom-control-input"
|
||||
type="checkbox"
|
||||
value=""
|
||||
checked={settingsStore.themeConfig.config.darkTheme || false}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<label
|
||||
className="custom-control-label cursor-pointer mr-3"
|
||||
htmlFor="configuration-theme"
|
||||
>
|
||||
Enable dark mode
|
||||
</label>
|
||||
<span className="ml-5 badge badge-danger align-text-bottom">
|
||||
Experimental
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-group mb-2">
|
||||
<Select
|
||||
styles={this.context.reactSelectStyles}
|
||||
classNamePrefix="react-select"
|
||||
instanceId="configuration-theme"
|
||||
defaultValue={this.valueToOption(
|
||||
settingsStore.themeConfig.config.theme
|
||||
)}
|
||||
options={Object.values(settingsStore.themeConfig.options)}
|
||||
onChange={this.onCollapseChange}
|
||||
hideSelectedOptions
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import { mount } from "enzyme";
|
||||
import toDiffableHtml from "diffable-html";
|
||||
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { ThemeContext } from "Components/Theme";
|
||||
import {
|
||||
ReactSelectColors,
|
||||
ReactSelectStyles
|
||||
} from "Components/Theme/ReactSelect";
|
||||
import { ThemeConfiguration } from "./ThemeConfiguration";
|
||||
|
||||
let settingsStore;
|
||||
@@ -13,7 +18,15 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
const FakeConfiguration = () => {
|
||||
return mount(<ThemeConfiguration settingsStore={settingsStore} />);
|
||||
return mount(
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
reactSelectStyles: ReactSelectStyles(ReactSelectColors.Light)
|
||||
}}
|
||||
>
|
||||
<ThemeConfiguration settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<ThemeConfiguration />", () => {
|
||||
@@ -22,32 +35,41 @@ describe("<ThemeConfiguration />", () => {
|
||||
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("darkTheme is 'false' by default", () => {
|
||||
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
|
||||
});
|
||||
|
||||
it("unchecking the checkbox sets stored darkTheme value to 'false'", done => {
|
||||
it("resets stored config to defaults if it is invalid", done => {
|
||||
settingsStore.themeConfig.config.theme = "foo";
|
||||
const tree = FakeConfiguration();
|
||||
const checkbox = tree.find("#configuration-theme");
|
||||
|
||||
settingsStore.themeConfig.config.darkTheme = true;
|
||||
expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
|
||||
checkbox.simulate("change", { target: { checked: false } });
|
||||
const select = tree.find("div.react-select__value-container");
|
||||
expect(select.text()).toBe(settingsStore.themeConfig.options.auto.label);
|
||||
setTimeout(() => {
|
||||
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
|
||||
expect(settingsStore.themeConfig.config.theme).toBe(
|
||||
settingsStore.themeConfig.options.auto.value
|
||||
);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("checking the checkbox sets stored darkTheme value to 'true'", done => {
|
||||
it("rendered correct default value", done => {
|
||||
settingsStore.themeConfig.config.theme =
|
||||
settingsStore.themeConfig.options.auto.value;
|
||||
const tree = FakeConfiguration();
|
||||
const checkbox = tree.find("#configuration-theme");
|
||||
|
||||
settingsStore.themeConfig.config.darkTheme = false;
|
||||
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
|
||||
checkbox.simulate("change", { target: { checked: true } });
|
||||
const select = tree.find("div.react-select__value-container");
|
||||
setTimeout(() => {
|
||||
expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
|
||||
expect(select.text()).toBe(settingsStore.themeConfig.options.auto.label);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("clicking on a label option updates settingsStore", done => {
|
||||
const tree = FakeConfiguration();
|
||||
tree
|
||||
.find("input#react-select-configuration-theme-input")
|
||||
.simulate("change", { target: { value: " " } });
|
||||
const options = tree.find("div.react-select__option");
|
||||
options.at(1).simulate("click");
|
||||
setTimeout(() => {
|
||||
expect(settingsStore.themeConfig.config.theme).toBe(
|
||||
settingsStore.themeConfig.options.dark.value
|
||||
);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`<AlertGroupTitleBarColor /> matches snapshot with default values 1`] = `
|
||||
"
|
||||
<div class=\\"form-group mb-2\\">
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-colortitlebar\\"
|
||||
|
||||
+46
-17
@@ -2,23 +2,52 @@
|
||||
|
||||
exports[`<ThemeConfiguration /> matches snapshot with default values 1`] = `
|
||||
"
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-theme\\"
|
||||
class=\\"custom-control-input\\"
|
||||
type=\\"checkbox\\"
|
||||
value
|
||||
>
|
||||
<label class=\\"custom-control-label cursor-pointer mr-3\\"
|
||||
for=\\"configuration-theme\\"
|
||||
>
|
||||
Enable dark mode
|
||||
</label>
|
||||
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
|
||||
Experimental
|
||||
</span>
|
||||
</span>
|
||||
<div class=\\"form-group mb-2\\">
|
||||
<div class=\\" css-2b097c-container\\">
|
||||
<div class=\\"react-select__control css-r5n82u-control\\">
|
||||
<div class=\\"react-select__value-container react-select__value-container--has-value css-97xgis\\">
|
||||
<div class=\\"react-select__single-value css-1wh03ml-singleValue\\">
|
||||
Automatic theme, follow browser preferences
|
||||
</div>
|
||||
<div class=\\"css-b8ldur-Input\\">
|
||||
<div class=\\"react-select__input\\"
|
||||
style=\\"display: inline-block;\\"
|
||||
>
|
||||
<input autocapitalize=\\"none\\"
|
||||
autocomplete=\\"off\\"
|
||||
autocorrect=\\"off\\"
|
||||
id=\\"react-select-configuration-theme-input\\"
|
||||
spellcheck=\\"false\\"
|
||||
tabindex=\\"0\\"
|
||||
type=\\"text\\"
|
||||
aria-autocomplete=\\"list\\"
|
||||
style=\\"box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;\\"
|
||||
value
|
||||
>
|
||||
<div style=\\"position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-size: inherit; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;\\">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"react-select__indicators css-vcwr3k-IndicatorsContainer\\">
|
||||
<span class=\\"react-select__indicator-separator css-1okebmr-indicatorSeparator\\">
|
||||
</span>
|
||||
<div aria-hidden=\\"true\\"
|
||||
class=\\"react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer\\"
|
||||
>
|
||||
<svg height=\\"20\\"
|
||||
width=\\"20\\"
|
||||
viewbox=\\"0 0 20 20\\"
|
||||
aria-hidden=\\"true\\"
|
||||
focusable=\\"false\\"
|
||||
class=\\"css-6q0nyr-Svg\\"
|
||||
>
|
||||
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"
|
||||
|
||||
@@ -151,6 +151,54 @@ exports[`<Configuration /> matches snapshot 1`] = `
|
||||
>
|
||||
<div class=\\"Collapsible__contentInner card-body my-2\\">
|
||||
<div class=\\"form-group mb-2\\">
|
||||
<div class=\\" css-2b097c-container\\">
|
||||
<div class=\\"react-select__control css-r5n82u-control\\">
|
||||
<div class=\\"react-select__value-container react-select__value-container--has-value css-97xgis\\">
|
||||
<div class=\\"react-select__single-value css-1wh03ml-singleValue\\">
|
||||
Automatic theme, follow browser preferences
|
||||
</div>
|
||||
<div class=\\"css-b8ldur-Input\\">
|
||||
<div class=\\"react-select__input\\"
|
||||
style=\\"display: inline-block;\\"
|
||||
>
|
||||
<input autocapitalize=\\"none\\"
|
||||
autocomplete=\\"off\\"
|
||||
autocorrect=\\"off\\"
|
||||
id=\\"react-select-configuration-theme-input\\"
|
||||
spellcheck=\\"false\\"
|
||||
tabindex=\\"0\\"
|
||||
type=\\"text\\"
|
||||
aria-autocomplete=\\"list\\"
|
||||
style=\\"box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;\\"
|
||||
value
|
||||
>
|
||||
<div style=\\"position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-size: inherit; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;\\">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"react-select__indicators css-vcwr3k-IndicatorsContainer\\">
|
||||
<span class=\\"react-select__indicator-separator css-1okebmr-indicatorSeparator\\">
|
||||
</span>
|
||||
<div aria-hidden=\\"true\\"
|
||||
class=\\"react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer\\"
|
||||
>
|
||||
<svg height=\\"20\\"
|
||||
width=\\"20\\"
|
||||
viewbox=\\"0 0 20 20\\"
|
||||
aria-hidden=\\"true\\"
|
||||
focusable=\\"false\\"
|
||||
class=\\"css-6q0nyr-Svg\\"
|
||||
>
|
||||
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-colortitlebar\\"
|
||||
@@ -166,25 +214,6 @@ exports[`<Configuration /> matches snapshot 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-theme\\"
|
||||
class=\\"custom-control-input\\"
|
||||
type=\\"checkbox\\"
|
||||
value
|
||||
>
|
||||
<label class=\\"custom-control-label cursor-pointer mr-3\\"
|
||||
for=\\"configuration-theme\\"
|
||||
>
|
||||
Enable dark mode
|
||||
</label>
|
||||
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
|
||||
Experimental
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@ const Configuration = ({ settingsStore, defaultIsOpen }) => (
|
||||
text="Theme"
|
||||
content={
|
||||
<React.Fragment>
|
||||
<AlertGroupTitleBarColor settingsStore={settingsStore} />
|
||||
<ThemeConfiguration settingsStore={settingsStore} />
|
||||
<AlertGroupTitleBarColor settingsStore={settingsStore} />
|
||||
</React.Fragment>
|
||||
}
|
||||
extraProps={{ open: defaultIsOpen }}
|
||||
|
||||
@@ -170,6 +170,54 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
|
||||
>
|
||||
<div class=\\"Collapsible__contentInner card-body my-2\\">
|
||||
<div class=\\"form-group mb-2\\">
|
||||
<div class=\\" css-2b097c-container\\">
|
||||
<div class=\\"react-select__control css-r5n82u-control\\">
|
||||
<div class=\\"react-select__value-container react-select__value-container--has-value css-97xgis\\">
|
||||
<div class=\\"react-select__single-value css-1wh03ml-singleValue\\">
|
||||
Automatic theme, follow browser preferences
|
||||
</div>
|
||||
<div class=\\"css-b8ldur-Input\\">
|
||||
<div class=\\"react-select__input\\"
|
||||
style=\\"display: inline-block;\\"
|
||||
>
|
||||
<input autocapitalize=\\"none\\"
|
||||
autocomplete=\\"off\\"
|
||||
autocorrect=\\"off\\"
|
||||
id=\\"react-select-configuration-theme-input\\"
|
||||
spellcheck=\\"false\\"
|
||||
tabindex=\\"0\\"
|
||||
type=\\"text\\"
|
||||
aria-autocomplete=\\"list\\"
|
||||
style=\\"box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;\\"
|
||||
value
|
||||
>
|
||||
<div style=\\"position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-size: inherit; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;\\">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"react-select__indicators css-vcwr3k-IndicatorsContainer\\">
|
||||
<span class=\\"react-select__indicator-separator css-1okebmr-indicatorSeparator\\">
|
||||
</span>
|
||||
<div aria-hidden=\\"true\\"
|
||||
class=\\"react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer\\"
|
||||
>
|
||||
<svg height=\\"20\\"
|
||||
width=\\"20\\"
|
||||
viewbox=\\"0 0 20 20\\"
|
||||
aria-hidden=\\"true\\"
|
||||
focusable=\\"false\\"
|
||||
class=\\"css-6q0nyr-Svg\\"
|
||||
>
|
||||
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-colortitlebar\\"
|
||||
@@ -185,25 +233,6 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"form-group mb-0\\">
|
||||
<div class=\\"form-check form-check-inline\\">
|
||||
<span class=\\"custom-control custom-switch\\">
|
||||
<input id=\\"configuration-theme\\"
|
||||
class=\\"custom-control-input\\"
|
||||
type=\\"checkbox\\"
|
||||
value
|
||||
>
|
||||
<label class=\\"custom-control-label cursor-pointer mr-3\\"
|
||||
for=\\"configuration-theme\\"
|
||||
>
|
||||
Enable dark mode
|
||||
</label>
|
||||
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
|
||||
Experimental
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
||||
|
||||
@@ -41,16 +39,30 @@ const Placeholder = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Theme = observer(({ settingsStore }) => (
|
||||
<React.Suspense fallback={<Placeholder />}>
|
||||
{settingsStore.themeConfig.config.darkTheme ? (
|
||||
<DarkTheme />
|
||||
) : (
|
||||
<LightTheme />
|
||||
)}
|
||||
</React.Suspense>
|
||||
));
|
||||
|
||||
const ThemeContext = React.createContext();
|
||||
|
||||
export { Theme, ThemeContext };
|
||||
class BodyTheme extends Component {
|
||||
onToggleBodyClass = isDark => {
|
||||
document.body.classList.toggle("theme-dark", isDark);
|
||||
document.body.classList.toggle("theme-light", !isDark);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.onToggleBodyClass(this.context.isDark);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.onToggleBodyClass(this.context.isDark);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Suspense fallback={<Placeholder />}>
|
||||
{this.context.isDark ? <DarkTheme /> : <LightTheme />}
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
BodyTheme.contextType = ThemeContext;
|
||||
|
||||
export { BodyTheme, ThemeContext };
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { Theme } from ".";
|
||||
|
||||
let settingsStore;
|
||||
import { BodyTheme, ThemeContext } from ".";
|
||||
|
||||
beforeEach(() => {
|
||||
settingsStore = new Settings();
|
||||
document.body.classList.remove("theme-light");
|
||||
document.body.classList.remove("theme-dark");
|
||||
});
|
||||
|
||||
describe("<Theme />", () => {
|
||||
it("renders DarkTheme when settingsStore.themeConfig.config.darkTheme is true", () => {
|
||||
settingsStore.themeConfig.config.darkTheme = true;
|
||||
const tree = mount(<Theme settingsStore={settingsStore} />);
|
||||
expect(tree.text()).toBe("");
|
||||
describe("<BodyTheme />", () => {
|
||||
it("uses light theme when ThemeContext->isDark is false", () => {
|
||||
mount(<BodyTheme />, {
|
||||
wrappingComponent: ThemeContext.Provider,
|
||||
wrappingComponentProps: { value: { isDark: false } }
|
||||
});
|
||||
expect(document.body.classList.contains("theme-light")).toEqual(true);
|
||||
});
|
||||
|
||||
it("renders LightTheme when settingsStore.themeConfig.config.darkTheme is false", () => {
|
||||
settingsStore.themeConfig.config.darkTheme = false;
|
||||
const tree = mount(<Theme settingsStore={settingsStore} />);
|
||||
expect(tree.text()).toBe("");
|
||||
it("uses dark theme when ThemeContext->isDark is true", () => {
|
||||
mount(<BodyTheme />, {
|
||||
wrappingComponent: ThemeContext.Provider,
|
||||
wrappingComponentProps: { value: { isDark: true } }
|
||||
});
|
||||
expect(document.body.classList.contains("theme-dark")).toEqual(true);
|
||||
});
|
||||
|
||||
it("updates theme when ThemeContext->isDark is updated", () => {
|
||||
const tree = mount(<BodyTheme />, {
|
||||
wrappingComponent: ThemeContext.Provider,
|
||||
wrappingComponentProps: { value: { isDark: true } }
|
||||
});
|
||||
expect(document.body.classList.contains("theme-dark")).toEqual(true);
|
||||
|
||||
document.body.classList.remove("theme-light");
|
||||
document.body.classList.remove("theme-dark");
|
||||
|
||||
const provider = tree.getWrappingComponent();
|
||||
provider.setProps({ value: { isDark: false } });
|
||||
|
||||
expect(document.body.classList.contains("theme-light")).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,11 +111,19 @@ class FilterBarConfig {
|
||||
}
|
||||
|
||||
class ThemeConfig {
|
||||
constructor(darkTheme) {
|
||||
options = Object.freeze({
|
||||
auto: {
|
||||
label: "Automatic theme, follow browser preferences",
|
||||
value: "auto"
|
||||
},
|
||||
light: { label: "Light theme", value: "light" },
|
||||
dark: { label: "Dark theme", value: "dark" }
|
||||
});
|
||||
constructor(defaultTheme) {
|
||||
this.config = localStored(
|
||||
"themeConfig",
|
||||
{
|
||||
darkTheme: darkTheme
|
||||
theme: defaultTheme
|
||||
},
|
||||
{
|
||||
delay: 100
|
||||
@@ -132,7 +140,7 @@ class Settings {
|
||||
Refresh: 30 * 1000 * 1000 * 1000,
|
||||
HideFiltersWhenIdle: true,
|
||||
ColorTitlebar: false,
|
||||
DarkTheme: false,
|
||||
Theme: "auto",
|
||||
MinimalGroupWidth: 420,
|
||||
AlertsPerGroup: 5,
|
||||
CollapseGroups: "collapsedOnMobile"
|
||||
@@ -155,7 +163,7 @@ class Settings {
|
||||
this.filterBarConfig = new FilterBarConfig(
|
||||
defaultSettings.HideFiltersWhenIdle
|
||||
);
|
||||
this.themeConfig = new ThemeConfig(defaultSettings.DarkTheme);
|
||||
this.themeConfig = new ThemeConfig(defaultSettings.Theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const DefaultsBase64 =
|
||||
"eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJEYXJrTW9kZSI6ZmFsc2UsIk1pbmltYWxHcm91cFdpZHRoIjo1NTUsIkFsZXJ0c1Blckdyb3VwIjoxNSwiQ29sbGFwc2VHcm91cHMiOiJleHBhbmRlZCJ9Cg==";
|
||||
"eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJUaGVtZSI6ImxpZ2h0IiwiTWluaW1hbEdyb3VwV2lkdGgiOjU1NSwiQWxlcnRzUGVyR3JvdXAiOjE1LCJDb2xsYXBzZUdyb3VwcyI6ImV4cGFuZGVkIn0K==";
|
||||
const DefaultsObject = {
|
||||
Refresh: 45000000000,
|
||||
HideFiltersWhenIdle: false,
|
||||
ColorTitlebar: false,
|
||||
DarkMode: false,
|
||||
Theme: "light",
|
||||
MinimalGroupWidth: 555,
|
||||
AlertsPerGroup: 15,
|
||||
CollapseGroups: "expanded"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const mockMatchMedia = mapOfMedia => {
|
||||
return jest.fn().mockImplementation(query => {
|
||||
return {
|
||||
matches: mapOfMedia[query] ? mapOfMedia[query].matches : false,
|
||||
media: mapOfMedia[query] ? mapOfMedia[query].media : "not all",
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export { mockMatchMedia };
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EmptyAPIResponse } from "__mocks__/Fetch";
|
||||
import { DefaultsBase64 } from "__mocks__/Defaults";
|
||||
import { mockMatchMedia } from "__mocks__/matchMedia";
|
||||
|
||||
const settingsElement = {
|
||||
dataset: {
|
||||
@@ -9,6 +10,14 @@ const settingsElement = {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders without crashing with missing defaults div", () => {
|
||||
const root = document.createElement("div");
|
||||
jest.spyOn(global.document, "getElementById").mockImplementation(name => {
|
||||
|
||||
Reference in New Issue
Block a user