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