diff --git a/.circleci/config.yml b/.circleci/config.yml
index 83729f7..f5de002 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -8,7 +8,7 @@ version: 2
.test: &test
run:
name: "Test"
- command: npm test
+ command: npm run coverage -- --maxWorkers=4
.store_version: &store_version
run:
@@ -50,6 +50,11 @@ jobs:
chmod +x ./FTL-client
./FTL-client "$FOLDER" pihole-web.tar.gz "$FTL_SECRET"
rm ./FTL-client
+ - run:
+ name: "Upload Code Coverage"
+ command: |
+ npm install codecov -g
+ codecov
- save_cache:
key: v1-build-{{ checksum "package-lock.json" }}
paths:
diff --git a/.gitignore b/.gitignore
index f46e49f..44052dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@ node_modules
build
src/scss/style.css
+# Test Files
+coverage
+
# Generated Files
src/languages.json
diff --git a/package-lock.json b/package-lock.json
index 4325523..bbc5894 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1177,9 +1177,9 @@
}
},
"@coreui/coreui": {
- "version": "2.1.11",
- "resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-2.1.11.tgz",
- "integrity": "sha512-qGgRcNstB6bqW+D5UMUFQHPVUD4gfTmsPsZpiQYNLvAKWCszq2LhilTIWAAJHxs9yf2iTer0s30MjccTsqjXOA==",
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-2.1.12.tgz",
+ "integrity": "sha512-wO2olnHWyvzW6CHaiU4oRq/tomuuHItiifcyU40UBN00vACjBUH4kpxfhZGGjwM031GRZRL6LnAWzwZb9wg8Lw==",
"requires": {
"@coreui/coreui-plugin-npm-postinstall": "^1.0.2",
"bootstrap": "^4.3.1",
@@ -1300,9 +1300,9 @@
"dev": true
},
"@fortawesome/fontawesome-free": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.8.2.tgz",
- "integrity": "sha512-E4fDUF4fbu9AxKpaQQqCN3XBnNzb/5e0Gvd9OaQsYkK574LVI57v/EqqPfIm/mC7jYbxaPNrhvT5AF+Yzwyizg=="
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz",
+ "integrity": "sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg=="
},
"@hapi/address": {
"version": "2.0.0",
@@ -2610,9 +2610,9 @@
}
},
"@types/jest": {
- "version": "24.0.13",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.13.tgz",
- "integrity": "sha512-3m6RPnO35r7Dg+uMLj1+xfZaOgIHHHut61djNjzwExXN4/Pm9has9C6I1KMYSfz7mahDhWUOVg4HW/nZdv5Pww==",
+ "version": "24.0.14",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.14.tgz",
+ "integrity": "sha512-IxS2AO0nOr4zrpKfRCxobQUb1bSK6ejodZ7odCzHXMjsASCI8J10N8qVQhrCjvJTc3bUjGGeuD+ytKZqyhajqQ==",
"dev": true,
"requires": {
"@types/jest-diff": "*"
@@ -2648,10 +2648,19 @@
"@types/lodash": "*"
}
},
+ "@types/lodash.isequal": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz",
+ "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"@types/node": {
- "version": "12.0.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.4.tgz",
- "integrity": "sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw==",
+ "version": "12.0.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.8.tgz",
+ "integrity": "sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==",
"dev": true
},
"@types/prop-types": {
@@ -9990,13 +9999,6 @@
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
"requires": {
"react-is": "^16.7.0"
- },
- "dependencies": {
- "react-is": {
- "version": "16.8.6",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
- "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
- }
}
},
"hosted-git-info": {
@@ -10517,9 +10519,9 @@
"dev": true
},
"i18next": {
- "version": "17.0.1",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.1.tgz",
- "integrity": "sha512-pg+RxJWoqEt4JtueOkoxXshivtmsf8u0SuTYQTSB5cUTNWUyw/Py3zF//A/vd5FEUaiooyPCNNJ94leQ8J4JEQ==",
+ "version": "17.0.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.3.tgz",
+ "integrity": "sha512-vQyW6a4ZLt3Dxnd6GXSnhbW5DwGYC4uLPKk1MFE5pfFbR9CEiNatdwwUZDQfrcNOh2x0eOGDFYeCEyLlkLvDQA==",
"requires": {
"@babel/runtime": "^7.3.1"
},
@@ -14892,8 +14894,7 @@
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
- "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
- "dev": true
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.isfunction": {
"version": "3.0.9",
@@ -15250,6 +15251,31 @@
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
"dev": true
},
+ "mini-create-react-context": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
+ "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
+ "requires": {
+ "@babel/runtime": "^7.4.0",
+ "gud": "^1.0.0",
+ "tiny-warning": "^1.0.2"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
+ "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
+ "requires": {
+ "regenerator-runtime": "^0.13.2"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+ "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
+ }
+ }
+ },
"mini-css-extract-plugin": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz",
@@ -17875,9 +17901,9 @@
"dev": true
},
"prettier": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.1.tgz",
- "integrity": "sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==",
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
+ "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
"dev": true
},
"pretty-bytes": {
@@ -18719,39 +18745,32 @@
}
},
"react-router": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.0.tgz",
- "integrity": "sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
+ "integrity": "sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg==",
"requires": {
"@babel/runtime": "^7.1.2",
- "create-react-context": "^0.2.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
+ "mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
- },
- "dependencies": {
- "react-is": {
- "version": "16.8.6",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
- "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
- }
}
},
"react-router-dom": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.0.tgz",
- "integrity": "sha512-wSpja5g9kh5dIteZT3tUoggjnsa+TPFHSMrpHXMpFsaHhQkm/JNVGh2jiF9Dkh4+duj4MKCkwO6H08u6inZYgQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.1.tgz",
+ "integrity": "sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
- "react-router": "5.0.0",
+ "react-router": "5.0.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
diff --git a/package.json b/package.json
index 0b6012a..56eadbc 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,10 @@
"@types/enzyme": "^3.9.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/fetch-mock": "^7.3.0",
- "@types/jest": "^24.0.13",
+ "@types/jest": "^24.0.14",
"@types/lodash.debounce": "^4.0.6",
- "@types/node": "^12.0.4",
+ "@types/lodash.isequal": "^4.5.5",
+ "@types/node": "^12.0.8",
"@types/react": "^16.8.19",
"@types/react-bootstrap-daterangepicker": "0.0.26",
"@types/react-bootstrap-typeahead": "^3.4.5",
@@ -30,24 +31,25 @@
"node-fetch": "^2.6.0",
"node-sass-chokidar": "^1.3.5",
"npm-run-all": "^4.1.5",
- "prettier": "^1.17.1",
+ "prettier": "^1.18.2",
"react-scripts": "^3.0.1",
"react-test-renderer": "^16.8.6",
"typescript": "^3.5.1"
},
"dependencies": {
- "@coreui/coreui": "^2.1.11",
+ "@coreui/coreui": "^2.1.12",
"@coreui/icons": "0.3.0",
"@coreui/react": "^2.5.1",
- "@fortawesome/fontawesome-free": "^5.8.2",
+ "@fortawesome/fontawesome-free": "^5.9.0",
"bootstrap": "^4.3.1",
"bootstrap-daterangepicker": "^3.0.5",
"chart.js": "^2.8.0",
- "i18next": "^17.0.1",
+ "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^3.0.0",
"ionicons": "^4.5.8",
"lodash.debounce": "^4.0.8",
+ "lodash.isequal": "^4.5.0",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
@@ -56,7 +58,7 @@
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
"react-i18next": "^10.11.0",
- "react-router-dom": "^5.0.0",
+ "react-router-dom": "^5.0.1",
"react-table": "^6.10.0",
"reactstrap": "^8.0.0",
"sha.js": "^2.4.11",
@@ -74,6 +76,7 @@
"generate-language-list": "node scripts/generate-language-list.js",
"make-fake-data": "node scripts/make-fake-data.js",
"test": "react-scripts test --env=jsdom",
+ "coverage": "react-scripts test --env=jsdom --coverage",
"eject": "react-scripts eject",
"format": "prettier --write \"src/**/*.tsx\"",
"check-format": "prettier --list-different \"src/**/*.tsx\""
diff --git a/public/index.html b/public/index.html
index 7536bea..74ffa1e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -40,7 +40,7 @@
-
+
diff --git a/src/components/common/FooterUpdateStatus.tsx b/src/components/common/FooterUpdateStatus.tsx
index cabe53c..4a19ebf 100644
--- a/src/components/common/FooterUpdateStatus.tsx
+++ b/src/components/common/FooterUpdateStatus.tsx
@@ -12,19 +12,19 @@ import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Link } from "react-router-dom";
-const FooterUpdateStatus = (props: WithTranslation) => {
- const { t } = props;
- const updateAvailable = false;
-
- if (updateAvailable) {
- return (
-
- {t("Update Available")}
-
- );
- } else {
+const FooterUpdateStatus = ({
+ updateAvailable = false,
+ t
+}: { updateAvailable: boolean } & WithTranslation) => {
+ if (!updateAvailable) {
return null;
}
+
+ return (
+
+ {t("Update Available")}
+
+ );
};
export default withTranslation("footer")(FooterUpdateStatus);
diff --git a/src/components/common/NavButton.tsx b/src/components/common/NavButton.tsx
index ac5fc02..1bf715c 100644
--- a/src/components/common/NavButton.tsx
+++ b/src/components/common/NavButton.tsx
@@ -15,14 +15,10 @@ import React, { MouseEventHandler } from "react";
export interface NavButtonProps {
name: string;
icon: string;
- onClick: MouseEventHandler;
+ onClick?: MouseEventHandler;
}
-export default ({
- name,
- icon,
- onClick = e => e.preventDefault()
-}: NavButtonProps) => (
+export default ({ name, icon, onClick = () => {} }: NavButtonProps) => (
{
wrapper
.find(NavButton)
.at(buttonIndex)
- .props()
- .onClick({} as MouseEvent);
+ .props().onClick!({} as MouseEvent);
// Wait for the setStatus promise to resolve
await tick();
@@ -129,10 +128,7 @@ describe("EnableDisable", () => {
wrapper.setState({ processing: true });
- wrapper
- .find(NavButton)
- .props()
- .onClick({} as MouseEvent);
+ wrapper.find(NavButton).props().onClick!({} as MouseEvent);
expect(setStatus).not.toHaveBeenCalled();
});
@@ -151,10 +147,7 @@ describe("EnableDisable", () => {
/>
);
- wrapper
- .find(NavButton)
- .props()
- .onClick({} as MouseEvent);
+ wrapper.find(NavButton).props().onClick!({} as MouseEvent);
wrapper.unmount();
@@ -175,10 +168,7 @@ describe("EnableDisable", () => {
/>
);
- wrapper
- .find(NavButton)
- .props()
- .onClick({} as MouseEvent);
+ wrapper.find(NavButton).props().onClick!({} as MouseEvent);
expect(wrapper.state().processing).toBeTruthy();
@@ -226,8 +216,7 @@ describe("EnableDisable", () => {
wrapper
.find(NavButton)
.last()
- .props()
- .onClick({} as MouseEvent);
+ .props().onClick!({} as MouseEvent);
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
});
diff --git a/src/components/common/__tests__/FooterUpdateStatus.test.tsx b/src/components/common/__tests__/FooterUpdateStatus.test.tsx
new file mode 100644
index 0000000..1d2f88f
--- /dev/null
+++ b/src/components/common/__tests__/FooterUpdateStatus.test.tsx
@@ -0,0 +1,26 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * FooterUpdateStatus component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import { shallow } from "enzyme";
+import FooterUpdateStatus from "../FooterUpdateStatus";
+import * as React from "react";
+import { Link } from "react-router-dom";
+
+it("renders as null if no update is available", () => {
+ const wrapper = shallow();
+
+ expect(wrapper).toBeEmptyRender();
+});
+
+it("renders a link to the versions page if there is an update", () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(Link).props().to).toEqual("/settings/versions");
+});
diff --git a/src/components/common/__tests__/Header.test.tsx b/src/components/common/__tests__/Header.test.tsx
new file mode 100644
index 0000000..b6cf852
--- /dev/null
+++ b/src/components/common/__tests__/Header.test.tsx
@@ -0,0 +1,69 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * Header component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import React, { MouseEvent } from "react";
+import api from "../../../util/api";
+import { shallow } from "enzyme";
+import Header from "../Header";
+import { TimeRangeSelectorContainer } from "../../dashboard/TimeRangeSelector";
+
+it("shows the time range selector on the dashboard", () => {
+ api.loggedIn = true;
+ history.pushState({}, "", "/dashboard");
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(TimeRangeSelectorContainer)).toExist();
+});
+
+it("does not show the time range selector when not logged in", () => {
+ api.loggedIn = false;
+ history.pushState({}, "", "/dashboard");
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(TimeRangeSelectorContainer)).not.toExist();
+});
+
+it("does not show the time range selector on non-dashboard pages", () => {
+ api.loggedIn = true;
+ history.pushState({}, "", "/whitelist");
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(TimeRangeSelectorContainer)).not.toExist();
+});
+
+it("toggle the sidebar when clicked", () => {
+ const wrapper = shallow();
+
+ expect(document.body.classList).not.toContain("sidebar-minimized");
+ expect(document.body.classList).not.toContain("brand-minimized");
+
+ wrapper
+ .find(".sidebar-toggler")
+ .simulate("click", { preventDefault: () => {} });
+
+ expect(document.body.classList).toContain("sidebar-minimized");
+ expect(document.body.classList).toContain("brand-minimized");
+});
+
+it("toggle the sidebar on mobile when clicked", () => {
+ const wrapper = shallow();
+
+ expect(document.body.classList).not.toContain("sidebar-show");
+
+ wrapper
+ .find(".navbar-toggler")
+ .first()
+ .simulate("click", { preventDefault: () => {} });
+
+ expect(document.body.classList).toContain("sidebar-show");
+});
diff --git a/src/components/common/__tests__/LanguageApplier.test.tsx b/src/components/common/__tests__/LanguageApplier.test.tsx
new file mode 100644
index 0000000..4457719
--- /dev/null
+++ b/src/components/common/__tests__/LanguageApplier.test.tsx
@@ -0,0 +1,60 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * LanguageApplier component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import { mount } from "enzyme";
+import * as React from "react";
+import LanguageApplier from "../LanguageApplier";
+import {
+ PreferencesContext,
+ PreferencesContextType
+} from "../context/PreferencesContext";
+import i18n from "i18next";
+
+it("does not update i18next if the language is already set correctly", () => {
+ const settings: PreferencesContextType = {
+ settings: {
+ language: "en",
+ layout: "boxed"
+ },
+ refresh: () => {}
+ };
+
+ i18n.language = "en";
+ i18n.changeLanguage = jest.fn();
+
+ mount(
+
+
+
+ );
+
+ expect(i18n.changeLanguage).not.toHaveBeenCalled();
+});
+
+it("updates i18next with the language if it was using a different one", () => {
+ const settings: PreferencesContextType = {
+ settings: {
+ language: "es",
+ layout: "boxed"
+ },
+ refresh: () => {}
+ };
+
+ i18n.language = "en";
+ i18n.changeLanguage = jest.fn();
+
+ mount(
+
+
+
+ );
+
+ expect(i18n.changeLanguage).toHaveBeenCalledWith("es");
+});
diff --git a/src/components/common/__tests__/LayoutApplier.test.tsx b/src/components/common/__tests__/LayoutApplier.test.tsx
new file mode 100644
index 0000000..61e906d
--- /dev/null
+++ b/src/components/common/__tests__/LayoutApplier.test.tsx
@@ -0,0 +1,63 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * LayoutApplier component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import React from "react";
+import { mount } from "enzyme";
+import LayoutApplier from "../LayoutApplier";
+import {
+ PreferencesContext,
+ PreferencesContextType
+} from "../context/PreferencesContext";
+
+it("adds box layout CSS when the layout is box", () => {
+ const settings: PreferencesContextType = {
+ settings: {
+ language: "en",
+ layout: "boxed"
+ },
+ refresh: () => {}
+ };
+
+ expect(document.body.classList).not.toContain("boxcontainer");
+ expect(document.body.classList).not.toContain("background-image");
+
+ mount(
+
+
+
+ );
+
+ expect(document.body.classList).toContain("boxcontainer");
+ expect(document.body.classList).toContain("background-image");
+});
+
+it("removes box layout CSS when the layout is traditional", () => {
+ const settings: PreferencesContextType = {
+ settings: {
+ language: "en",
+ layout: "traditional"
+ },
+ refresh: () => {}
+ };
+
+ document.body.classList.add("boxcontainer", "background-image");
+
+ expect(document.body.classList).toContain("boxcontainer");
+ expect(document.body.classList).toContain("background-image");
+
+ mount(
+
+
+
+ );
+
+ expect(document.body.classList).not.toContain("boxcontainer");
+ expect(document.body.classList).not.toContain("background-image");
+});
diff --git a/src/components/common/__tests__/NavButton.test.tsx b/src/components/common/__tests__/NavButton.test.tsx
new file mode 100644
index 0000000..e08761b
--- /dev/null
+++ b/src/components/common/__tests__/NavButton.test.tsx
@@ -0,0 +1,43 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * NavButton component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import NavButton from "../NavButton";
+
+it("renders with the given icon and name", () => {
+ const name = "testName";
+ const icon = "testIcon";
+ const wrapper = shallow();
+
+ expect(wrapper).toHaveText(name);
+ expect(wrapper.find("i")).toHaveClassName(icon);
+});
+
+it("calls onClick when clicked", () => {
+ const onClick = jest.fn();
+ const event = { preventDefault: jest.fn(), test: true };
+ const wrapper = shallow(
+
+ );
+
+ wrapper.find("a").simulate("click", event);
+
+ expect(onClick).toHaveBeenCalledWith(event);
+});
+
+it("has a default onClick", () => {
+ const event = { preventDefault: jest.fn() };
+ const wrapper = shallow();
+
+ wrapper.find("a").simulate("click", event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+});
diff --git a/src/components/common/__tests__/NavDropdown.test.tsx b/src/components/common/__tests__/NavDropdown.test.tsx
new file mode 100644
index 0000000..7803aca
--- /dev/null
+++ b/src/components/common/__tests__/NavDropdown.test.tsx
@@ -0,0 +1,57 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2019 Pi-hole, LLC (https://pi-hole.net)
+ * Network-wide ad blocking via your own hardware.
+ *
+ * Web Interface
+ * NavDropdown component test
+ *
+ * This file is copyright under the latest version of the EUPL.
+ * Please see LICENSE file for your rights under this license. */
+
+import React from "react";
+import { shallow } from "enzyme";
+import NavDropdown from "../NavDropdown";
+
+it("renders as open when isOpen is true", () => {
+ const wrapper = shallow(
+
+ {}
+
+ );
+
+ expect(wrapper.find("li")).toHaveClassName("open");
+});
+
+it("renders as closed when isOpen is false", () => {
+ const wrapper = shallow(
+
+ {}
+
+ );
+
+ expect(wrapper.find("li")).not.toHaveClassName("open");
+});
+
+it("toggles the dropdown when clicked", () => {
+ const toggle = jest.fn();
+ const wrapper = shallow(
+
+ {}
+
+ );
+
+ expect(wrapper.find("li")).toHaveClassName("open");
+
+ wrapper.find("button").simulate("click", {
+ preventDefault: () => {},
+ target: {
+ parentElement: {
+ classList: {
+ toggle
+ }
+ }
+ }
+ });
+
+ expect(toggle).toHaveBeenCalledWith("open");
+});
diff --git a/src/components/common/__tests__/Sidebar.test.tsx b/src/components/common/__tests__/Sidebar.test.tsx
index 283d44a..fe44bf0 100644
--- a/src/components/common/__tests__/Sidebar.test.tsx
+++ b/src/components/common/__tests__/Sidebar.test.tsx
@@ -14,6 +14,7 @@ import Sidebar, { NavList, PiholeNavDropdown, PiholeNavItem } from "../Sidebar";
import api from "../../../util/api";
import { RouteCustomItem, RouteGroup } from "../../../routes";
import NavDropdown from "../NavDropdown";
+import { NavLink } from "react-router-dom";
const t = global.t;
@@ -192,3 +193,25 @@ it("renders the NavList in the sidebar", () => {
expect(props.items).toEqual([item]);
expect(props.location).toEqual(location);
});
+
+it("should hide the sidebar on mobile when an item is clicked", () => {
+ const item = {
+ url: "/testUrl",
+ icon: "test-icon",
+ name: "testName",
+ auth: false,
+ component: () =>
+ };
+ const wrapper = shallow();
+
+ document.body.classList.add("sidebar-show");
+ expect(document.body.classList).toContain("sidebar-show");
+
+ // @ts-ignore
+ wrapper
+ .find(NavLink)
+ .props()
+ .onClick();
+
+ expect(document.body.classList).not.toContain("sidebar-show");
+});
diff --git a/src/components/common/__tests__/StatusBadge.test.tsx b/src/components/common/__tests__/StatusBadge.test.tsx
index 6521d0d..52548e2 100644
--- a/src/components/common/__tests__/StatusBadge.test.tsx
+++ b/src/components/common/__tests__/StatusBadge.test.tsx
@@ -9,8 +9,9 @@
* Please see LICENSE file for your rights under this license. */
import React from "react";
-import { shallow } from "enzyme";
-import { TranslatedStatusBadge } from "../StatusBadge";
+import { mount, shallow } from "enzyme";
+import StatusBadge, { TranslatedStatusBadge } from "../StatusBadge";
+import { StatusContext, StatusContextType } from "../context/StatusContext";
it("shows green enabled message if API returns enabled", async () => {
const wrapper = shallow();
@@ -25,3 +26,19 @@ it("shows red disabled message if API doesn't return enabled", async () => {
expect(wrapper.childAt(2)).toHaveText("Disabled");
expect(wrapper.childAt(0)).toHaveClassName("text-danger");
});
+
+it("uses context to get status", () => {
+ const context: StatusContextType = {
+ status: "enabled",
+ refresh: () => {}
+ };
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(TranslatedStatusBadge).props().status).toEqual(
+ context.status
+ );
+});
diff --git a/src/components/log/QueryLog.tsx b/src/components/log/QueryLog.tsx
index 077c352..b402093 100644
--- a/src/components/log/QueryLog.tsx
+++ b/src/components/log/QueryLog.tsx
@@ -19,6 +19,7 @@ import i18n from "i18next";
import i18next from "i18next";
import { WithTranslation, withTranslation } from "react-i18next";
import debounce from "lodash.debounce";
+import isEqual from "lodash.isequal";
import moment from "moment";
import { padNumber } from "../../util/graphUtils";
import api from "../../util/api";
@@ -201,17 +202,15 @@ class QueryLog extends Component {
* @param pageSize The number of queries in the page
*/
fetchQueries = ({ page, pageSize }: { page: number; pageSize: number }) => {
- // Check if we've reached the end of the queries, or are still waiting for
- // the last fetch to finish
- if (this.state.atEnd || this.state.loading) {
- return;
- }
-
- // Check if the filters are the same and we already have this page and the
- // next page.
+ // Don't fetch the queries if:
+ // - We've reached the end of the queries
+ // - We are still waiting for the last fetch to finish
+ // - Filters are the same and we already have this page and the next
if (
- !this.state.filtersChanged &&
- this.state.history.length >= (page + 2) * pageSize
+ this.state.atEnd ||
+ this.state.loading ||
+ (!this.state.filtersChanged &&
+ this.state.history.length >= (page + 2) * pageSize)
) {
return;
}
@@ -255,7 +254,7 @@ class QueryLog extends Component {
data={this.state.history}
loading={this.state.loading}
onFetchData={state => {
- if (state.filtered === this.state.filters) {
+ if (isEqual(state.filtered, this.state.filters)) {
// If the filters have not changed, do not debounce the fetch.
// This allows fetching the next page to happen without waiting for
// the debounce.
@@ -267,6 +266,10 @@ class QueryLog extends Component {
}
}}
onFilteredChange={debounce(filters => {
+ if (isEqual(filters, this.state.filters)) {
+ return;
+ }
+
this.setState({
filters,
filtersChanged: true,
@@ -276,6 +279,12 @@ class QueryLog extends Component {
history: []
});
}, 300)}
+ defaultFiltered={[
+ {
+ id: "time",
+ value: getDefaultRange(t)
+ }
+ ]}
getTrProps={this.getRowProps}
ofText={this.state.atEnd ? "of" : "of at least"}
// Pad empty rows to have the same height as filled rows
diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss
index 0c90f45..52f9744 100644
--- a/src/scss/_custom.scss
+++ b/src/scss/_custom.scss
@@ -219,3 +219,9 @@ body {
display: none;
}
}
+
+@media (max-width: 576px) {
+ .modal-dialog {
+ margin: 0.5rem auto;
+ }
+}
diff --git a/src/util/__tests__/graphUtils.test.tsx b/src/util/__tests__/graphUtils.test.tsx
index 3258910..98a332a 100644
--- a/src/util/__tests__/graphUtils.test.tsx
+++ b/src/util/__tests__/graphUtils.test.tsx
@@ -12,6 +12,9 @@ import { padNumber, getIntervalForRange } from "../graphUtils";
import { TimeRange } from "../../components/common/context/TimeRangeContext";
import moment from "moment";
+// A timestamp used for testing
+const timestamp = 1559614028;
+
describe("padNumber", () => {
it("pads 0 to 00", () => {
expect(padNumber(0)).toEqual("00");
@@ -30,8 +33,8 @@ describe("getIntervalForRange", () => {
it("returns 10 minutes for 24 hours", () => {
const range: TimeRange = {
name: "24 Hours",
- from: moment().subtract(1, "day"),
- until: moment()
+ from: moment(timestamp).subtract(1, "day"),
+ until: moment(timestamp)
};
expect(getIntervalForRange(range)).toEqual(10 * 60);
@@ -42,8 +45,8 @@ describe("getIntervalForRange", () => {
name: "144 days",
// Use seconds instead of days to ensure the difference in epoch time is
// equal to 144 days
- from: moment().subtract(144 * 24 * 60 * 60, "seconds"),
- until: moment()
+ from: moment(timestamp).subtract(144 * 24 * 60 * 60, "seconds"),
+ until: moment(timestamp)
};
expect(getIntervalForRange(range)).toEqual(24 * 60 * 60);