diff --git a/package-lock.json b/package-lock.json index 1cfc247..3e12456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": "*" @@ -2676,9 +2676,9 @@ "dev": true }, "@types/react": { - "version": "16.8.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.19.tgz", - "integrity": "sha512-QzEzjrd1zFzY9cDlbIiFvdr+YUmefuuRYrPxmkwG0UQv5XF35gFIi7a95m1bNVcFU0VimxSZ5QVGSiBmlggQXQ==", + "version": "16.8.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.20.tgz", + "integrity": "sha512-ZLmI+ubSJpfUIlQuULDDrdyuFQORBuGOvNnMue8HeA0GVrAJbWtZQhcBvnBPNRBI/GrfSfrKPFhthzC2SLEtLQ==", "dev": true, "requires": { "@types/prop-types": "*", @@ -2714,9 +2714,9 @@ } }, "@types/react-router": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.1.tgz", - "integrity": "sha512-vOyVO0u3Cs0w6G5DzYqNVqcTsurEnDgOmmkJf2s7VwtunWzpPgI6dHsCBX68vXqeICpP6jCfojgJcHkm5BV7hQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.2.tgz", + "integrity": "sha512-sdMN284GEOcqDEMS/hE/XD06Abw2fws30+xkZf3C9cSRcWopiv/HDTmunYI7DKLYKVRaWFkq1lkuJ6qeYu0E7A==", "dev": true, "requires": { "@types/history": "*", @@ -2724,9 +2724,9 @@ } }, "@types/react-router-dom": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.3.tgz", - "integrity": "sha512-xj0DmFjgvAqRfh/kJPO7apD5G30yPQe+8slu/dugioQOkdKpyzc4Fgk4hoTelm6CSHz7pI2PPsW5+Y6GRBF2zw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.4.tgz", + "integrity": "sha512-xrwaWHpnxKk/TTRe7pmoGy3E4SyF/ojFqNfFJacw7OLdfLXRvGfk4r/XePVaZNVfeJzL8fcnNilPN7xOdJ/vGw==", "dev": true, "requires": { "@types/history": "*", @@ -21687,9 +21687,9 @@ "dev": true }, "typescript": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz", - "integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 77cf7ef..5643f3c 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,15 @@ "@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/lodash.isequal": "^4.5.5", "@types/node": "^12.0.8", - "@types/react": "^16.8.19", + "@types/react": "^16.8.20", "@types/react-bootstrap-daterangepicker": "0.0.26", "@types/react-bootstrap-typeahead": "^3.4.5", "@types/react-dom": "^16.8.4", - "@types/react-router-dom": "^4.3.3", + "@types/react-router-dom": "^4.3.4", "@types/react-table": "^6.8.3", "@types/reactstrap": "^8.0.1", "@types/sha.js": "^2.4.0", @@ -34,7 +34,7 @@ "prettier": "^1.18.2", "react-scripts": "^3.0.1", "react-test-renderer": "^16.8.6", - "typescript": "^3.5.1" + "typescript": "^3.5.2" }, "dependencies": { "@coreui/coreui": "^2.1.12", diff --git a/src/components/common/WithAPIData.tsx b/src/components/common/WithAPIData.tsx index 2a165b3..e5977d4 100644 --- a/src/components/common/WithAPIData.tsx +++ b/src/components/common/WithAPIData.tsx @@ -102,9 +102,7 @@ export class WithAPIData extends Component< if (cancelOptions) { // If the request should be repeated, wait for the interval and then // refresh with data from the API - new Promise(resolve => - setTimeout(resolve, cancelOptions.interval) - ).then(() => this.loadData()); + setTimeout(() => this.loadData(), cancelOptions.interval); } return; } diff --git a/src/components/common/__tests__/WithAPIData.test.tsx b/src/components/common/__tests__/WithAPIData.test.tsx new file mode 100644 index 0000000..aa7d5a3 --- /dev/null +++ b/src/components/common/__tests__/WithAPIData.test.tsx @@ -0,0 +1,321 @@ +/* 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 + * WithAPIData 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, ShallowWrapper } from "enzyme"; +import { + WithAPIData, + WithAPIDataProps, + WithAPIDataState +} from "../WithAPIData"; + +const tick = global.tick; +const emptyRender = () => null; +const emptyAPICall = () => Promise.resolve({}); + +type WithAPIDataWrapper = ShallowWrapper< + WithAPIDataProps, + WithAPIDataState, + WithAPIData +>; + +it("should use renderInitial at the start", () => { + const renderInitial = jest.fn(() => "test"); + const wrapper = shallow( + + ); + + expect(renderInitial).toHaveBeenCalled(); + expect(wrapper).toHaveText("test"); +}); + +it("should use renderOk after fetching the data", async () => { + const renderOk = jest.fn(data => data); + const wrapper = shallow( + Promise.resolve("test")} + renderOk={renderOk} + /> + ); + + // Let the API call resolve + await tick(); + + expect(renderOk).toHaveBeenCalled(); + expect(wrapper).toHaveText("test"); +}); + +it("should cancel the in-flight request when unmounting", () => { + const renderOk = jest.fn(data => data); + const apiCall = jest.fn(emptyAPICall); + const wrapper = shallow( + + ); + + wrapper.unmount(); + + expect(apiCall).toHaveBeenCalled(); + expect(renderOk).not.toHaveBeenCalled(); + expect(wrapper).not.toHaveText("test"); +}); + +it("should only rerender once if refresh is called twice", async () => { + let called = false; + const renderOk = jest.fn((_, refresh) => { + if (called) return null; + called = true; + refresh(); + refresh(); + return null; + }); + + shallow( + + ); + + // Let the two API calls resolve + await tick(); + await tick(); + + expect(renderOk).toHaveBeenCalledTimes(2); +}); + +it("should ignore cancel errors if the option is enabled", async () => { + const apiCall = () => Promise.reject({ isCanceled: true }); + const renderOk = jest.fn(emptyRender); + const renderErr = jest.fn(emptyRender); + + shallow( + + ); + + // Let the API call resolve + await tick(); + + // If the request was canceled, neither the Ok nor Err renderers would have + // been called + expect(renderErr).not.toHaveBeenCalled(); + expect(renderOk).not.toHaveBeenCalled(); +}); + +it("should throw non-cancel errors when ignoring cancel errors", async () => { + const error = { test: true }; + const apiCall = () => Promise.reject(error); + const renderOk = jest.fn(emptyRender); + const renderErr = jest.fn(() => "test"); + + const wrapper: WithAPIDataWrapper = shallow( + + ); + + // Let the API call resolve + await tick(); + + expect(renderErr).toHaveBeenCalledWith(error, wrapper.instance().loadData); + expect(renderOk).not.toHaveBeenCalled(); + expect(wrapper).toHaveText("test"); +}); + +it("should throw cancel errors when not ignoring cancel", async () => { + const error = { isCanceled: true }; + const apiCall = () => Promise.reject(error); + const renderOk = jest.fn(emptyRender); + const renderErr = jest.fn(() => "test"); + + const wrapper: WithAPIDataWrapper = shallow( + + ); + + // Let the API call resolve + await tick(); + + expect(renderErr).toHaveBeenCalledWith(error, wrapper.instance().loadData); + expect(renderOk).not.toHaveBeenCalled(); + expect(wrapper).toHaveText("test"); +}); + +it("should clear the data when flushOnUpdate is true and props are changed", async () => { + const renderInitial = jest.fn(() => "initial"); + const renderOk = jest.fn(() => "ok"); + const apiCall = jest.fn(() => Promise.resolve("test")); + const wrapper: WithAPIDataWrapper = shallow( + + ); + + // Let the API call resolve + await tick(); + + expect(apiCall).toBeCalledTimes(1); + expect(renderInitial).toHaveBeenCalledTimes(1); + expect(renderOk).toHaveBeenCalledTimes(1); + + // Change the props in a small way + wrapper.setProps({ repeatOptions: { ignoreCancel: false, interval: 0 } }); + + // Let the second API call resolve + await tick(); + + expect(apiCall).toBeCalledTimes(2); + expect(renderInitial).toHaveBeenCalledTimes(2); + expect(renderOk).toHaveBeenCalledTimes(3); +}); + +it("should use the provided data on refresh instead of hitting the API", async () => { + let refreshTest: ((data: any) => void) | undefined = undefined; + const renderOk = jest.fn((data, refresh) => { + refreshTest = refresh; + return data; + }); + const apiCall = jest.fn(() => Promise.resolve("test data")); + const wrapper = shallow( + + ); + + // Let the API call resolve + await tick(); + + // Call the refresh function with the new data + expect(refreshTest).toBeDefined(); + refreshTest!("new data"); + + expect(apiCall).toHaveBeenCalledTimes(1); + expect(renderOk).toHaveBeenCalledTimes(2); + expect(wrapper).toHaveText("new data"); +}); + +it("should wait for the interval after refreshing with provided data", async () => { + jest.useFakeTimers(); + + let refreshTest: ((data: any) => void) | undefined = undefined; + const renderOk = jest.fn((data, refresh) => { + refreshTest = refresh; + return data; + }); + const apiCall = jest.fn(() => Promise.resolve("test data")); + + shallow( + + ); + + // Let the API call resolve + await tick(); + + // Call the refresh function with the new data + expect(refreshTest).toBeDefined(); + refreshTest!("new data"); + + // Wait for the interval + jest.advanceTimersByTime(1000); + + // The API call should have been triggered again + expect(apiCall).toHaveBeenCalledTimes(2); +}); + +it("should pass through repeat options", async () => { + jest.useFakeTimers(); + + const wrapper: WithAPIDataWrapper = shallow( + + ); + + // Let the API call resolve + await tick(); + + expect(setTimeout).toHaveBeenCalledWith(wrapper.instance().loadData, 1000); +}); + +it("should pass through repeat options after refresh", async () => { + jest.useFakeTimers(); + + let refreshTest: ((data: any) => void) | undefined = undefined; + const renderOk = jest.fn((data, refresh) => { + refreshTest = refresh; + return data; + }); + const apiCall = jest.fn(() => Promise.resolve("test data")); + + shallow( + + ); + + // Let the API call resolve + await tick(); + + // Call the refresh function with the new data + expect(refreshTest).toBeDefined(); + refreshTest!("new data"); + + expect(setTimeout).toHaveBeenCalledTimes(2); +}); diff --git a/src/components/settings/FTLInfo.tsx b/src/components/settings/FTLInfo.tsx index f4c4b49..88ee63f 100644 --- a/src/components/settings/FTLInfo.tsx +++ b/src/components/settings/FTLInfo.tsx @@ -10,6 +10,7 @@ import React, { Component } from "react"; import { WithTranslation, withTranslation } from "react-i18next"; +import { Form, Col, Input, FormGroup, Label } from "reactstrap"; import api from "../../util/api"; import { WithAPIData } from "../common/WithAPIData"; @@ -24,13 +25,47 @@ class FTLInfo extends Component { const { t } = this.props; return ( -
-        {t("Queries")}: {this.props.queries}
-        
- {t("Filesize")}: {this.props.fileSize.toLocaleString()} B
- {t("SQLite version")}: {this.props.sqliteVersion.toLocaleString()} -
-
+
+ + + + + + + + + + + + + + + + + + +
); } }