Merge branch 'development' into tweak/opendns-ipv6

This commit is contained in:
Mark Drobnak
2019-06-16 14:37:22 -04:00
committed by GitHub
5 changed files with 383 additions and 29 deletions
+15 -15
View File
@@ -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": {
+4 -4
View File
@@ -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",
+1 -3
View File
@@ -102,9 +102,7 @@ export class WithAPIData<T> 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;
}
@@ -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<T> = ShallowWrapper<
WithAPIDataProps<T>,
WithAPIDataState<T>,
WithAPIData<T>
>;
it("should use renderInitial at the start", () => {
const renderInitial = jest.fn(() => "test");
const wrapper = shallow(
<WithAPIData
renderErr={emptyRender}
renderInitial={renderInitial}
apiCall={emptyAPICall}
renderOk={emptyRender}
/>
);
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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={() => 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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
/>
);
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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={emptyAPICall}
renderOk={renderOk}
/>
);
// 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(
<WithAPIData
renderErr={renderErr}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: true, interval: 0 }}
/>
);
// 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<null> = shallow(
<WithAPIData
renderErr={renderErr}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: true, interval: 0 }}
/>
);
// 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<null> = shallow(
<WithAPIData
renderErr={renderErr}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: false, interval: 0 }}
/>
);
// 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<string> = shallow(
<WithAPIData
renderErr={emptyRender}
renderInitial={renderInitial}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: true, interval: 0 }}
flushOnUpdate={true}
/>
);
// 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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
/>
);
// 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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: true, interval: 1000 }}
/>
);
// 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<null> = shallow(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={emptyAPICall}
renderOk={emptyRender}
repeatOptions={{ ignoreCancel: true, interval: 1000 }}
/>
);
// 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(
<WithAPIData
renderErr={emptyRender}
renderInitial={emptyRender}
apiCall={apiCall}
renderOk={renderOk}
repeatOptions={{ ignoreCancel: true, interval: 1000 }}
/>
);
// 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);
});
+42 -7
View File
@@ -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<FTLInfoProps & WithTranslation, {}> {
const { t } = this.props;
return (
<pre>
{t("Queries")}: {this.props.queries}
<br />
{t("Filesize")}: {this.props.fileSize.toLocaleString()} B<br />
{t("SQLite version")}: {this.props.sqliteVersion.toLocaleString()}
<br />
</pre>
<Form>
<FormGroup row>
<Label className="bold" for="queries" sm={4}>
{t("Queries")}
</Label>
<Col sm={8}>
<Input
plaintext
readOnly
id="queries"
value={this.props.queries.toLocaleString()}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label className="bold" for="filesize" sm={4}>
{t("Filesize")}
</Label>
<Col sm={8}>
<Input
plaintext
readOnly
id="filesize"
value={`${this.props.fileSize.toLocaleString()} B`}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label className="bold" for="sqliteversion" sm={4}>
{t("SQLite version")}
</Label>
<Col sm={8}>
<Input
plaintext
readOnly
id="sqliteversion"
value={this.props.sqliteVersion}
/>
</Col>
</FormGroup>
</Form>
);
}
}